Files
91/backend/internal/transcode/transcode_test.go
T
nianzhibai 4dd9015bd7 feat: add per-storage manual transcode for browser-incompatible videos
Add a transcode control to each storage in the admin drives page,
modeled after the cover/preview generation controls:

- Manual start/stop button per storage; transcoding is off by default
  and never runs automatically (not triggered by scans or the nightly
  pipeline)
- New transcode worker probes candidates (non mp4/webm extensions)
  with ffprobe: already-compatible files are marked skipped; AVI with
  H.264 is remuxed losslessly; incompatible codecs (MPEG-4 Part 2,
  WMV, RMVB, HEVC...) are transcoded to H.264/AAC MP4 with +faststart
- Transcoded output is uploaded back to the same storage under a
  "91转码" directory which is auto-added to the drive's scan skip list
  so the scanner never re-imports the artifacts
- Playback source automatically prefers the transcoded file once
  ready, keeping the 302 direct-link mode for cloud drives
- videos table gains transcode_status/error/file_id/size columns via
  startup migration; counts and live task status surface in the
  admin drives API and generation panel UI
- Stop semantics: per-drive stop button, drive-level "stop all tasks"
  and global stop all include the transcode task; interrupted videos
  keep their candidate status and resume on next start

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 09:41:08 +08:00

126 lines
3.9 KiB
Go

package transcode
import (
"strings"
"testing"
"github.com/video-site/backend/internal/catalog"
)
func TestNeedsTranscode(t *testing.T) {
cases := []struct {
name string
info MediaInfo
ext string
want bool
}{
{
name: "h264 aac mp4 is compatible",
info: MediaInfo{FormatName: "mov,mp4,m4a,3gp,3g2,mj2", VideoCodecs: []string{"h264"}, AudioCodecs: []string{"aac"}},
ext: "mp4",
want: false,
},
{
name: "mpeg4 in avi needs transcode",
info: MediaInfo{FormatName: "avi", VideoCodecs: []string{"mpeg4"}, AudioCodecs: []string{"mp3"}},
ext: "avi",
want: true,
},
{
name: "h264 in avi needs remux",
info: MediaInfo{FormatName: "avi", VideoCodecs: []string{"h264"}, AudioCodecs: []string{"aac"}},
ext: "avi",
want: true,
},
{
name: "hevc in mp4 needs transcode",
info: MediaInfo{FormatName: "mov,mp4,m4a,3gp,3g2,mj2", VideoCodecs: []string{"hevc"}, AudioCodecs: []string{"aac"}},
ext: "mp4",
want: true,
},
{
name: "vp9 opus webm is compatible",
info: MediaInfo{FormatName: "matroska,webm", VideoCodecs: []string{"vp9"}, AudioCodecs: []string{"opus"}},
ext: "webm",
want: false,
},
{
name: "h264 in mkv is conservative transcode",
info: MediaInfo{FormatName: "matroska,webm", VideoCodecs: []string{"h264"}, AudioCodecs: []string{"aac"}},
ext: "mkv",
want: true,
},
{
name: "pcm audio in mov needs transcode",
info: MediaInfo{FormatName: "mov,mp4,m4a,3gp,3g2,mj2", VideoCodecs: []string{"h264"}, AudioCodecs: []string{"pcm_s16le"}},
ext: "mov",
want: true,
},
{
name: "video only h264 mp4 is compatible",
info: MediaInfo{FormatName: "mov,mp4,m4a,3gp,3g2,mj2", VideoCodecs: []string{"h264"}},
ext: "mp4",
want: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := NeedsTranscode(tc.info, tc.ext); got != tc.want {
t.Fatalf("NeedsTranscode(%+v, %q) = %v, want %v", tc.info, tc.ext, got, tc.want)
}
})
}
}
func TestBuildFFmpegArgsRemuxWhenCodecsCompatible(t *testing.T) {
// AVI 里装 H.264+AAC:只需要换容器,应该走流拷贝
info := MediaInfo{FormatName: "avi", VideoCodecs: []string{"h264"}, AudioCodecs: []string{"aac"}}
args := strings.Join(buildFFmpegArgs(info, "in.avi", "out.mp4"), " ")
if !strings.Contains(args, "-c:v copy") {
t.Fatalf("expected video stream copy, got: %s", args)
}
if !strings.Contains(args, "-c:a copy") {
t.Fatalf("expected audio stream copy, got: %s", args)
}
if !strings.Contains(args, "+faststart") {
t.Fatalf("expected faststart flag, got: %s", args)
}
}
func TestBuildFFmpegArgsTranscodesIncompatibleCodecs(t *testing.T) {
info := MediaInfo{FormatName: "avi", VideoCodecs: []string{"mpeg4"}, AudioCodecs: []string{"wmav2"}}
args := strings.Join(buildFFmpegArgs(info, "in.avi", "out.mp4"), " ")
if !strings.Contains(args, "-c:v libx264") {
t.Fatalf("expected libx264 video encode, got: %s", args)
}
if !strings.Contains(args, "-c:a aac") {
t.Fatalf("expected aac audio encode, got: %s", args)
}
if !strings.Contains(args, "yuv420p") {
t.Fatalf("expected yuv420p pixel format, got: %s", args)
}
}
func TestBuildFFmpegArgsDropsAudioWhenNoAudioStream(t *testing.T) {
info := MediaInfo{FormatName: "avi", VideoCodecs: []string{"mpeg4"}}
args := strings.Join(buildFFmpegArgs(info, "in.avi", "out.mp4"), " ")
if !strings.Contains(args, "-an") {
t.Fatalf("expected -an for video without audio, got: %s", args)
}
}
func TestTranscodedName(t *testing.T) {
for _, tc := range []struct {
fileName, title, id, want string
}{
{"www.98T.la@167.avi", "www.98T.la@167", "p115-1", "www.98T.la@167.mp4"},
{"", "标题", "p115-2", "标题.mp4"},
{"a/b\\c.wmv", "", "p115-3", "a_b_c.mp4"},
} {
v := &catalog.Video{FileName: tc.fileName, Title: tc.title, ID: tc.id}
if got := transcodedName(v); got != tc.want {
t.Fatalf("transcodedName(%q,%q,%q) = %q, want %q", tc.fileName, tc.title, tc.id, got, tc.want)
}
}
}