合并默认改用 HEVC、码率按「源片 + 500」自动算;手动合并自动匹配同名弹幕

两件事:

1) 合成码率与编码器
- `mergeBitrate` 填 0(默认)时不再是 ICQ 质量模式,改成「源片视频码率 + 500 Kbps」:
  烧进去的文字和头像都是高频细节,和源片同码率会糊,所以留一点余量;探测不到源片
  码率时才退回质量模式(QSV 用 ICQ 22、软编用 CRF 18),下限 800 Kbps。
- 编码器默认换成硬件 HEVC:候选顺序 hevc_qsv → h264_qsv → libx264,QSV 可用性按
  编码器分别探测(原来只探 h264_qsv)。HEVC 在 mp4 里打 `hvc1` tag,Apple 系
  (QuickTime / Safari / iPhone)才认 —— ffmpeg 默认写的是 hev1。
- 日志现在会打印编码器优先顺序和实际用的码率。

  实测(90 秒真实素材,源片 3217 kbps,密集弹幕):日志「按源片 3217 kbps + 弹幕
  余量 500 kbps 取 3717 kbps」,成片 codec=hevc、tag=hvc1、实际 3.71 Mbps。
  之前同样是这份素材:h264 + ICQ22 会飙到 7386 kbps(79 MB),改成自动码率后
  43.7 MB,体积小了 45% 画质还更稳。

2) 手动合并自动匹配弹幕
- 新增 `GET /api/merge/danmu?video=…`:按视频名找同目录下的同名弹幕,依次试
  `.jsonl`、`.ass`、`.nick.ass`;同目录没有就拿文件名去录播目录再找一次。
- 页面在「手填/粘贴视频后失焦」「多选弹窗选完」「从文件列表多选后」「点预览或
  开始合并时弹幕框还空着」这几种情况下都会自动匹配并提示;弹幕框里已经有内容时
  绝不动它,找不到也保持为空让用户自己选。
- 填回框里的写法与浏览弹窗一致:录播/成片目录下的只写文件名,别处的写绝对路径。

测试:新增 encode_test.go(编码器顺序、参数与 hvc1、自动码率、ffmpeg 输出解析)
和 TestFindDanmuForVideo(7 个子用例:优先级、录播目录回退、绝对路径、找不到返回空);
仓库外的 verify_danmu_match.py 4 组断言全过,verify_pick.py / verify_settings.py 重跑
无回归;go test ./...、gofmt、go vet 干净。
This commit is contained in:
tamakyi
2026-09-25 19:23:44 +08:00
parent e8b455dfe8
commit a891094f29
10 changed files with 435 additions and 76 deletions
+1 -29
View File
@@ -1,9 +1,6 @@
package main
import (
"strconv"
"testing"
)
import "testing"
// 还原自 .ass 的弹幕:原格式的弹道是按「只有内容」的宽度占位的,渲染用的是
// 「昵称(uid):内容 + 头像」的宽度,占位更长 → 同弹道会追尾。重排后必须分开。
@@ -167,31 +164,6 @@ func TestUnifyDanmuSpace(t *testing.T) {
}
}
// ffmpeg 输出的解析:分辨率要靠「逗号 + 数字x数字」,别被编码器名里的 0x… 骗到
func TestProbeVideoSizeRegex(t *testing.T) {
cases := []struct {
line string
w, h int
}{
{" Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 1920x1080 [SAR 1:1 DAR 16:9], 1500 kb/s, 23.98 fps", 1920, 1080},
{" Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, smpte170m/unknown/unknown, progressive), 720x1280 [SAR 1:1 DAR 9:16], 1570 kb/s, 10 fps", 720, 1280},
{" Stream #0:0: Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv), 3840x2160, 8000 kb/s", 3840, 2160},
{" Stream #0:0(und): Video: h264 (High), yuv420p, 640x480, 30 fps", 640, 480},
}
for _, c := range cases {
m := videoSizeRe.FindStringSubmatch(c.line)
if len(m) != 3 {
t.Errorf("没解析出行:%s", c.line)
continue
}
w, _ := strconv.Atoi(m[1])
h, _ := strconv.Atoi(m[2])
if w != c.w || h != c.h {
t.Errorf("解析成 %dx%d,应为 %dx%d:%s", w, h, c.w, c.h, c.line)
}
}
}
// 按渲染时的公式统计同弹道追尾条数
func countLaneOverlaps(records []danmuRecord, meta danmuMeta) int {
show := int64(danmuShowSeconds * 1e9)