两件事: 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 干净。
105 lines
3.7 KiB
Go
105 lines
3.7 KiB
Go
package main
|
||
|
||
import (
|
||
"strconv"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// 编码器候选顺序:硬件 HEVC 最省 → 硬件 H.264 → CPU 软编
|
||
func TestEncoderCandidatesOrder(t *testing.T) {
|
||
cases := []struct {
|
||
hevc, h264 bool
|
||
want []string
|
||
}{
|
||
{true, true, []string{"hevc_qsv", "h264_qsv", "libx264"}},
|
||
{false, true, []string{"h264_qsv", "libx264"}},
|
||
{true, false, []string{"hevc_qsv", "libx264"}},
|
||
{false, false, []string{"libx264"}},
|
||
}
|
||
for _, c := range cases {
|
||
got := encoderCandidates(c.hevc, c.h264)
|
||
if strings.Join(got, ",") != strings.Join(c.want, ",") {
|
||
t.Errorf("hevc=%v h264=%v 应为 %v,实际 %v", c.hevc, c.h264, c.want, got)
|
||
}
|
||
}
|
||
// 候选列表必须能真的跑出结果,最后一个永远是纯 CPU 的 libx264
|
||
last := encoderCandidates(true, true)
|
||
if last[len(last)-1] != "libx264" {
|
||
t.Errorf("最后一个候选必须是 libx264,实际 %s", last[len(last)-1])
|
||
}
|
||
}
|
||
|
||
// 编码器参数:固定码率 / 质量模式,HEVC 必须带 hvc1 tag(Apple 系才认)
|
||
func TestEncoderOpts(t *testing.T) {
|
||
join := func(ss []string) string { return strings.Join(ss, " ") }
|
||
|
||
cases := []struct {
|
||
encoder string
|
||
bitrate int
|
||
want []string
|
||
}{
|
||
{"hevc_qsv", 4000, []string{"-b:v", "4000k", "-tag:v", "hvc1"}},
|
||
{"h264_qsv", 4000, []string{"-b:v", "4000k"}},
|
||
{"libx264", 4000, []string{"-b:v", "4000k", "-preset", "medium"}},
|
||
{"hevc_qsv", 0, []string{"-global_quality", "22", "-preset", "medium", "-tag:v", "hvc1"}},
|
||
{"libx265", 0, []string{"-crf", "18", "-preset", "medium", "-tag:v", "hvc1"}},
|
||
}
|
||
for _, c := range cases {
|
||
got := encoderOpts(c.encoder, c.bitrate)
|
||
if join(got) != join(c.want) {
|
||
t.Errorf("%s @ %d 应为 %v,实际 %v", c.encoder, c.bitrate, c.want, got)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 「合成码率」填 0 时的算法:源片码率 + 弹幕余量,探测不到就交回 0(走质量模式)
|
||
func TestAutoMergeBitrate(t *testing.T) {
|
||
cases := []struct {
|
||
source int
|
||
want int
|
||
}{
|
||
{3197, 3697}, // 用户那场录播的源片码率
|
||
{0, 0}, // 探测不到 → 质量模式
|
||
{-1, 0},
|
||
{100, 800}, // 低于下限时抬到下限
|
||
{3000, 3500}, // 正常情况就是 +500
|
||
}
|
||
for _, c := range cases {
|
||
if got := autoMergeBitrate(c.source); got != c.want {
|
||
t.Errorf("源码率 %d 应得 %d,实际 %d", c.source, c.want, got)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ffmpeg 输出的解析:分辨率靠「逗号 + 数字x数字」,码率靠视频流那一行的 kb/s,
|
||
// 视频流没写码率时退回容器总码率
|
||
func TestProbeStreamRegex(t *testing.T) {
|
||
sizeLine := " Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 1920x1080 [SAR 1:1 DAR 16:9], 3217 kb/s, 23.98 fps"
|
||
m := videoSizeRe.FindStringSubmatch(sizeLine)
|
||
if len(m) != 3 {
|
||
t.Fatalf("没解析出分辨率:%s", sizeLine)
|
||
}
|
||
w, _ := strconv.Atoi(m[1])
|
||
h, _ := strconv.Atoi(m[2])
|
||
if w != 1920 || h != 1080 {
|
||
t.Errorf("分辨率解析成 %dx%d", w, h)
|
||
}
|
||
// 视频流那一行里的码率(不是编码器名里的 0x…)
|
||
if b := videoBitrateRe.FindStringSubmatch(sizeLine); len(b) != 2 || b[1] != "3217" {
|
||
t.Errorf("视频流码率没解析对:%v", b)
|
||
}
|
||
|
||
portrait := " 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"
|
||
m = videoSizeRe.FindStringSubmatch(portrait)
|
||
if len(m) != 3 || m[1] != "720" || m[2] != "1280" {
|
||
t.Errorf("竖屏分辨率没解析对:%v", m)
|
||
}
|
||
|
||
// 视频流那行没写码率时用容器总码率兜底
|
||
durLine := " Duration: 00:02:55.07, start: 0.000000, bitrate: 1767 kb/s"
|
||
if m := containerBitrateRe.FindStringSubmatch(durLine); len(m) != 2 || m[1] != "1767" {
|
||
t.Errorf("容器码率没解析对:%v", m)
|
||
}
|
||
}
|