合并默认改用 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:
@@ -119,6 +119,52 @@ func resolveMergeInput(value string) (string, error) {
|
||||
return "", fmt.Errorf("找不到文件:%s", value)
|
||||
}
|
||||
|
||||
// 找同名弹幕文件时按这个顺序试:本程序录的 .jsonl 带头像链接,优先用
|
||||
var mergeDanmuCandidates = []string{".jsonl", ".ass", ".nick.ass"}
|
||||
|
||||
// 按视频文件找同目录下的同名弹幕文件,找不到返回空字符串。
|
||||
// 同目录找不到时再拿文件名去录播目录里试一次(视频被挪走、弹幕还留在录播目录的情况)
|
||||
func findDanmuForVideo(video string) string {
|
||||
full, err := resolveMergeInput(video)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
base := strings.TrimSuffix(full, filepath.Ext(full))
|
||||
dirs := []string{filepath.Dir(full)}
|
||||
if recDir := recordSaveDir(); normalizePath(recDir) != normalizePath(filepath.Dir(full)) {
|
||||
dirs = append(dirs, recDir)
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
for _, ext := range mergeDanmuCandidates {
|
||||
cand := filepath.Join(dir, filepath.Base(base)+ext)
|
||||
if info, err := os.Stat(cand); err == nil && !info.IsDir() {
|
||||
return cand
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 填回输入框时的写法:录播目录/成片目录里的只写文件名,别处的写绝对路径(和浏览弹窗一致)
|
||||
func mergeFillValue(path string) string {
|
||||
dir := normalizePath(filepath.Dir(path))
|
||||
if dir == normalizePath(recordSaveDir()) || dir == normalizePath(mergeSaveDir()) {
|
||||
return filepath.Base(path)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// GET /api/merge/danmu?video=xxx —— 页面选好视频后自动找同名弹幕文件用
|
||||
func mergeDanmuMatchHandler(w http.ResponseWriter, r *http.Request) {
|
||||
path := findDanmuForVideo(r.URL.Query().Get("video"))
|
||||
resp := map[string]string{"danmu": ""}
|
||||
if path != "" {
|
||||
resp["danmu"] = mergeFillValue(path)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
// 扩展名白名单的说明文字
|
||||
func extListText(m map[string]struct{}) string {
|
||||
out := make([]string, 0, len(m))
|
||||
|
||||
Reference in New Issue
Block a user