修第二批 P1:合并收尾、分块上限、确定性失败与 ffmpeg 路径归一
一、合并收尾只删真正进了成片的分段 prepareForMerge 中途有 4 处会回退成「只合并第一个分段」,而收尾仍按原分段列表 全删源视频 —— 后半场没进成片却照样被删掉,等于永久丢数据。现在 prepareForMerge 把「实际参与合并的源文件」一并返回(新增 used),收尾只删这些。 二、拼接中断不再在录播目录留几 GB 中间文件 concat 失败分支原来只有「被停止」那条路径删了 acfunlive_chunk_joined.*, 普通失败会把它留在录播目录,而且带着 chunkTempPrefix,界面文件列表里根本看不见。 三、把「被停止」和「拼接失败」分开 停止任务会杀掉 ffmpeg 让它非 0 退出,原来一律报成「拼接失败」, 卡片上显示的是失败而不是已停止;prepareForMerge 那里也统一改用 errors.Is 判断。 四、单块弹幕条数加硬上限(maxDanmuPerChunkHard = 200) 块尾会把「显示时间跨过块尾」的弹幕一并并入,弹幕密集时单块能膨胀到几百条。 一条弹幕在 -filter_complex 里约占 110 字节,几百条足以顶破 Windows 的 32767 字符命令行上限,表现同样是「合并失败且看不出原因」。到上限就把块尾 收到下一条弹幕出现之前,保证被挤到下一块的弹幕不会因为「整条早于块头」被丢弃。 五、确定性失败不再白重试三次 头像超出体积/尺寸上限这类失败重试多少次结果都一样,原来会白白把十几 MB 重新下三遍。 新增 noRetryError(runThrice 用 errors.As 识别)只标记这两个上限,网络类错误照旧重试。 六、ffmpeg 路径统一归一成绝对路径 PATH 里带「.」这类相对条目、或程序目录探测失败时,getFFmpeg 会返回相对路径。 合并时 ffmpeg 工作目录被改成输出目录,相对路径到那边就找不着了;而且 Windows 上 exec 会直接拒绝执行「相对当前目录」找到的可执行文件,报错还跟路径完全看不出关系。 验证: - gofmt -l 空;go vet ./... 空;go build ./... 与 GOOS=linux go build 通过 - go test ./... 全绿(10.8s);go test -race ./... 全绿(14.7s) - 新增 chunks_test.go 10 个用例;6 项修复逐条反向验证(临时退回修复 → 对应测试确实失败) - 51897 隔离实例上新产物实测:e2e.py 24 项全过;smoke_extra.py 34 项全过 (含并发合并返回 409 并报出在跑的任务名、口令失败后正确口令仍立刻可用、 超大头像被拒不误伤正常头像且只尝试 1 次、停止后无临时文件残留)
This commit is contained in:
+22
-4
@@ -61,6 +61,13 @@ const danmuShowSeconds = 10.0
|
||||
// 弹幕一多整条命令就会超限且报错完全看不出原因,所以超过这个数量就分块渲染再拼接。
|
||||
const maxDanmuPerChunk = 80
|
||||
|
||||
// 单块条数的硬上限。块尾会把「显示时间跨过块尾」的弹幕一并并入,
|
||||
// 弹幕密集时(10 秒停留窗口里塞进几百条)单块就会膨胀到几百条 ——
|
||||
// 一条弹幕在 -filter_complex 里约占 110 字节,几百条足以顶破上面那条命令行上限,
|
||||
// 表现同样是「合并失败且看不出原因」,所以并入也要有个头。
|
||||
// 到上限就把块尾收到下一条弹幕出现之前,让剩下的弹幕落到下一块。
|
||||
const maxDanmuPerChunkHard = 200
|
||||
|
||||
// 分块渲染产生的临时文件前缀
|
||||
const chunkTempPrefix = "acfunlive_chunk_"
|
||||
|
||||
@@ -645,11 +652,18 @@ func splitDanmuChunks(records []danmuRecord, offset, maxEnd float64) []danmuChun
|
||||
if end <= start {
|
||||
end = start + danmuShowSeconds
|
||||
}
|
||||
// 起点早于块尾的弹幕(显示会跨过块尾)也并入这一块
|
||||
// 起点早于块尾的弹幕(显示会跨过块尾)也并入这一块,但不许超过硬上限
|
||||
j := limit
|
||||
for j < n && records[j].Time+offset < end {
|
||||
for j < n && j < i+maxDanmuPerChunkHard && records[j].Time+offset < end {
|
||||
j++
|
||||
}
|
||||
// 被硬上限挡住时,把块尾收紧到下一条弹幕出现之前:它因此落在下一块里,
|
||||
// 而且不会因为「整条都早于下一块块头」在下一块里被当成无效区间丢掉
|
||||
if j < n && records[j].Time+offset < end {
|
||||
if t := records[j].Time + offset; t > start {
|
||||
end = t
|
||||
}
|
||||
}
|
||||
chunks = append(chunks, danmuChunk{start: start, end: end, records: records[i:j]})
|
||||
i = j
|
||||
start = end
|
||||
@@ -1024,6 +1038,10 @@ func concatChunks(ffmpegFile string, files []string, out, dir string, cb ffmpegP
|
||||
defer os.Remove(listFile)
|
||||
|
||||
if err := runFFmpegWith(ffmpegFile, []string{"-y", "-f", "concat", "-safe", "0", "-i", listFile, "-c", "copy", out}, dir, cb); err != nil {
|
||||
// 停止任务时杀掉 ffmpeg 也会让它非 0 退出,这是「已停止」不是「拼接失败」
|
||||
if jobStopRequested() {
|
||||
return errJobStopped
|
||||
}
|
||||
return fmt.Errorf("拼接各分块失败:%v", err)
|
||||
}
|
||||
return nil
|
||||
@@ -1131,7 +1149,7 @@ func downloadAvatar(uid int64, url, path string) error {
|
||||
return err
|
||||
}
|
||||
if len(data) > avatarMaxBytes {
|
||||
return fmt.Errorf("头像体积超过 %d MB,不下载", avatarMaxBytes>>20)
|
||||
return noRetry(fmt.Errorf("头像体积超过 %d MB,不下载", avatarMaxBytes>>20))
|
||||
}
|
||||
src, err := decodeAvatar(data, resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
@@ -1157,7 +1175,7 @@ func decodeAvatar(data []byte, contentType string) (image.Image, error) {
|
||||
// 防压缩炸弹:几十 KB 的 PNG/GIF 能解压出上亿像素,几 MB 的文件就能吃掉几 GB 内存。
|
||||
// 头像最后只会被缩到几十像素(跟随字号),根本用不到这么大的原图
|
||||
if b := src.Bounds(); b.Dx()*b.Dy() > avatarMaxPixels {
|
||||
return nil, fmt.Errorf("头像尺寸 %dx%d 超出上限(最多 %d 像素)", b.Dx(), b.Dy(), avatarMaxPixels)
|
||||
return nil, noRetry(fmt.Errorf("头像尺寸 %dx%d 超出上限(最多 %d 像素)", b.Dx(), b.Dy(), avatarMaxPixels))
|
||||
}
|
||||
return src, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user