主播列表的码率改为直播实际档位下拉,没选过时默认第二档
原来是一个手填的码率上限(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:
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
@@ -25,19 +26,27 @@ 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 表示码率最高的直播源
|
||||
UID int `json:"uid"` // 主播 uid
|
||||
Name string `json:"name"` // 主播名字
|
||||
Danmu bool `json:"-"` // 是否录制直播弹幕
|
||||
Record bool `json:"-"` // 是否录制直播视频
|
||||
Bitrate int `json:"-"` // 录制的直播源最高码率(Kbps),0 表示还没定(按实际档位取第二高)
|
||||
Qualities []qualityTier `json:"-"` // 上次探测到的码率档位,没开播时页面靠它显示下拉
|
||||
}
|
||||
|
||||
// 直播源的码率档位
|
||||
type qualityTier struct {
|
||||
Name string `json:"name"` // 档位名,如「蓝光4M」「高清」
|
||||
Bitrate int `json:"bitrate"` // 档位码率(Kbps)
|
||||
}
|
||||
|
||||
// 单个主播的开关设置
|
||||
type uidSetting struct {
|
||||
UID int `json:"uid"`
|
||||
Danmu bool `json:"danmu"`
|
||||
Record bool `json:"record"`
|
||||
Bitrate int `json:"bitrate"` // 录制的直播源最高码率(Kbps),0 表示码率最高的直播源
|
||||
UID int `json:"uid"`
|
||||
Danmu bool `json:"danmu"`
|
||||
Record bool `json:"record"`
|
||||
Bitrate int `json:"bitrate"` // 录制的直播源最高码率(Kbps),0 表示还没定(按实际档位取第二高)
|
||||
Qualities []qualityTier `json:"qualities,omitempty"` // 上次探测到的档位,缓存下来让没开播时也能在页面上选
|
||||
}
|
||||
|
||||
// uidSetting 列表,兼容旧的纯数字数组格式(旧格式默认弹幕和录播都开启)
|
||||
@@ -440,6 +449,7 @@ func buildStreamers() {
|
||||
s.Danmu = u.Danmu
|
||||
s.Record = u.Record
|
||||
s.Bitrate = u.Bitrate
|
||||
s.Qualities = u.Qualities
|
||||
news[u.UID] = s
|
||||
continue
|
||||
}
|
||||
@@ -447,11 +457,106 @@ 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}
|
||||
news[u.UID] = streamer{UID: u.UID, Name: name, Danmu: u.Danmu, Record: u.Record, Bitrate: u.Bitrate, Qualities: u.Qualities}
|
||||
}
|
||||
streamers.crt = news
|
||||
}
|
||||
|
||||
// 档位按码率从高到低排(页面的下拉按这个顺序显示)
|
||||
func sortQualitiesDesc(tiers []qualityTier) []qualityTier {
|
||||
out := append([]qualityTier(nil), tiers...)
|
||||
sort.SliceStable(out, func(i, j int) bool { return out[i].Bitrate > out[j].Bitrate })
|
||||
return out
|
||||
}
|
||||
|
||||
// 没手动选过码率(0)时的默认档位:按码率自上往下取第二档;
|
||||
// 只有一个档位就用它,没有档位返回 0(交给调用方保持原样)
|
||||
func defaultQualityBitrate(tiers []qualityTier) int {
|
||||
desc := sortQualitiesDesc(tiers)
|
||||
switch len(desc) {
|
||||
case 0:
|
||||
return 0
|
||||
case 1:
|
||||
return desc[0].Bitrate
|
||||
default:
|
||||
return desc[1].Bitrate
|
||||
}
|
||||
}
|
||||
|
||||
// 某个码率对应的档位名,匹配不上返回空
|
||||
func qualityNameFor(tiers []qualityTier, bitrate int) string {
|
||||
for _, t := range sortQualitiesDesc(tiers) {
|
||||
if t.Bitrate == bitrate {
|
||||
return t.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 两个档位列表是不是一样(避免每次都写配置)
|
||||
func sameQualities(a, b []qualityTier) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
da, db := sortQualitiesDesc(a), sortQualitiesDesc(b)
|
||||
for i := range da {
|
||||
if da[i] != db[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 记下某个主播当前的码率档位(离线时页面靠它显示下拉)。
|
||||
// 顺手把「还没设过码率」的主播按默认档位(第二高)定下来 —— 这样默认不会一直吃满最高档。
|
||||
// 返回是否改动了配置,调用方决定要不要 saveConfig。
|
||||
func setStreamerQualities(uid int, tiers []qualityTier) bool {
|
||||
if uid <= 0 || len(tiers) == 0 {
|
||||
return false
|
||||
}
|
||||
changed := false
|
||||
streamers.Lock()
|
||||
for i := range config.UIDs {
|
||||
if config.UIDs[i].UID != uid {
|
||||
continue
|
||||
}
|
||||
if !sameQualities(config.UIDs[i].Qualities, tiers) {
|
||||
config.UIDs[i].Qualities = tiers
|
||||
changed = true
|
||||
}
|
||||
if config.UIDs[i].Bitrate == 0 {
|
||||
if def := defaultQualityBitrate(tiers); def > 0 {
|
||||
config.UIDs[i].Bitrate = def
|
||||
changed = true
|
||||
lPrintf("uid %d 还没设过下载码率,按当前直播档位默认取第二档:%s(%d Kbps)",
|
||||
uid, qualityNameFor(tiers, def), def)
|
||||
}
|
||||
}
|
||||
if s, ok := streamers.crt[uid]; ok {
|
||||
s.Qualities = config.UIDs[i].Qualities
|
||||
// 档位定下来后正在录的那场不打断,配置值从下次录制开始生效
|
||||
s.Bitrate = config.UIDs[i].Bitrate
|
||||
streamers.crt[uid] = s
|
||||
}
|
||||
break
|
||||
}
|
||||
streamers.Unlock()
|
||||
return changed
|
||||
}
|
||||
|
||||
// 启动时把「缓存过档位、但还没定过码率」的主播按默认档位定下来
|
||||
func applyCachedDefaultQualities() bool {
|
||||
changed := false
|
||||
for _, u := range append(uidList(nil), config.UIDs...) {
|
||||
if u.Bitrate == 0 && len(u.Qualities) > 0 {
|
||||
if setStreamerQualities(u.UID, u.Qualities) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
// 保存 config.json,写入串行化并原子替换,避免并发写坏文件或读到写了一半的内容
|
||||
func saveConfig() {
|
||||
configMu.Lock()
|
||||
|
||||
Reference in New Issue
Block a user