录制档位改成「最高/第二高/最低/自定义」四种相对偏好,没开播也能先设

上一版的下拉里列的是「这次读到的实际档位」,没读到档位表(没开播)时下拉里就只剩
一项,想预先设「我要最高档」或「最低档」都做不到。改成按「实际档位里的第几档」来选:

- 新增 `qualityPref`:top(最高档)/ second(第二高,默认)/ lowest(最低档)/
  custom(取不超过给定码率的最高档,都比它高时退回最低档)。四种偏好任何时候都
  在列表里,开播后自动对应到实际档位;读到档位表后把对应结果标在选项后面
  (`第二高(蓝光 4M)`、`自定义(≤2500K = 高清)`)。
- `bitrate` 只在 custom 时有意义。`resolveQualityBitrate(tiers, pref, custom)` 统一
  解析:相对偏好没有档位表时返回 0(取流退回最高档),custom 返回填的上限。
  解析只在取流时做,所以主播换了档位表,偏好会跟着一起变。
- 老配置兼容:没有 qualityPref 时,`bitrate > 0` 当成 custom(就是原来的「码率上限」,
  行为不变),`bitrate == 0` 当成默认的第二高;在 checkConfig 里补齐(不写盘,下次
  保存自然带上)。`setBitrate` 相应换成 `setQuality(uid, pref, custom)`。
- 页面:选 custom 时旁边出现码率框,初始值就是**当前实际在用的那一档**(不会一切过去
  就掉到最低档),回车或失焦即提交,填错给提示并回填服务端的值;从 custom 切到别的
  档位不带 bitrate,服务端保留原值,改回来还是原来那个数。
- 设置区同步:新主播的默认档位也用同一套四项(`defaultQualityPref`),只有选自定义
  才露出码率上限框(切过去会补一个能用的 3000,免得新主播默默用最低档)。

顺带修一个脚本抓到的真 bug:档位名传成了档位对象(`esc()` 里会 `.replace`),只要
档位表非空就会抛异常,而 refresh() 整段包在 try/catch 里 —— 表现是整个主播列表
变成空白且没有任何报错。现在取的是 `qs[i].name`。

测试:qualities_test.go 重写 TestResolveQualityBitrate(10 种偏好/码率组合)并新增
TestMigrateQualityPref、TestUpgradeQualityPrefs、TestSetQuality;verify_streamer_br.py
改成带迷你后端的假服务(POST 会真正落到假状态上),30 项断言全过;verify_qualities.py
25 项(含设置区与 config.json 往返)全过;verify_settings.py / verify_pick.py /
verify_danmu_match.py 无回归;go test ./...、gofmt、go vet 干净。
This commit is contained in:
tamakyi
2026-09-25 20:42:22 +08:00
parent 7b853c1a22
commit 237fffa8fe
6 changed files with 549 additions and 215 deletions
+136 -21
View File
@@ -123,38 +123,153 @@ func TestSetStreamerQualities(t *testing.T) {
}
}
// 实际用哪个档位:0 = 自动(第二高),填了具体码率 = 不超过它的最高档,
// 都比它高时退回最低档
// 按偏好决定实际用哪个档位:最高/第二高/最低按实际档位排序取,
// 自定义取不超过给定码率的最高档、都比它高时退回最低档
func TestResolveQualityBitrate(t *testing.T) {
cases := []struct {
name string
bitrate int
want int
name string
pref string
custom int
want int
}{
{"自动取第二高", 0, 4000},
{"指定最高档", 8000, 8000},
{"指定第二高", 4000, 4000},
{"指定最低档", 1000, 1000},
{"填了中间值取不超过它的最高档", 3000, 2000},
{"比所有档位都高就取最高档", 99999, 8000},
{"比所有档位都低就退回最低档", 200, 1000},
{"最高档", qualityPrefTop, 0, 8000},
{"第二高(默认)", qualityPrefSecond, 0, 4000},
{"最低档", qualityPrefLowest, 0, 1000},
{"空偏好按默认的第二高", "", 0, 4000},
{"没见过的偏好也按第二高", "bogus", 0, 4000},
{"自定义取不超过上限的最高档", qualityPrefCustom, 3000, 2000},
{"自定义上限正好等于某一档", qualityPrefCustom, 4000, 4000},
{"自定义上限比所有档位都高就取最高档", qualityPrefCustom, 99999, 8000},
{"自定义上限比所有档位都低就退回最低档", qualityPrefCustom, 200, 1000},
{"自定义填 0 等于最低档", qualityPrefCustom, 0, 1000},
}
for _, c := range cases {
if got := resolveQualityBitrate(tiersForTest(), c.bitrate); got != c.want {
if got := resolveQualityBitrate(tiersForTest(), c.pref, c.custom); got != c.want {
t.Errorf("%s:应为 %d,实际 %d", c.name, c.want, got)
}
}
// 没有档位表时保持原样(取流逻辑会退回最高档)
if got := resolveQualityBitrate(nil, 0); got != 0 {
t.Errorf("没有档位表时应返回 0,实际 %d", got)
// 没有档位表时:相对偏好返回 0(取流时退回最高档),自定义返回填的上限
if got := resolveQualityBitrate(nil, qualityPrefSecond, 0); got != 0 {
t.Errorf("没有档位表时相对偏好应返回 0,实际 %d", got)
}
if got := resolveQualityBitrate(nil, 2000); got != 2000 {
t.Errorf("没有档位表时应原样返回,实际 %d", got)
if got := resolveQualityBitrate(nil, qualityPrefCustom, 2500); got != 2500 {
t.Errorf("没有档位表时自定义应返回填的 2500,实际 %d", got)
}
// 只有一档时自动档就是它
// 只有一档时:最高/第二高都是它,最低也是它
one := []qualityTier{{"高清", 2000}}
if got := resolveQualityBitrate(one, 0); got != 2000 {
t.Errorf("只有一档时自动应为 2000,实际 %d", got)
for _, pref := range []string{qualityPrefTop, qualityPrefSecond, qualityPrefLowest} {
if got := resolveQualityBitrate(one, pref, 0); got != 2000 {
t.Errorf("只有一档时 %s 应为 2000,实际 %d", pref, got)
}
}
}
// 老配置只有 bitrate,读进来时要能推断出偏好:>0 是「自定义上限」,0 是默认的第二高
func TestMigrateQualityPref(t *testing.T) {
cases := []struct {
pref string
bitrate int
want string
}{
{"", 0, qualityPrefSecond},
{"", 5000, qualityPrefCustom},
{"", 1000, qualityPrefCustom},
{qualityPrefTop, 0, qualityPrefTop},
{qualityPrefLowest, 5000, qualityPrefLowest},
{qualityPrefCustom, 3000, qualityPrefCustom},
{" ", 0, qualityPrefSecond}, // 认不出来的值按默认
{"TOP", 0, qualityPrefSecond}, // 大小写不敏感不做,保持严格
}
for _, c := range cases {
if got := migrateQualityPref(c.pref, c.bitrate); got != c.want {
t.Errorf("pref=%q bitrate=%d 应为 %s,实际 %s", c.pref, c.bitrate, c.want, got)
}
}
// normalizeQualityPref 对四种合法值原样返回
for _, p := range []string{qualityPrefTop, qualityPrefSecond, qualityPrefLowest, qualityPrefCustom} {
if got := normalizeQualityPref(p); got != p {
t.Errorf("%s 应原样返回,实际 %s", p, got)
}
}
}
// 升级老配置:config.UIDs 和默认档位都要补上 qualityPref
func TestUpgradeQualityPrefs(t *testing.T) {
savedConfig := config
defer func() { config = savedConfig }()
config.UIDs = uidList{
{UID: 1}, // 老配置:bitrate 0 → 第二高
{UID: 2, Bitrate: 5000}, // 老配置:有码率 → 自定义
{UID: 3, QualityPref: qualityPrefTop}, // 已经是新配置 → 不动
}
config.DefaultQualityPref = ""
config.DefaultBitrate = 0
upgradeQualityPrefs()
if config.UIDs[0].QualityPref != qualityPrefSecond {
t.Errorf("uid 1 应补成 second,实际 %s", config.UIDs[0].QualityPref)
}
if config.UIDs[1].QualityPref != qualityPrefCustom {
t.Errorf("uid 2 应补成 custom,实际 %s", config.UIDs[1].QualityPref)
}
if config.UIDs[2].QualityPref != qualityPrefTop {
t.Errorf("uid 3 不该被改动,实际 %s", config.UIDs[2].QualityPref)
}
if config.DefaultQualityPref != qualityPrefSecond {
t.Errorf("默认档位应补成 second,实际 %s", config.DefaultQualityPref)
}
// 幂等
upgradeQualityPrefs()
if config.UIDs[1].QualityPref != qualityPrefCustom {
t.Error("重复升级不该改变结果")
}
}
// 改档位偏好:写配置 + 同步运行时数据
func TestSetQuality(t *testing.T) {
savedConfig := config
savedStreamers := streamers.crt
savedLocation := configFileLocation
oldDir := configDir
if configDir == nil {
configDir = new(string)
}
defer func() {
config = savedConfig
streamers.crt = savedStreamers
configFileLocation = savedLocation
configDir = oldDir
}()
*configDir = t.TempDir()
configFileLocation = filepath.Join(*configDir, configFile)
config.UIDs = uidList{{UID: 7, QualityPref: qualityPrefSecond}}
streamers.crt = map[int]streamer{7: {UID: 7, QualityPref: qualityPrefSecond}}
if !setQuality(7, qualityPrefTop, 0) {
t.Fatal("改档位应该成功")
}
if config.UIDs[0].QualityPref != qualityPrefTop || streamers.crt[7].QualityPref != qualityPrefTop {
t.Errorf("档位偏好没同步:%+v / %+v", config.UIDs[0], streamers.crt[7])
}
// 自定义时码率上限也要存下来
if !setQuality(7, qualityPrefCustom, 2500) {
t.Fatal("改成自定义应该成功")
}
if streamers.crt[7].Bitrate != 2500 || config.UIDs[0].Bitrate != 2500 {
t.Errorf("自定义码率没存:%+v / %+v", config.UIDs[0], streamers.crt[7])
}
// 认不出来的偏好按默认处理,负数码率直接拒绝
setQuality(7, "bogus", 2500)
if streamers.crt[7].QualityPref != qualityPrefSecond {
t.Errorf("非法偏好应按默认第二高,实际 %s", streamers.crt[7].QualityPref)
}
if setQuality(7, qualityPrefCustom, -1) {
t.Error("负数码率应该被拒绝")
}
if setQuality(999, qualityPrefTop, 0) {
t.Error("不在列表里的 uid 应该失败")
}
}