合并先在本地做完再落到成片目录;头像支持 gif 和静态 webp
起因是核查合并落盘和头像处理时发现的两处问题:
1) 成片落盘:ffmpeg 原来是直接写目标目录(automerge.go 里那句注释「不再事后再
移动一次」)。成片目录可能是网络盘或外接盘,写到一半失败会在那里留下一个
看起来正常的半成品。现在改成先本地合成、完成后再落盘:
- 输出先写到弹幕文件所在目录(本地)的 acfunlive_chunk_merging_*
- 写完后由 placeOutput 落盘:同盘直接 os.Rename(瞬时),跨盘退回拷贝;
拷贝中途失败留下的是 *.acfunlive.part,一眼看得出不是成品
- 成功或失败都会清掉本地临时文件,失败时两个目录都不留东西
- 临时文件沿用 chunkTempPrefix,不再出现在文件管理的列表里(listFiles 过滤)
- 启动时清理上次残留;删不掉时提示一句 —— 强杀程序不会连带杀掉 ffmpeg
子进程,它可能还在写这个文件,这种情况下次启动会再清一次
2) 头像:downloadAvatar 只注册了 jpeg 和 png 解码器,AcFun 上的 gif 头像必然
解码失败,静态 webp 同样。加上 _ "image/gif"(多帧取第一帧)和
_ "golang.org/x/image/webp"(x/image 本来就是直接依赖,零新增模块)。
动画 webp 两条路都解不开(x/image 报 invalid format,ffmpeg 5.0 也不行),
保持只烧文字。解码失败时错误信息带上 Content-Type,方便排查是哪张图。
在隔离实例上用真 ffmpeg 跑了真合并验证:gif 头像和 webp 头像都进了缓存并烧进
画面(抽帧确认圆形头像);日志里 ffmpeg 的输出是 record 下的临时文件,完成后
才出现在 merged;坏视频合成失败后成片目录没有半成品、录播目录零残留;合并
中途强杀留下的临时文件在下次启动时被清掉。
新增 output_test.go(7 个)和 avatar_test.go(3 个);go test ./...、gofmt、
go vet 干净。
This commit is contained in:
@@ -7,7 +7,9 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -257,3 +259,81 @@ func isRecording(liveID string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 跨盘搬运成片时的临时后缀:拷一半失败也能一眼看出不是成品
|
||||
const partSuffix = ".acfunlive.part"
|
||||
|
||||
// 合成时的临时成片放在弹幕文件所在目录(本地),
|
||||
// 完成后再落到成片目录 —— 成片目录可能是网络盘或外接盘,直接往里写、
|
||||
// 写到一半失败会在那里留下一个看起来正常的半成品。文件名沿用临时前缀,
|
||||
// 所以不会出现在文件管理的列表里,启动时也会清理残留。
|
||||
func tempMergeOutput(danmuFile, outBase string) string {
|
||||
return filepath.Join(filepath.Dir(danmuFile), chunkTempPrefix+"merging_"+outBase)
|
||||
}
|
||||
|
||||
// 把合成好的本地临时文件落到成片位置:同盘直接改名(瞬时),跨盘退回拷贝
|
||||
func placeOutput(tmp, out string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(out), 0755); err != nil {
|
||||
return fmt.Errorf("创建成片文件夹失败:%v", err)
|
||||
}
|
||||
if err := os.Rename(tmp, out); err == nil {
|
||||
return nil
|
||||
}
|
||||
// 跨盘时 os.Rename 会失败(Windows 上尤其明确),退回拷贝:
|
||||
// 先拷成 .part 再改名,中途失败也不会在成片目录留下像成品的文件
|
||||
part := out + partSuffix
|
||||
if err := copyFile(tmp, part); err != nil {
|
||||
_ = os.Remove(part)
|
||||
return fmt.Errorf("拷贝到成片目录 %s 失败:%v", filepath.Dir(out), err)
|
||||
}
|
||||
if err := os.Rename(part, out); err != nil {
|
||||
_ = os.Remove(part)
|
||||
return fmt.Errorf("成片改名失败:%v", err)
|
||||
}
|
||||
_ = os.Remove(tmp)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 拷贝文件,跨盘搬运成片时用
|
||||
func copyFile(src, dst string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
_ = out.Close()
|
||||
return err
|
||||
}
|
||||
return out.Close()
|
||||
}
|
||||
|
||||
// 清掉上次运行残留的合成临时文件。正常结束时都会删掉,这里是兜底:
|
||||
// 程序在合成中途被强杀时,临时文件会留在录播目录或成片目录里。
|
||||
// 注意强杀不会连带杀掉 ffmpeg 子进程,它可能还在写这个文件(Windows 上就删不掉),
|
||||
// 所以删不掉时只提示一句,下次启动会再清一次。
|
||||
func cleanTempFiles(dir string) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := e.Name()
|
||||
if !strings.HasPrefix(name, chunkTempPrefix) && !strings.HasSuffix(name, partSuffix) {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.Remove(path); err == nil {
|
||||
lPrintln("清理上次留下的临时文件:" + name)
|
||||
} else {
|
||||
lPrintWarnf("临时文件 %s 现在删不掉(多半是上次强杀程序后残留的 ffmpeg 还在写),下次启动会自动清理:%v", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user