录制档位改成「最高/第二高/最低/自定义」四种相对偏好,没开播也能先设
上一版的下拉里列的是「这次读到的实际档位」,没读到档位表(没开播)时下拉里就只剩 一项,想预先设「我要最高档」或「最低档」都做不到。改成按「实际档位里的第几档」来选: - 新增 `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:
@@ -26,12 +26,57 @@ var configFileLocation string // 设置文件位置
|
||||
|
||||
// 主播的设置数据
|
||||
type streamer struct {
|
||||
UID int `json:"uid"` // 主播 uid
|
||||
Name string `json:"name"` // 主播名字
|
||||
Danmu bool `json:"-"` // 是否录制直播弹幕
|
||||
Record bool `json:"-"` // 是否录制直播视频
|
||||
Bitrate int `json:"-"` // 选定的档位码率(Kbps),0 = 自动(按实际档位取第二高)
|
||||
Qualities []qualityTier `json:"-"` // 上次探测到的码率档位,没开播时页面靠它显示下拉
|
||||
UID int `json:"uid"` // 主播 uid
|
||||
Name string `json:"name"` // 主播名字
|
||||
Danmu bool `json:"-"` // 是否录制直播弹幕
|
||||
Record bool `json:"-"` // 是否录制直播视频
|
||||
QualityPref string `json:"-"` // 档位偏好:最高/第二高/最低/自定义(见 qualityPref* 常量)
|
||||
Bitrate int `json:"-"` // 自定义档位时的码率上限(Kbps),只在 QualityPref 为 custom 时有意义
|
||||
Qualities []qualityTier `json:"-"` // 上次探测到的码率档位,没开播时页面靠它显示对应关系
|
||||
}
|
||||
|
||||
// 录制档位的偏好。档位名每次开播都可能不一样(不一定都有「蓝光 8M」),
|
||||
// 所以按「实际档位里的第几档」来选,不开播也能先设好,开播后自动对应上去。
|
||||
const (
|
||||
qualityPrefTop = "top" // 最高档
|
||||
qualityPrefSecond = "second" // 自上往下第二高(默认)
|
||||
qualityPrefLowest = "lowest" // 最低档
|
||||
qualityPrefCustom = "custom" // 自定义:不超过指定码率的最高档
|
||||
qualityPrefDefault = qualityPrefSecond
|
||||
)
|
||||
|
||||
// 把档位偏好规整成合法值,空值或没见过的值一律当默认(第二高)
|
||||
func normalizeQualityPref(pref string) string {
|
||||
switch pref {
|
||||
case qualityPrefTop, qualityPrefSecond, qualityPrefLowest, qualityPrefCustom:
|
||||
return pref
|
||||
}
|
||||
return qualityPrefDefault
|
||||
}
|
||||
|
||||
// 老配置里没有 qualityPref,只有 bitrate:填了码率就是「自定义上限」,0 就是默认的第二高
|
||||
func migrateQualityPref(pref string, bitrate int) string {
|
||||
if pref == "" {
|
||||
if bitrate > 0 {
|
||||
return qualityPrefCustom
|
||||
}
|
||||
return qualityPrefDefault
|
||||
}
|
||||
return normalizeQualityPref(pref)
|
||||
}
|
||||
|
||||
// 档位偏好对应的中文说法,用于日志
|
||||
func qualityPrefText(pref string) string {
|
||||
switch normalizeQualityPref(pref) {
|
||||
case qualityPrefTop:
|
||||
return "最高档"
|
||||
case qualityPrefLowest:
|
||||
return "最低档"
|
||||
case qualityPrefCustom:
|
||||
return "自定义"
|
||||
default:
|
||||
return "第二高"
|
||||
}
|
||||
}
|
||||
|
||||
// 直播源的码率档位
|
||||
@@ -42,11 +87,12 @@ type qualityTier struct {
|
||||
|
||||
// 单个主播的开关设置
|
||||
type uidSetting struct {
|
||||
UID int `json:"uid"`
|
||||
Danmu bool `json:"danmu"`
|
||||
Record bool `json:"record"`
|
||||
Bitrate int `json:"bitrate"` // 选定的档位码率(Kbps),0 = 自动(按实际档位取第二高)
|
||||
Qualities []qualityTier `json:"qualities,omitempty"` // 上次探测到的档位,缓存下来让没开播时也能在页面上选
|
||||
UID int `json:"uid"`
|
||||
Danmu bool `json:"danmu"`
|
||||
Record bool `json:"record"`
|
||||
QualityPref string `json:"qualityPref,omitempty"` // 档位偏好,留空按老规则从 bitrate 推(见 migrateQualityPref)
|
||||
Bitrate int `json:"bitrate"` // 自定义档位时的码率上限(Kbps)
|
||||
Qualities []qualityTier `json:"qualities,omitempty"` // 上次探测到的档位,缓存下来让没开播时也能显示对应关系
|
||||
}
|
||||
|
||||
// uidSetting 列表,兼容旧的纯数字数组格式(旧格式默认弹幕和录播都开启)
|
||||
@@ -93,7 +139,9 @@ type configData struct {
|
||||
MergeBitrate int `json:"mergeBitrate"` // 合并成片的目标码率(Kbps),0 表示使用默认质量
|
||||
DanmuFontSize int `json:"danmuFontSize"` // 合并时弹幕文字的字号(头像随字号等比缩放),0 表示跟随弹幕文件
|
||||
|
||||
DefaultBitrate int `json:"defaultBitrate"` // 新添加主播默认选的档位码率(Kbps),0 = 自动(按实际档位取第二高)
|
||||
// 新添加主播的默认录制档位(留空/非法按第二高)与自定义时的码率上限
|
||||
DefaultQualityPref string `json:"defaultQualityPref,omitempty"`
|
||||
DefaultBitrate int `json:"defaultBitrate"`
|
||||
|
||||
WebExternal bool `json:"webExternal"` // 是否允许外网访问 web UI,false 时只监听本机
|
||||
|
||||
@@ -426,6 +474,8 @@ func checkConfig() {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
// 老配置没有 qualityPref,这里按 bitrate 推一个出来,不写盘(下次保存时自然带上)
|
||||
upgradeQualityPrefs()
|
||||
}
|
||||
|
||||
// 清掉上次运行残留的合成临时文件,只在启动时调用一次。
|
||||
@@ -448,6 +498,7 @@ func buildStreamers() {
|
||||
if s, ok := streamers.crt[u.UID]; ok {
|
||||
s.Danmu = u.Danmu
|
||||
s.Record = u.Record
|
||||
s.QualityPref = u.QualityPref
|
||||
s.Bitrate = u.Bitrate
|
||||
s.Qualities = u.Qualities
|
||||
news[u.UID] = s
|
||||
@@ -457,11 +508,26 @@ func buildStreamers() {
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("uid%d", u.UID)
|
||||
}
|
||||
news[u.UID] = streamer{UID: u.UID, Name: name, Danmu: u.Danmu, Record: u.Record, Bitrate: u.Bitrate, Qualities: u.Qualities}
|
||||
news[u.UID] = streamer{UID: u.UID, Name: name, Danmu: u.Danmu, Record: u.Record,
|
||||
QualityPref: u.QualityPref, Bitrate: u.Bitrate, Qualities: u.Qualities}
|
||||
}
|
||||
streamers.crt = news
|
||||
}
|
||||
|
||||
// 老配置里没有 qualityPref(只有 bitrate),读进来时补一个,并把结果写回 config.UIDs,
|
||||
// 这样运行时各处读到的偏好都是确定的,下次保存也就把新字段落了盘
|
||||
func upgradeQualityPrefs() {
|
||||
for i := range config.UIDs {
|
||||
want := migrateQualityPref(config.UIDs[i].QualityPref, config.UIDs[i].Bitrate)
|
||||
if config.UIDs[i].QualityPref != want {
|
||||
config.UIDs[i].QualityPref = want
|
||||
}
|
||||
}
|
||||
if want := migrateQualityPref(config.DefaultQualityPref, config.DefaultBitrate); config.DefaultQualityPref != want {
|
||||
config.DefaultQualityPref = want
|
||||
}
|
||||
}
|
||||
|
||||
// 档位按码率从高到低排(页面的下拉按这个顺序显示)
|
||||
func sortQualitiesDesc(tiers []qualityTier) []qualityTier {
|
||||
out := append([]qualityTier(nil), tiers...)
|
||||
@@ -469,8 +535,8 @@ func sortQualitiesDesc(tiers []qualityTier) []qualityTier {
|
||||
return out
|
||||
}
|
||||
|
||||
// 没手动选过码率(0)时的默认档位:按码率自上往下取第二档;
|
||||
// 只有一个档位就用它,没有档位返回 0(交给调用方保持原样)
|
||||
// 没手动选过(第二高)时的默认档位:按码率自上往下取第二档;
|
||||
// 只有一个档位就用它;没有档位返回 0
|
||||
func defaultQualityBitrate(tiers []qualityTier) int {
|
||||
desc := sortQualitiesDesc(tiers)
|
||||
switch len(desc) {
|
||||
@@ -534,23 +600,32 @@ func setStreamerQualities(uid int, tiers []qualityTier) bool {
|
||||
return changed
|
||||
}
|
||||
|
||||
// 按选定的码率决定实际用哪个档位:
|
||||
// 0(自动)取自上往下第二档(只有一个档位就用它);填了具体码率就取不超过它的最高档,
|
||||
// 都比它高时退回最低档。档位列表为空返回 0,交给调用方按老规矩取最高档。
|
||||
func resolveQualityBitrate(tiers []qualityTier, bitrate int) int {
|
||||
// 按偏好决定实际用哪个档位,返回档位码率:
|
||||
// 最高/第二高/最低按本次读到的档位排序取;自定义取不超过 custom 的最高档,都比它高时退回最低档。
|
||||
// 档位列表为空时返回 0(自定义时返回 custom),交给调用方按老规矩取最高档。
|
||||
func resolveQualityBitrate(tiers []qualityTier, pref string, custom int) int {
|
||||
desc := sortQualitiesDesc(tiers)
|
||||
if len(desc) == 0 {
|
||||
return bitrate
|
||||
if normalizeQualityPref(pref) == qualityPrefCustom {
|
||||
return custom
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if bitrate <= 0 {
|
||||
switch normalizeQualityPref(pref) {
|
||||
case qualityPrefTop:
|
||||
return desc[0].Bitrate
|
||||
case qualityPrefLowest:
|
||||
return desc[len(desc)-1].Bitrate
|
||||
case qualityPrefCustom:
|
||||
for _, t := range desc {
|
||||
if t.Bitrate <= custom {
|
||||
return t.Bitrate
|
||||
}
|
||||
}
|
||||
return desc[len(desc)-1].Bitrate
|
||||
default:
|
||||
return defaultQualityBitrate(desc)
|
||||
}
|
||||
for _, t := range desc {
|
||||
if t.Bitrate <= bitrate {
|
||||
return t.Bitrate
|
||||
}
|
||||
}
|
||||
return desc[len(desc)-1].Bitrate
|
||||
}
|
||||
|
||||
// 从文件(成片或录播)名里猜是哪个主播:名字出现在文件名里、且只有一个能对上时才认,
|
||||
@@ -683,7 +758,8 @@ func setStreamerFlags(uid int, danmu, record bool) bool {
|
||||
lPrintWarnf("不存在 uid 为%d的用户", uid)
|
||||
return false
|
||||
}
|
||||
config.UIDs = append(config.UIDs, uidSetting{UID: uid, Danmu: danmu, Record: record, Bitrate: config.DefaultBitrate})
|
||||
config.UIDs = append(config.UIDs, uidSetting{UID: uid, Danmu: danmu, Record: record,
|
||||
QualityPref: normalizeQualityPref(config.DefaultQualityPref), Bitrate: config.DefaultBitrate})
|
||||
}
|
||||
// 同步 streamers.crt 里的开关
|
||||
if s, ok := streamers.crt[uid]; ok {
|
||||
@@ -710,12 +786,14 @@ func setStreamerFlags(uid int, danmu, record bool) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// 设置指定主播的录播码率上限(Kbps,0 为码率最高的直播源)并保存,正在录制时自动重启录制让新码率生效
|
||||
func setBitrate(uid, bitrate int) bool {
|
||||
if uid <= 0 || bitrate < 0 {
|
||||
lPrintWarn("uid 必须大于 0,bitrate 不能小于 0")
|
||||
// 改某个主播的录制档位偏好(pref 见 qualityPref* 常量,custom 是自定义时的码率上限)。
|
||||
// 正在录制的会把这一段停掉重开,让新档位立刻生效
|
||||
func setQuality(uid int, pref string, custom int) bool {
|
||||
if uid <= 0 || custom < 0 {
|
||||
lPrintWarn("uid 必须大于 0,自定义码率不能小于 0")
|
||||
return false
|
||||
}
|
||||
pref = normalizeQualityPref(pref)
|
||||
|
||||
wasRecording := false
|
||||
if infoList, ok := getLiveInfoByUID(uid); ok {
|
||||
@@ -730,14 +808,16 @@ func setBitrate(uid, bitrate int) bool {
|
||||
found := false
|
||||
for i, u := range config.UIDs {
|
||||
if u.UID == uid {
|
||||
config.UIDs[i].Bitrate = bitrate
|
||||
config.UIDs[i].QualityPref = pref
|
||||
config.UIDs[i].Bitrate = custom
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
if s, ok := streamers.crt[uid]; ok {
|
||||
s.Bitrate = bitrate
|
||||
s.QualityPref = pref
|
||||
s.Bitrate = custom
|
||||
streamers.crt[uid] = s
|
||||
}
|
||||
}
|
||||
@@ -749,15 +829,23 @@ func setBitrate(uid, bitrate int) bool {
|
||||
|
||||
saveConfig()
|
||||
|
||||
// 码率只对新分段生效,正在录制时重启录制
|
||||
// 档位只对新分段生效,正在录制时重启录制
|
||||
if wasRecording {
|
||||
stopRecord(uid)
|
||||
startRecord(uid)
|
||||
lPrintf("已将 uid %d 的录播码率上限改为 %d Kbps 并重启录制", uid, bitrate)
|
||||
lPrintf("已将 uid %d 的录制档位改为 %s并重启录制", uid, qualityLogText(pref, custom))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 档位的中文描述,用于日志
|
||||
func qualityLogText(pref string, custom int) string {
|
||||
if normalizeQualityPref(pref) == qualityPrefCustom {
|
||||
return fmt.Sprintf("自定义(不超过 %d Kbps)", custom)
|
||||
}
|
||||
return qualityPrefText(pref)
|
||||
}
|
||||
|
||||
// 停止或重新开始指定主播的录制,target 为 danmu、record 或 all(空字符串表示全部),action 为 stop 或 restart
|
||||
func controlStreamer(uid int, target, action string) error {
|
||||
if uid <= 0 {
|
||||
|
||||
Reference in New Issue
Block a user