refactor(*): 平台架构 - 拆分后端领域模块并同步插件协议与模型能力
This commit is contained in:
178
backend/internal/app/video_transcode_test.go
Normal file
178
backend/internal/app/video_transcode_test.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"errors"
|
||||
|
||||
"infinite-canvas/backend/internal/model"
|
||||
)
|
||||
|
||||
// stsdBox 构造仅含 first-sample-entry fourcc 的最小 stsd box(置于 moov 切片内)。
|
||||
func stsdBoxWithFourcc(fourcc string) []byte {
|
||||
b := make([]byte, 24)
|
||||
binary.BigEndian.PutUint32(b[0:4], 24)
|
||||
copy(b[4:8], "stsd")
|
||||
// body: version/flags(4) + entry_count(4)=1 + entry_size(4) + fourcc(4)
|
||||
binary.BigEndian.PutUint32(b[12:16], 1)
|
||||
binary.BigEndian.PutUint32(b[16:20], 8)
|
||||
copy(b[20:24], fourcc)
|
||||
return b
|
||||
}
|
||||
|
||||
func TestCodecFromMoov(t *testing.T) {
|
||||
cases := []struct {
|
||||
fourcc string
|
||||
want string
|
||||
}{
|
||||
{"hvc1", videoCodecH265},
|
||||
{"hev1", videoCodecH265},
|
||||
{"avc1", videoCodecH264},
|
||||
{"av01", videoCodecAV1},
|
||||
{"vp09", videoCodecVP9},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := codecFromMoov(stsdBoxWithFourcc(c.fourcc)); got != c.want {
|
||||
t.Errorf("codecFromMoov(%s) = %q, want %q", c.fourcc, got, c.want)
|
||||
}
|
||||
}
|
||||
if got := codecFromMoov([]byte{0, 1, 2, 3}); got != "" {
|
||||
t.Errorf("garbage moov codec = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeVideoCodecReadsRealFile(t *testing.T) {
|
||||
// 构造 ftyp + moov(含 hvc1 stsd) 的最小 mp4 文件,验证 probeVideoCodec 走文件读取路径。
|
||||
stsd := stsdBoxWithFourcc("hvc1")
|
||||
moov := make([]byte, 8+len(stsd))
|
||||
binary.BigEndian.PutUint32(moov[0:4], uint32(len(moov)))
|
||||
copy(moov[4:8], "moov")
|
||||
copy(moov[8:], stsd)
|
||||
full := make([]byte, 0, 16+len(moov))
|
||||
ftyp := make([]byte, 16)
|
||||
binary.BigEndian.PutUint32(ftyp[0:4], 16)
|
||||
copy(ftyp[4:8], "ftyp")
|
||||
copy(ftyp[8:12], "isom")
|
||||
full = append(full, ftyp...)
|
||||
full = append(full, moov...)
|
||||
|
||||
path := filepath.Join(t.TempDir(), "clip.mp4")
|
||||
if err := os.WriteFile(path, full, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := probeVideoCodec(path); got != videoCodecH265 {
|
||||
t.Errorf("probeVideoCodec = %q, want h265", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackfillRejudgesLegacyNoneVideos 覆盖修复:判定规则变更前(H.265/MPEG-4 Part 2
|
||||
// 曾被误判可播)落 none 的存量本地视频,启动回填必须重新按 codec 判定——
|
||||
// H.264 保持 none(幂等),MPEG-4 触发转码并最终落到 failed/ready 终态。
|
||||
func TestBackfillRejudgesLegacyNoneVideos(t *testing.T) {
|
||||
service, db := newProjectAssetLinkTestService(t)
|
||||
dataDir := t.TempDir()
|
||||
service.dataDir = dataDir
|
||||
|
||||
seedLegacy := func(id, fourcc string) {
|
||||
t.Helper()
|
||||
stsd := stsdBoxWithFourcc(fourcc)
|
||||
moov := make([]byte, 8+len(stsd))
|
||||
binary.BigEndian.PutUint32(moov[0:4], uint32(len(moov)))
|
||||
copy(moov[4:8], "moov")
|
||||
copy(moov[8:], stsd)
|
||||
ftyp := make([]byte, 16)
|
||||
binary.BigEndian.PutUint32(ftyp[0:4], 16)
|
||||
copy(ftyp[4:8], "ftyp")
|
||||
copy(ftyp[8:12], "isom")
|
||||
full := append(append([]byte{}, ftyp...), moov...)
|
||||
rel := filepath.Join("clips", id+".mp4")
|
||||
p := filepath.Join(dataDir, "resources", filepath.FromSlash(rel))
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(p, full, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res := model.Resource{
|
||||
ID: id, UserID: "user-1", Kind: "video", Status: model.ResourceStatusReady,
|
||||
Provider: "local", ObjectKey: rel, PlaybackStatus: model.PlaybackStatusNone,
|
||||
}
|
||||
if err := db.Create(&res).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
seedLegacy("legacy-h264", "avc1")
|
||||
seedLegacy("legacy-mpeg4", "mp4v")
|
||||
|
||||
service.BackfillPlaybackTranscodes()
|
||||
|
||||
var h264 model.Resource
|
||||
if err := db.First(&h264, "id = ?", "legacy-h264").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h264.PlaybackStatus != model.PlaybackStatusNone {
|
||||
t.Fatalf("H.264 存量 none 行被错误改判为 %q", h264.PlaybackStatus)
|
||||
}
|
||||
|
||||
// MPEG-4 行应被抢占转码。fake mp4 不含真实视频流,ffmpeg 解码必败 → failed;
|
||||
// 若某环境恰有同名 ready 副本则也接受。轮询直到终态,避免 goroutine 竞态。
|
||||
if _, err := exec.LookPath("ffmpeg"); err != nil {
|
||||
t.Log("无 ffmpeg,跳过 MPEG-4 终态断言")
|
||||
return
|
||||
}
|
||||
var mp4v model.Resource
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
for {
|
||||
if err := db.First(&mp4v, "id = ?", "legacy-mpeg4").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if mp4v.PlaybackStatus == model.PlaybackStatusFailed || mp4v.PlaybackStatus == model.PlaybackStatusReady {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("MPEG-4 存量行未达终态,停在 %q(error=%q)", mp4v.PlaybackStatus, mp4v.PlaybackError)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
if mp4v.PlaybackStatus != model.PlaybackStatusFailed {
|
||||
t.Fatalf("fake mp4v 应转码失败落 failed,实际 %q", mp4v.PlaybackStatus)
|
||||
}
|
||||
}
|
||||
|
||||
type failingResourceSaver struct {
|
||||
failTimes int
|
||||
calls int
|
||||
}
|
||||
|
||||
func (s *failingResourceSaver) SaveResource(*model.Resource) error {
|
||||
s.calls++
|
||||
if s.calls <= s.failTimes {
|
||||
return errors.New("db busy")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPersistPlaybackResourceRetriesThenSucceeds(t *testing.T) {
|
||||
saver := &failingResourceSaver{failTimes: 2}
|
||||
if err := persistPlaybackResource(saver, &model.Resource{ID: "r1"}, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saver.calls != playbackPersistAttempts {
|
||||
t.Fatalf("calls = %d, want %d", saver.calls, playbackPersistAttempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersistPlaybackResourceReturnsAfterExhaustedRetries(t *testing.T) {
|
||||
saver := &failingResourceSaver{failTimes: 10}
|
||||
if err := persistPlaybackResource(saver, &model.Resource{ID: "r1"}, "test"); err == nil {
|
||||
t.Fatal("expected persist error")
|
||||
}
|
||||
if saver.calls != playbackPersistAttempts {
|
||||
t.Fatalf("calls = %d, want %d", saver.calls, playbackPersistAttempts)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user