合并前重排弹道并支持自定义弹幕字号
用户反馈两件事:弹幕想再小些;某场合并的 1:19:25 处弹幕糊在一起(把成片抽帧 出来一看,是同一弹道上两条弹幕追尾叠字)。 重叠的根因:弹幕从上游原版的 .ass 还原成 jsonl 时,lane 是按「只有内容」的 宽度反推的(原格式的 x/y 就是这么摆的),而渲染用的是「昵称(uid):内容 + 头像」 的完整宽度 —— 后者占位时间长得多,照搬原弹道必然追尾。用用户那份 3011 条的真实 jsonl 验过:按渲染公式统计,重排前 960 条(31.9%)追尾,重排后 0 条。 - 新增 relayoutLanes:渲染前按最终字号统一重排弹道并重算宽度,公式和录制时的 placeLane 完全一致,所以录制下来的 jsonl 重排后弹道不变(已加用例覆盖)。 - 空弹道用 math.MinInt64 占位:placeLane 的条件是「本条出现时间晚于上一条的让位 时刻」,零值下 0 > 0 不成立,第一条弹幕(还原出来的时间可能正好是 0)会占不上 弹道、占用状态丢失,后面又跟着乱排。录制的弹幕时间总大于 0,所以原逻辑没暴露。 - 弹道全满(100 条都占着)时退到最早让出来的那条:录制时是直接不写这条,合并时 丢一条等于凭空少一句弹幕,压一点也比丢了强。 - 新增 danmuFontSize 设置(合并分区,0 = 跟随弹幕文件):覆盖渲染字号,头像按 同一比例缩放;改了字号会在重排时一并生效,不会因为宽度变了又追尾。 验证:layout_test.go 5 个用例;用新 ffmpeg(用户已自行升级到 n9.0.2)在隔离实例上 跑了真合并 —— 字号 95 以上的对照(60px → 42px:抽帧量到的弹幕墨迹高度 48px → 36px)、 按 1:19:00 裁出的 130 秒片段合并前后追尾 25 条 → 0 条;重跑 verify_settings.py (新增第 8 节覆盖字号字段)、verify_pick.py、verify_streamer_br.py 无回归; go test ./...、gofmt、go vet 干净。
This commit is contained in:
@@ -81,6 +81,7 @@ GOOS=windows GOARCH=amd64 go build -o acfunlive.exe
|
|||||||
"mergeDelDanmu": false, // 合并完成后删除弹幕文件(ass、nick.ass、jsonl)
|
"mergeDelDanmu": false, // 合并完成后删除弹幕文件(ass、nick.ass、jsonl)
|
||||||
"mergeOutputDir": "", // 合并完成后成片移动到该文件夹,留空则不移动
|
"mergeOutputDir": "", // 合并完成后成片移动到该文件夹,留空则不移动
|
||||||
"mergeBitrate": 0, // 合并成片的目标码率(Kbps),0 表示使用默认质量
|
"mergeBitrate": 0, // 合并成片的目标码率(Kbps),0 表示使用默认质量
|
||||||
|
"danmuFontSize": 0, // 合并时弹幕文字的字号(像素),0 表示跟随弹幕文件;头像随字号等比缩放
|
||||||
"defaultBitrate": 0, // 新添加主播的默认 bitrate,0 表示码率最高的直播源
|
"defaultBitrate": 0, // 新添加主播的默认 bitrate,0 表示码率最高的直播源
|
||||||
|
|
||||||
"webExternal": false, // 是否允许外网访问 web UI,false 时只监听本机 127.0.0.1
|
"webExternal": false, // 是否允许外网访问 web UI,false 时只监听本机 127.0.0.1
|
||||||
@@ -213,6 +214,8 @@ acfunlive -danmuburn xxx.jsonl -video xxx.flv -ffmpeg D:\tools\ffmpeg.exe
|
|||||||
|
|
||||||
手动停止录制(web UI 的`停止`按钮或命令行里停止)不会触发自动合并,重新开始录制后新的一组文件仍会在直播结束时自动合并。这一场想补出成片的话,用下面的`手动合并`。合并需要 ffmpeg,且会占用 CPU 或显卡资源,长录播的合并耗时较长。
|
手动停止录制(web UI 的`停止`按钮或命令行里停止)不会触发自动合并,重新开始录制后新的一组文件仍会在直播结束时自动合并。这一场想补出成片的话,用下面的`手动合并`。合并需要 ffmpeg,且会占用 CPU 或显卡资源,长录播的合并耗时较长。
|
||||||
|
|
||||||
|
弹幕字号默认沿用弹幕文件里记的字号(1080p 下通常是 48~60),嫌大可以调`弹幕字号`,头像会跟着等比缩小。合并前会按最终字号把弹幕重新排一遍弹道:从上游原版的`.ass`还原出来的弹幕,原本是按「只有内容」的宽度占位的,而渲染用的是「昵称(uid):内容 + 头像」的宽度,占位更久,照搬原弹道会追尾叠字——重排按完整宽度重新分配,同一条弹道上不会再有两条弹幕叠在一起。
|
||||||
|
|
||||||
停止录播时会向 ffmpeg 发送正常退出的指令,让它把文件正常收尾(直接杀进程会在文件尾部留下损坏的数据包,mp4 甚至会因为缺少 moov atom 而整个读不出来)。
|
停止录播时会向 ffmpeg 发送正常退出的指令,让它把文件正常收尾(直接杀进程会在文件尾部留下损坏的数据包,mp4 甚至会因为缺少 moov atom 而整个读不出来)。
|
||||||
|
|
||||||
`autoMerge`、`mergeDelVideo`、`mergeDelDanmu`三项互不影响,可以同时开启:第一项决定是否自动合并,后两项是合并成功后的清理动作。
|
`autoMerge`、`mergeDelVideo`、`mergeDelDanmu`三项互不影响,可以同时开启:第一项决定是否自动合并,后两项是合并成功后的清理动作。
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ type configData struct {
|
|||||||
MergeDelDanmu bool `json:"mergeDelDanmu"` // 合并完成后删除弹幕文件
|
MergeDelDanmu bool `json:"mergeDelDanmu"` // 合并完成后删除弹幕文件
|
||||||
MergeOutputDir string `json:"mergeOutputDir"` // 合并完成后成片移动到该文件夹,留空不移动
|
MergeOutputDir string `json:"mergeOutputDir"` // 合并完成后成片移动到该文件夹,留空不移动
|
||||||
MergeBitrate int `json:"mergeBitrate"` // 合并成片的目标码率(Kbps),0 表示使用默认质量
|
MergeBitrate int `json:"mergeBitrate"` // 合并成片的目标码率(Kbps),0 表示使用默认质量
|
||||||
|
DanmuFontSize int `json:"danmuFontSize"` // 合并时弹幕文字的字号(头像随字号等比缩放),0 表示跟随弹幕文件
|
||||||
|
|
||||||
DefaultBitrate int `json:"defaultBitrate"` // 新添加主播的默认下载码率上限(Kbps),0 表示录制码率最高的直播源
|
DefaultBitrate int `json:"defaultBitrate"` // 新添加主播的默认下载码率上限(Kbps),0 表示录制码率最高的直播源
|
||||||
|
|
||||||
@@ -360,6 +361,10 @@ func checkConfig() {
|
|||||||
lPrintErrf("%s里的mergeBitrate不能小于 0", configFile)
|
lPrintErrf("%s里的mergeBitrate不能小于 0", configFile)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
if config.DanmuFontSize < 0 {
|
||||||
|
lPrintErrf("%s里的danmuFontSize不能小于 0(0 表示跟随弹幕文件)", configFile)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
if config.DefaultBitrate < 0 {
|
if config.DefaultBitrate < 0 {
|
||||||
lPrintErrf("%s里的defaultBitrate不能小于 0", configFile)
|
lPrintErrf("%s里的defaultBitrate不能小于 0", configFile)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -202,6 +203,14 @@ func burnDanmu(jsonlFile, videoFile, outFile string, offset float64) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 弹幕字号可被设置覆盖(0 = 跟随弹幕文件),头像尺寸随字号等比缩放
|
||||||
|
if config.DanmuFontSize > 0 {
|
||||||
|
lPrintf("弹幕字号按设置使用 %d(弹幕文件里是 %d)", config.DanmuFontSize, meta.FontSize)
|
||||||
|
meta.FontSize = config.DanmuFontSize
|
||||||
|
}
|
||||||
|
// 弹道按最终字号统一重排,避免还原弹幕或改字号后同弹道追尾
|
||||||
|
relayoutLanes(records, meta)
|
||||||
|
|
||||||
// 下载并圆形裁切头像
|
// 下载并圆形裁切头像
|
||||||
avatarPaths := make(map[int64]string)
|
avatarPaths := make(map[int64]string)
|
||||||
dir := filepath.Dir(jsonlFile)
|
dir := filepath.Dir(jsonlFile)
|
||||||
@@ -269,6 +278,57 @@ func burnDanmu(jsonlFile, videoFile, outFile string, offset float64) error {
|
|||||||
return ctx.burnChunked(chunks)
|
return ctx.burnChunked(chunks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 渲染前按最终字号重排弹道:jsonl 里的 lane/width 是录制或 .ass 还原时算的,
|
||||||
|
// 从原格式 .ass 还原的弹幕没把昵称和头像算进宽度(占位提前放行了同弹道的下一条,
|
||||||
|
// 画面上就追尾叠字),改过字号后更是全对不上。这里用和录制时相同的碰撞公式、
|
||||||
|
// 按「昵称(uid):内容 + 头像」的完整宽度重新分配一遍,保证弹道不重叠。
|
||||||
|
func relayoutLanes(records []danmuRecord, meta danmuMeta) {
|
||||||
|
sort.SliceStable(records, func(i, j int) bool { return records[i].Time < records[j].Time })
|
||||||
|
fs := meta.FontSize
|
||||||
|
show := int64(danmuShowSeconds * 1e9)
|
||||||
|
resX := int64(meta.PlayResX)
|
||||||
|
avW, avGap := avatarSize(fs), avatarGap(fs)
|
||||||
|
// 空弹道用「远古」状态占位:placeLane 的条件是本条出现时间晚于上一条的让位时刻,
|
||||||
|
// 零值下 0 > 0 不成立,第一条弹幕(还原出来的弹幕时间可能正好是 0)会占不上弹道,
|
||||||
|
// 占用状态丢掉后后面的弹幕又会跟着落回空弹道,等于白排。录制时弹幕时间总大于 0,
|
||||||
|
// 所以原逻辑没暴露这个问题。
|
||||||
|
lanes := make([]laneState, laneNum)
|
||||||
|
for i := range lanes {
|
||||||
|
lanes[i] = laneState{appear: math.MinInt64, emerge: math.MinInt64, disappear: math.MinInt64}
|
||||||
|
}
|
||||||
|
for i := range records {
|
||||||
|
r := &records[i]
|
||||||
|
r.Width = textWidth(speakerText(r.Name, r.UID, r.Content), fs)
|
||||||
|
total := int64(avW+avGap) + int64(r.Width)
|
||||||
|
rel := int64(r.Time * 1e9)
|
||||||
|
dt := laneState{
|
||||||
|
appear: rel,
|
||||||
|
emerge: rel + total*show/(resX+total),
|
||||||
|
disappear: rel + show,
|
||||||
|
}
|
||||||
|
left := rel + resX*show/(resX+total)
|
||||||
|
j := placeLane(lanes, dt, left)
|
||||||
|
if j < 0 {
|
||||||
|
// 100 条弹道都还占着:退到最早让出来的那条。录制时是直接不写这条,
|
||||||
|
// 合并时丢一条等于凭空少一句弹幕,压一点也比丢了强。
|
||||||
|
j = earliestLane(lanes)
|
||||||
|
lanes[j] = dt
|
||||||
|
}
|
||||||
|
r.Lane = j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最早会空出来的弹道行
|
||||||
|
func earliestLane(lanes []laneState) int {
|
||||||
|
best := 0
|
||||||
|
for i := range lanes {
|
||||||
|
if lanes[i].disappear < lanes[best].disappear {
|
||||||
|
best = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
// 用 ffmpeg 探测媒体文件的时长(秒),探测不到返回 0
|
// 用 ffmpeg 探测媒体文件的时长(秒),探测不到返回 0
|
||||||
func probeDuration(ffmpegFile, path string) float64 {
|
func probeDuration(ffmpegFile, path string) float64 {
|
||||||
cmd := exec.Command(ffmpegFile, "-hide_banner", "-i", path)
|
cmd := exec.Command(ffmpegFile, "-hide_banner", "-i", path)
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ web UI(`-webui`,端口看`webPort`)用的是另一套`/api/`接口,全
|
|||||||
|
|
||||||
合并状态通过`GET /api/status`查看:`mergeBusy`(是否在跑)、`mergeOutName`(当前成片名)、`mergeErr`(失败原因)、`mergeDoneAt`(上次完成时间)。合并期间成片会出现在`GET /api/files`里且`inUse`为 true,此时`POST /api/file/remove`会返回 409 拒绝删除。
|
合并状态通过`GET /api/status`查看:`mergeBusy`(是否在跑)、`mergeOutName`(当前成片名)、`mergeErr`(失败原因)、`mergeDoneAt`(上次完成时间)。合并期间成片会出现在`GET /api/files`里且`inUse`为 true,此时`POST /api/file/remove`会返回 409 拒绝删除。
|
||||||
|
|
||||||
|
`POST /api/config`里的`danmuFontSize`控制合并时弹幕文字的字号(像素),头像按同一比例缩放,填 0 表示沿用弹幕文件里记的字号。渲染前会按最终字号重排弹道:从上游原版`.ass`还原出来的弹幕只按内容宽度占位,而渲染用的是「昵称(uid):内容 + 头像」的完整宽度,占位时间更长,照搬原弹道会追尾叠字;重排按完整宽度重新分配,同一条弹道上不会再有两条弹幕叠在一起。
|
||||||
|
|
||||||
`POST /api/notify/test` 测试某个推送渠道能不能连通。用传进来的值直接测,留空的项自动用已保存的配置补齐,所以不用先保存设置。测试不受渠道开关和事件勾选影响。
|
`POST /api/notify/test` 测试某个推送渠道能不能连通。用传进来的值直接测,留空的项自动用已保存的配置补齐,所以不用先保存设置。测试不受渠道开关和事件勾选影响。
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// 还原自 .ass 的弹幕:原格式的弹道是按「只有内容」的宽度占位的,渲染用的是
|
||||||
|
// 「昵称(uid):内容 + 头像」的宽度,占位更长 → 同弹道会追尾。重排后必须分开。
|
||||||
|
func TestRelayoutSeparatesRestoredOverlap(t *testing.T) {
|
||||||
|
meta := danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 60}
|
||||||
|
content := "哈哈哈哈哈哈哈哈"
|
||||||
|
// 两条原本同弹道、间隔 2.5 秒:按内容宽度(8×60=480)算,2.5 秒时前一条已经让位;
|
||||||
|
// 按昵称头像宽度(头像48+间隔10+文本720)算,前一条要到约 2.9 秒才让位
|
||||||
|
records := []danmuRecord{
|
||||||
|
{Time: 0, UID: 101, Name: "甲", Content: content, Lane: 0, Width: 480},
|
||||||
|
{Time: 2.5, UID: 202, Name: "乙", Content: content, Lane: 0, Width: 480},
|
||||||
|
}
|
||||||
|
|
||||||
|
relayoutLanes(records, meta)
|
||||||
|
|
||||||
|
if records[0].Lane == records[1].Lane {
|
||||||
|
t.Fatalf("两条追尾的弹幕重排后仍在同一条弹道 %d", records[0].Lane)
|
||||||
|
}
|
||||||
|
// 宽度也要按昵称头像版重算,否则滚动速度还是错的
|
||||||
|
want := textWidth(speakerText("甲", 101, content), meta.FontSize)
|
||||||
|
if records[0].Width != want {
|
||||||
|
t.Errorf("宽度应重算为 %d,实际 %d", want, records[0].Width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 录制时存下来的弹道本来就是按同一套公式算的,重排应当原样保留
|
||||||
|
func TestRelayoutKeepsLiveLayout(t *testing.T) {
|
||||||
|
meta := danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 48}
|
||||||
|
var records []danmuRecord
|
||||||
|
for i := 0; i < 40; i++ {
|
||||||
|
records = append(records, danmuRecord{
|
||||||
|
Time: float64(i) * 0.4, UID: int64(100 + i), Name: "观众", Content: "测试弹幕内容",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
relayoutLanes(records, meta)
|
||||||
|
first := make([]int, len(records))
|
||||||
|
for i, r := range records {
|
||||||
|
first[i] = r.Lane
|
||||||
|
}
|
||||||
|
|
||||||
|
relayoutLanes(records, meta)
|
||||||
|
for i, r := range records {
|
||||||
|
if r.Lane != first[i] {
|
||||||
|
t.Fatalf("第 %d 条重排两次结果不一致:%d → %d", i, first[i], r.Lane)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输入顺序乱了也要按时间重排,否则弹道占用顺序错乱
|
||||||
|
func TestRelayoutSortsByTime(t *testing.T) {
|
||||||
|
meta := danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 48}
|
||||||
|
shuffled := []danmuRecord{
|
||||||
|
{Time: 5, UID: 1, Name: "甲", Content: "第三条"},
|
||||||
|
{Time: 1, UID: 2, Name: "乙", Content: "第一条"},
|
||||||
|
{Time: 3, UID: 3, Name: "丙", Content: "第二条"},
|
||||||
|
}
|
||||||
|
sorted := []danmuRecord{
|
||||||
|
{Time: 1, UID: 2, Name: "乙", Content: "第一条"},
|
||||||
|
{Time: 3, UID: 3, Name: "丙", Content: "第二条"},
|
||||||
|
{Time: 5, UID: 1, Name: "甲", Content: "第三条"},
|
||||||
|
}
|
||||||
|
relayoutLanes(shuffled, meta)
|
||||||
|
relayoutLanes(sorted, meta)
|
||||||
|
for i := range sorted {
|
||||||
|
if shuffled[i].Lane != sorted[i].Lane {
|
||||||
|
t.Errorf("第 %d 条弹道不一致:%d vs %d", i, shuffled[i].Lane, sorted[i].Lane)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字号越小,同一条弹幕在屏幕上停留时占用的时间越短,弹道占用也更宽松
|
||||||
|
func TestRelayoutWidthScalesWithFontSize(t *testing.T) {
|
||||||
|
big := []danmuRecord{{Time: 0, UID: 7, Name: "甲", Content: "短弹幕"}}
|
||||||
|
small := []danmuRecord{{Time: 0, UID: 7, Name: "甲", Content: "短弹幕"}}
|
||||||
|
|
||||||
|
relayoutLanes(big, danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 60})
|
||||||
|
relayoutLanes(small, danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 30})
|
||||||
|
|
||||||
|
if big[0].Width <= small[0].Width {
|
||||||
|
t.Errorf("字号 60 的宽度 %d 应大于字号 30 的 %d", big[0].Width, small[0].Width)
|
||||||
|
}
|
||||||
|
if big[0].Width != small[0].Width*2 {
|
||||||
|
t.Errorf("宽度应随字号等比缩放:60 → %d,30 → %d", big[0].Width, small[0].Width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密集弹幕(弹道信息全是坏的:lane 都是 0、width 都是 0)重排后不应再有追尾,
|
||||||
|
// 也不该超过弹道行数上限
|
||||||
|
func TestRelayoutNoOverlapOnDenseInput(t *testing.T) {
|
||||||
|
meta := danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 60}
|
||||||
|
texts := []string{"短", "中等长度的一条弹幕", "这是一条很长的弹幕内容用来占位置", "colong"}
|
||||||
|
var records []danmuRecord
|
||||||
|
t0 := 0.0
|
||||||
|
for i := 0; i < 600; i++ {
|
||||||
|
records = append(records, danmuRecord{
|
||||||
|
Time: t0,
|
||||||
|
UID: int64(1000 + i%7),
|
||||||
|
Name: "观众" + string(rune('A'+i%26)),
|
||||||
|
Content: texts[i%len(texts)],
|
||||||
|
Lane: 0,
|
||||||
|
Width: 0,
|
||||||
|
})
|
||||||
|
t0 += 0.05 + float64(i%5)*0.07 // 0.05 ~ 0.33 秒一条
|
||||||
|
}
|
||||||
|
|
||||||
|
relayoutLanes(records, meta)
|
||||||
|
|
||||||
|
show := int64(danmuShowSeconds * 1e9)
|
||||||
|
resX := int64(meta.PlayResX)
|
||||||
|
type state struct{ emerge int64 }
|
||||||
|
last := map[int]state{}
|
||||||
|
for i, r := range records {
|
||||||
|
if r.Lane < 0 || r.Lane >= laneNum {
|
||||||
|
t.Fatalf("第 %d 条弹道越界:%d", i, r.Lane)
|
||||||
|
}
|
||||||
|
total := int64(avatarSize(meta.FontSize)+avatarGap(meta.FontSize)) + int64(r.Width)
|
||||||
|
if total <= 0 {
|
||||||
|
t.Fatalf("第 %d 条宽度没算出来:%d", i, r.Width)
|
||||||
|
}
|
||||||
|
rel := int64(r.Time * 1e9)
|
||||||
|
emerge := rel + total*show/(resX+total)
|
||||||
|
if prev, ok := last[r.Lane]; ok && rel < prev.emerge {
|
||||||
|
t.Fatalf("第 %d 条和上一条挤在同弹道 %d:出现 %v 早于上一条让位 %v", i, r.Lane, r.Time, float64(prev.emerge)/1e9)
|
||||||
|
}
|
||||||
|
last[r.Lane] = state{emerge}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -308,6 +308,7 @@ const webuiHTML = `<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="f"><label>合成码率(Kbps,0 = 默认质量)</label><input type="text" id="mergeBitrate" inputmode="numeric" placeholder="例如 1500"></div>
|
<div class="f"><label>合成码率(Kbps,0 = 默认质量)</label><input type="text" id="mergeBitrate" inputmode="numeric" placeholder="例如 1500"></div>
|
||||||
|
<div class="f"><label>弹幕字号(像素,0 = 跟随弹幕文件)</label><input type="text" id="danmuFontSize" inputmode="numeric" placeholder="例如 42,头像随字号一起缩放"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hint">文本框改完按回车或点右上角「保存设置」生效,不做这一步的话 3 秒后会被刷回原值。</div>
|
<div class="hint">文本框改完按回车或点右上角「保存设置」生效,不做这一步的话 3 秒后会被刷回原值。</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -477,8 +478,8 @@ var token = localStorage.getItem("acfunlive_token") || ""; // 管理口令,存
|
|||||||
var authed = false; // 是否已通过口令校验,未通过时停止轮询
|
var authed = false; // 是否已通过口令校验,未通过时停止轮询
|
||||||
var tokenLoaded = false;
|
var tokenLoaded = false;
|
||||||
// 设置表单里的字段,元素 id 与 /api/config 的字段名一致
|
// 设置表单里的字段,元素 id 与 /api/config 的字段名一致
|
||||||
var settingFields = ["directory","autoMerge","mergeDelVideo","mergeDelDanmu","mergeOutputDir","mergeBitrate","defaultBitrate","recordSubdir","mergeSubdir","ffmpegPath","recordFormat","notifyEnabled","notifyUrl","notifyToken","notifyGroup","notifyLiveOn","notifyLiveOff","notifyRecordStart","notifyMergeDone","meowEnabled","meowNickname","meowMsgType","meowHtmlHeight","meowImgUrl","webExternal"];
|
var settingFields = ["directory","autoMerge","mergeDelVideo","mergeDelDanmu","mergeOutputDir","mergeBitrate","danmuFontSize","defaultBitrate","recordSubdir","mergeSubdir","ffmpegPath","recordFormat","notifyEnabled","notifyUrl","notifyToken","notifyGroup","notifyLiveOn","notifyLiveOff","notifyRecordStart","notifyMergeDone","meowEnabled","meowNickname","meowMsgType","meowHtmlHeight","meowImgUrl","webExternal"];
|
||||||
var numberFields = {mergeBitrate:1, defaultBitrate:1, meowHtmlHeight:1};
|
var numberFields = {mergeBitrate:1, danmuFontSize:1, defaultBitrate:1, meowHtmlHeight:1};
|
||||||
// 用户改过、还没提交的字段。定时刷新会拿服务端的值填表单,不记下这些字段的话,
|
// 用户改过、还没提交的字段。定时刷新会拿服务端的值填表单,不记下这些字段的话,
|
||||||
// 刚填好的数字 3 秒后会被刷回旧值,看起来就像「改了又自己恢复」
|
// 刚填好的数字 3 秒后会被刷回旧值,看起来就像「改了又自己恢复」
|
||||||
var dirty = {};
|
var dirty = {};
|
||||||
@@ -642,6 +643,7 @@ async function refresh(){
|
|||||||
setVal("directory", st.directory||"");
|
setVal("directory", st.directory||"");
|
||||||
setVal("mergeOutputDir", st.mergeOutputDir||"");
|
setVal("mergeOutputDir", st.mergeOutputDir||"");
|
||||||
setVal("mergeBitrate", st.mergeBitrate||0);
|
setVal("mergeBitrate", st.mergeBitrate||0);
|
||||||
|
setVal("danmuFontSize", st.danmuFontSize||0);
|
||||||
setVal("defaultBitrate", st.defaultBitrate||0);
|
setVal("defaultBitrate", st.defaultBitrate||0);
|
||||||
setVal("recordSubdir", st.recordSubdir||"record");
|
setVal("recordSubdir", st.recordSubdir||"record");
|
||||||
setVal("mergeSubdir", st.mergeSubdir||"merged");
|
setVal("mergeSubdir", st.mergeSubdir||"merged");
|
||||||
@@ -1261,6 +1263,7 @@ type webStatus struct {
|
|||||||
MergeDelDanmu bool `json:"mergeDelDanmu"`
|
MergeDelDanmu bool `json:"mergeDelDanmu"`
|
||||||
MergeOutputDir string `json:"mergeOutputDir"`
|
MergeOutputDir string `json:"mergeOutputDir"`
|
||||||
MergeBitrate int `json:"mergeBitrate"`
|
MergeBitrate int `json:"mergeBitrate"`
|
||||||
|
DanmuFontSize int `json:"danmuFontSize"`
|
||||||
DefaultBitrate int `json:"defaultBitrate"`
|
DefaultBitrate int `json:"defaultBitrate"`
|
||||||
WebExternal bool `json:"webExternal"`
|
WebExternal bool `json:"webExternal"`
|
||||||
RecordSubdir string `json:"recordSubdir"`
|
RecordSubdir string `json:"recordSubdir"`
|
||||||
@@ -1321,6 +1324,7 @@ func statusHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
MergeDelDanmu: config.MergeDelDanmu,
|
MergeDelDanmu: config.MergeDelDanmu,
|
||||||
MergeOutputDir: config.MergeOutputDir,
|
MergeOutputDir: config.MergeOutputDir,
|
||||||
MergeBitrate: config.MergeBitrate,
|
MergeBitrate: config.MergeBitrate,
|
||||||
|
DanmuFontSize: config.DanmuFontSize,
|
||||||
WebExternal: config.WebExternal,
|
WebExternal: config.WebExternal,
|
||||||
RecordSubdir: config.RecordSubdir,
|
RecordSubdir: config.RecordSubdir,
|
||||||
MergeSubdir: config.MergeSubdir,
|
MergeSubdir: config.MergeSubdir,
|
||||||
@@ -1398,6 +1402,7 @@ func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
MergeDelDanmu bool `json:"mergeDelDanmu"`
|
MergeDelDanmu bool `json:"mergeDelDanmu"`
|
||||||
MergeOutputDir string `json:"mergeOutputDir"`
|
MergeOutputDir string `json:"mergeOutputDir"`
|
||||||
MergeBitrate int `json:"mergeBitrate"`
|
MergeBitrate int `json:"mergeBitrate"`
|
||||||
|
DanmuFontSize int `json:"danmuFontSize"`
|
||||||
DefaultBitrate int `json:"defaultBitrate"`
|
DefaultBitrate int `json:"defaultBitrate"`
|
||||||
WebExternal bool `json:"webExternal"`
|
WebExternal bool `json:"webExternal"`
|
||||||
RecordSubdir string `json:"recordSubdir"`
|
RecordSubdir string `json:"recordSubdir"`
|
||||||
@@ -1435,6 +1440,10 @@ func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "mergeBitrate 不能小于 0", http.StatusBadRequest)
|
http.Error(w, "mergeBitrate 不能小于 0", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if req.DanmuFontSize < 0 {
|
||||||
|
http.Error(w, "danmuFontSize 不能小于 0(0 表示跟随弹幕文件)", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
if req.DefaultBitrate < 0 {
|
if req.DefaultBitrate < 0 {
|
||||||
http.Error(w, "defaultBitrate 不能小于 0", http.StatusBadRequest)
|
http.Error(w, "defaultBitrate 不能小于 0", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -1503,6 +1512,7 @@ func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
config.MergeDelDanmu = req.MergeDelDanmu
|
config.MergeDelDanmu = req.MergeDelDanmu
|
||||||
config.MergeOutputDir = req.MergeOutputDir
|
config.MergeOutputDir = req.MergeOutputDir
|
||||||
config.MergeBitrate = req.MergeBitrate
|
config.MergeBitrate = req.MergeBitrate
|
||||||
|
config.DanmuFontSize = req.DanmuFontSize
|
||||||
config.DefaultBitrate = req.DefaultBitrate
|
config.DefaultBitrate = req.DefaultBitrate
|
||||||
restartWeb := config.WebExternal != req.WebExternal
|
restartWeb := config.WebExternal != req.WebExternal
|
||||||
config.WebExternal = req.WebExternal
|
config.WebExternal = req.WebExternal
|
||||||
|
|||||||
Reference in New Issue
Block a user