主播列表的码率改为直播实际档位下拉,没选过时默认第二档
原来是一个手填的码率上限(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 干净。
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user