Files
acfunlive-remake/qualities_test.go
T
tamakyi eda83dd1e3 主播列表的码率改为直播实际档位下拉,没选过时默认第二档
原来是一个手填的码率上限(Kbps),现在改成直播的实际档位(蓝光 8M / 高清…):

- 读到档位后,主播那一行渲染成下拉,选完立即生效;正在录制时换档会自动重录。
- 没手动选过(bitrate 0)的主播按**自上往下第二档**定下来(如
  蓝光 8M / 蓝光 4M / 高清 就选蓝光 4M,只有一档就用那一档),不会一上来就吃满
  最高档;已经手动选过的永远不动。
- 档位要开播后读直播源列表才知道,读到以后(录制开始时、或页面点「档位」按钮)
  缓存进 config.json 的 qualities,所以没开播也能看和改;启动时会把「有缓存但还
  没定过码率」的主播按第二档补齐。
- 从来没读过档位的行仍然是手填输入框,旁边多了个「档位」按钮去直播间接一次。

后端:新增 `GET /api/streamer/qualities?uid=…`(同一主播 10 秒内只读一次),
`/api/status` 的每个主播多了 `qualities`(按码率从高到低)和 `qualityName`;
抽出 `newStreamInfo()`/`fetchStreamQualities()`,`getStreamURL()` 取流时顺手缓存
档位,并在 bitrate 为 0 时改用刚定下来的默认档位(否则还是最高档)。
档位相关的纯逻辑放在 config.go:`sortQualitiesDesc` / `defaultQualityBitrate` /
`qualityNameFor` / `sameQualities` / `setStreamerQualities` / `applyCachedDefaultQualities`。

测试:qualities_test.go 7 个用例(排序、默认第二档的 5 种档位数、档位名匹配、
写配置时不覆盖手动选择、启动补齐、JSON 存取往返);浏览器实测 5 组断言全过
(下拉选中当前档位、换档服务端立即变、无档位时保留输入框+按钮、启动默认第二档、
手填仍可用),verify_streamer_br.py / verify_pick.py / verify_settings.py 无回归;
go test ./...、gofmt、go vet 干净。
2026-09-25 19:42:47 +08:00

201 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func tiersForTest() []qualityTier {
// 故意乱序,验证排序
return []qualityTier{
{Name: "高清", Bitrate: 2000},
{Name: "蓝光 8M", Bitrate: 8000},
{Name: "标清", Bitrate: 1000},
{Name: "蓝光 4M", Bitrate: 4000},
}
}
func TestSortQualitiesDesc(t *testing.T) {
got := sortQualitiesDesc(tiersForTest())
want := []string{"蓝光 8M", "蓝光 4M", "高清", "标清"}
for i, name := range want {
if got[i].Name != name {
t.Fatalf("第 %d 个应为 %s,实际 %s(%v)", i, name, got[i].Name, got)
}
}
// 不能改动传进来的切片
in := tiersForTest()
_ = sortQualitiesDesc(in)
if in[0].Name != "高清" {
t.Errorf("排序不该改动原切片:%v", in)
}
}
// 默认档位 = 自上往下第二档(不是最高档)
func TestDefaultQualityBitrate(t *testing.T) {
cases := []struct {
name string
tiers []qualityTier
want int
}{
{"四档取第二高", tiersForTest(), 4000},
{"三档取第二高(你给的例子)", []qualityTier{{"高清", 2000}, {"蓝光 8M", 8000}, {"蓝光 4M", 4000}}, 4000},
{"两档取低的那档", []qualityTier{{"蓝光 4M", 4000}, {"高清", 2000}}, 2000},
{"只有一档就用它", []qualityTier{{"高清", 2000}}, 2000},
{"没有档位返回 0", nil, 0},
}
for _, c := range cases {
if got := defaultQualityBitrate(c.tiers); got != c.want {
t.Errorf("%s:应为 %d,实际 %d", c.name, c.want, got)
}
}
}
func TestQualityNameFor(t *testing.T) {
if got := qualityNameFor(tiersForTest(), 4000); got != "蓝光 4M" {
t.Errorf("4000 应匹配「蓝光 4M」,实际 %q", got)
}
if got := qualityNameFor(tiersForTest(), 9999); got != "" {
t.Errorf("匹配不上应返回空,实际 %q", got)
}
}
func TestSameQualities(t *testing.T) {
a := tiersForTest()
b := []qualityTier{{"标清", 1000}, {"蓝光 4M", 4000}, {"高清", 2000}, {"蓝光 8M", 8000}}
if !sameQualities(a, b) {
t.Error("顺序不同但内容相同的档位列表应视为一样")
}
c := append([]qualityTier(nil), a[:3]...)
if sameQualities(a, c) {
t.Error("档位数量不同应视为不一样")
}
}
// 记录档位时顺便把「还没设过码率」的主播按默认档位定下来
func TestSetStreamerQualities(t *testing.T) {
savedConfig := config
savedStreamers := streamers.crt
defer func() {
config = savedConfig
streamers.crt = savedStreamers
}()
config.UIDs = uidList{
{UID: 1, Danmu: true, Record: true, Bitrate: 0}, // 没设过 → 应被定成第二档
{UID: 2, Danmu: true, Record: true, Bitrate: 1000}, // 手动设过 → 不动
}
streamers.crt = map[int]streamer{
1: {UID: 1, Bitrate: 0},
2: {UID: 2, Bitrate: 1000},
}
if !setStreamerQualities(1, tiersForTest()) {
t.Error("第一次记录档位应该算改动了配置")
}
if config.UIDs[0].Bitrate != 4000 {
t.Errorf("uid 1 没设过码率,应被定成第二档 4000,实际 %d", config.UIDs[0].Bitrate)
}
if streamers.crt[1].Bitrate != 4000 || len(streamers.crt[1].Qualities) != 4 {
t.Errorf("运行时的主播数据没同步:%+v", streamers.crt[1])
}
// 手动设过的不能被默认档位覆盖
setStreamerQualities(2, tiersForTest())
if config.UIDs[1].Bitrate != 1000 {
t.Errorf("uid 2 手动设过 1000,不该被改成 %d", config.UIDs[1].Bitrate)
}
// 同样的档位再来一次不算改动
if setStreamerQualities(1, tiersForTest()) {
t.Error("档位没变化时不该算改动")
}
// 档位变了才算改动
if !setStreamerQualities(1, []qualityTier{{"高清", 2000}, {"流畅", 500}}) {
t.Error("档位变化时应算改动")
}
}
// 启动时按缓存里的档位把默认码率补齐
func TestApplyCachedDefaultQualities(t *testing.T) {
savedConfig := config
savedStreamers := streamers.crt
defer func() {
config = savedConfig
streamers.crt = savedStreamers
}()
config.UIDs = uidList{
{UID: 1, Bitrate: 0, Qualities: tiersForTest()}, // 有缓存、没定过 → 补上
{UID: 2, Bitrate: 2000, Qualities: tiersForTest()}, // 已经有值 → 不动
{UID: 3, Bitrate: 0}, // 没缓存 → 不动
}
streamers.crt = map[int]streamer{1: {UID: 1}, 2: {UID: 2, Bitrate: 2000}, 3: {UID: 3}}
if !applyCachedDefaultQualities() {
t.Fatal("有需要补的主播时应算改动")
}
if config.UIDs[0].Bitrate != 4000 {
t.Errorf("uid 1 应补成 4000,实际 %d", config.UIDs[0].Bitrate)
}
if config.UIDs[1].Bitrate != 2000 {
t.Errorf("uid 2 不该被改动,实际 %d", config.UIDs[1].Bitrate)
}
if config.UIDs[2].Bitrate != 0 {
t.Errorf("uid 3 没有档位缓存,不该被改动,实际 %d", config.UIDs[2].Bitrate)
}
}
// 档位要能存进 config.json 再读回来
func TestQualitiesConfigRoundTrip(t *testing.T) {
savedConfig := config
savedLocation := configFileLocation
oldDir := configDir
if configDir == nil {
configDir = new(string)
}
defer func() {
config = savedConfig
configFileLocation = savedLocation
configDir = oldDir
}()
dir := t.TempDir()
*configDir = dir
configFileLocation = filepath.Join(dir, configFile)
body := `{"uids":[{"uid":23682490,"danmu":true,"record":true,"bitrate":4000,
"qualities":[{"name":"蓝光 8M","bitrate":8000},{"name":"蓝光 4M","bitrate":4000}]}]}`
if err := os.WriteFile(configFileLocation, []byte(body), 0644); err != nil {
t.Fatal(err)
}
config = configData{}
loadConfig()
if len(config.UIDs) != 1 {
t.Fatalf("应解析出 1 个主播,实际 %d", len(config.UIDs))
}
u := config.UIDs[0]
if u.Bitrate != 4000 || len(u.Qualities) != 2 || u.Qualities[0].Name != "蓝光 8M" {
t.Fatalf("档位没正确读出来:%+v", u)
}
if got := qualityNameFor(u.Qualities, u.Bitrate); got != "蓝光 4M" {
t.Errorf("当前档位名应为「蓝光 4M」,实际 %q", got)
}
// 再序列化一次,字段名要对得上(qualities / name / bitrate)
config.UIDs[0].Qualities = sortQualitiesDesc(config.UIDs[0].Qualities)
data, err := json.Marshal(config.UIDs[0])
if err != nil {
t.Fatal(err)
}
for _, key := range []string{`"qualities"`, `"name"`, `"bitrate"`} {
if !json.Valid(data) || !strings.Contains(string(data), key) {
t.Errorf("序列化结果里应包含 %s:%s", key, data)
}
}
}