From 40f4f36aa6349329aaf53e5b5d8c1188795fcdef Mon Sep 17 00:00:00 2001 From: tamakyi Date: Mon, 28 Sep 2026 02:04:10 +0800 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=90=88=E5=B9=B6=E6=8C=89?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=AE=9E=E9=99=85=E4=BD=8D=E7=BD=AE=E6=89=BE?= =?UTF-8?q?=E5=BC=B9=E5=B9=95=EF=BC=8C=E4=BF=AE=E6=8E=89=E3=80=8Cjsonl=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8D=E5=AD=98=E5=9C=A8=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为什么: 录制结束时 moveFile 已经把 .ass / .nick.ass / .jsonl 搬到保存目录,但合并时只有视频 分段走了 findMovedFile 兜底,弹幕 jsonl 一直拿着录制时算出来的旧路径。于是文件明明 安安稳稳躺在保存目录里,burnDanmu 却去旧位置找,自动合并以「jsonl 文件不存在」告终 并推送一条失败通知。 连带还有两处也指向错误位置:tempMergeOutput 用 filepath.Dir(jsonlFile) 决定中间成片 写到哪个目录,mergeDelDanmu 按它去删那三个弹幕文件。 处理方式是新增 resolveAfterMove(path)——找不着就退回原路径,这样报错还能说清本来想 找哪儿——然后在两处归一:prepareForMerge 让返回的 jsonl 是解析后的路径;autoMergeJob 里也要归一,因为中间成片写到哪、事后删哪几个文件都看这条路径。 上一个提交(录制直接写保存目录)落地后这条会退化成空操作,文件压根不再挪窝;留着它 是为了兜住「运行中改了保存目录」「老录播补合并」这类场景。 验证: - 新增 automerge_test.go 两条:prepareForMerge 返回的视频与 jsonl 都从录制位置解析到 保存位置;目标目录里没有同名文件时退回原路径而不是返回空串。 - go test ./... 23.6s 与 go test -race ./... 26.4s 均全绿,215 个顶层用例。 - 反向验证新增一项:删掉 prepareForMerge 里那句归一,对应用例确实变红。 - 验证边界说明:测到的是路径解析这一层,没有构造一整场录制跑真 ffmpeg 走完合并流程 ——那需要一整套直播素材,成本远高于收益。 --- automerge.go | 16 ++++++++++ automerge_test.go | 76 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 automerge_test.go diff --git a/automerge.go b/automerge.go index bb4e46b..c625d04 100644 --- a/automerge.go +++ b/automerge.go @@ -85,6 +85,14 @@ func findMovedFile(path string) string { return "" } +// 收拢到实际位置:找不着时退回原路径,让后面的报错还能指出「本来想找哪儿」 +func resolveAfterMove(path string) string { + if p := findMovedFile(path); p != "" { + return p + } + return path +} + // 录播因为断流可能分成多段:单段直接使用,多段先拼成完整视频并把弹幕时间映射到拼接后的时间轴。 // 返回合并要用的视频、字幕文件和弹幕相对视频起点的时间偏移、**实际参与合并的源文件**, // 以及清理临时文件的函数。used 是给「合并完删源视频」用的:中途回退成「只合并第一个分段」时, @@ -92,6 +100,10 @@ func findMovedFile(path string) string { func prepareForMerge(ffmpegFile string, segs []recordSegment, jsonlFile string, danmuStartNs int64) (string, string, float64, []string, func(), error) { noop := func() {} + // 弹幕文件同样会被移动过去。以前只有视频分段走了查找,弹幕还拿着录制时的路径, + // 结果就是视频找得到、弹幕找不到,合并以「jsonl 文件不存在」告终 + jsonlFile = resolveAfterMove(jsonlFile) + // 录制结束时文件可能已经被移动到保存目录,逐个找实际位置 files := make([]string, 0, len(segs)) starts := make([]int64, 0, len(segs)) @@ -288,6 +300,10 @@ func autoMergeOutFile(jsonlFile string) string { func autoMergeJob(uid int, segs []recordSegment, jsonlFile string, danmuStartNs int64) error { lPrintln("开始自动合并" + longID(uid) + "的录播和弹幕层") + // 录制结束时弹幕文件可能已被移动到保存目录,先把路径收拢到实际位置: + // 中间成片写到哪、事后删哪几个弹幕文件都按这一条路径来,用旧路径会指向错误的地方 + jsonlFile = resolveAfterMove(jsonlFile) + // 视频和弹幕是一次录制产生的,文件名去掉扩展名后应该一致(扩展名由 recordFormat 决定) if len(segs) > 0 { videoBase := filepath.Base(strings.TrimSuffix(segs[0].file, filepath.Ext(segs[0].file))) diff --git a/automerge_test.go b/automerge_test.go new file mode 100644 index 0000000..7457b9c --- /dev/null +++ b/automerge_test.go @@ -0,0 +1,76 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +// 录制结束时文件会被移动到保存目录,视频分段和弹幕文件都要按新位置找。 +// 以前只有视频分段走了查找,弹幕还拿着录制时的路径:视频找到了、弹幕找不到, +// 整场合并以「jsonl 文件不存在」告终(用户看到的就是合成失败 + 一条失败推送)。 +func TestPrepareForMergeFindsMovedJsonl(t *testing.T) { + savedConfig := config + defer func() { config = savedConfig }() + + root := t.TempDir() + config.Directory = root + config.RecordSubdir = "record" + if err := os.MkdirAll(recordSaveDir(), 0755); err != nil { + t.Fatal(err) + } + + name := "2026-09-27 22-40-42 甲 标题" + // 录制时的位置:文件已经被搬走,这里不该再有 + oldDir := t.TempDir() + oldVideo := filepath.Join(oldDir, name+".mp4") + oldJsonl := filepath.Join(oldDir, name+".jsonl") + // 实际位置 + newVideo := filepath.Join(recordSaveDir(), name+".mp4") + newJsonl := filepath.Join(recordSaveDir(), name+".jsonl") + for _, p := range []string{newVideo, newJsonl} { + if err := os.WriteFile(p, []byte("{}\n"), 0644); err != nil { + t.Fatal(err) + } + } + + segs := []recordSegment{{file: oldVideo, startNs: 0}} + video, jsonl, _, _, cleanup, err := prepareForMerge("没有这个-ffmpeg.exe", segs, oldJsonl, 0) + defer cleanup() + if err != nil { + t.Fatalf("单段不该报错:%v", err) + } + if video != newVideo { + t.Errorf("视频应当按保存目录里的实际位置找:\n got %s\n want %s", video, newVideo) + } + if jsonl != newJsonl { + t.Errorf("弹幕文件应当按保存目录里的实际位置找:\n got %s\n want %s", jsonl, newJsonl) + } +} + +// 文件确实不在(既没在录制时的位置、也不在保存目录)时,要退回原路径而不是空串: +// 否则报错会变成「文件不存在:」,看不出到底缺的是哪个 +func TestResolveAfterMoveKeepsOriginalWhenMissing(t *testing.T) { + savedConfig := config + defer func() { config = savedConfig }() + + root := t.TempDir() + config.Directory = root + config.RecordSubdir = "record" + + gone := filepath.Join(t.TempDir(), "这个目录是对的但文件不在.mp4") + if got := resolveAfterMove(gone); got != gone { + t.Errorf("找不到时应当退回原路径:got %q want %q", got, gone) + } + + real := filepath.Join(root, "record", "在保存目录里.mp4") + if err := os.MkdirAll(filepath.Dir(real), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(real, []byte("x"), 0644); err != nil { + t.Fatal(err) + } + if got := resolveAfterMove(real); got != real { + t.Errorf("文件就在原路径时不该改:got %q want %q", got, real) + } +}