259 lines
8.4 KiB
Go
259 lines
8.4 KiB
Go
package playback
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestBuildFFmpegArgs_QSVDropsSuperfastPreset(t *testing.T) {
|
|
args := buildFFmpegArgs(TranscodeOpts{
|
|
InputPath: "/media/movie.mkv",
|
|
OutputDir: "/tmp/out",
|
|
SessionID: "session-1",
|
|
TargetCodecVideo: "h264",
|
|
TargetCodecAudio: "aac",
|
|
SegmentDuration: 2,
|
|
HWAccel: "qsv",
|
|
FastStart: true,
|
|
TargetResolution: "1080p",
|
|
TargetBitrateKbps: 2000,
|
|
})
|
|
|
|
joined := strings.Join(args, " ")
|
|
if strings.Contains(joined, "-preset superfast") {
|
|
t.Fatalf("QSV args should not use superfast preset: %s", joined)
|
|
}
|
|
if !strings.Contains(joined, "-preset veryfast") {
|
|
t.Fatalf("QSV args should use veryfast preset: %s", joined)
|
|
}
|
|
}
|
|
|
|
func TestBuildFFmpegArgs_CPUPreservesSuperfastFastStart(t *testing.T) {
|
|
args := buildFFmpegArgs(TranscodeOpts{
|
|
InputPath: "/media/movie.mkv",
|
|
OutputDir: "/tmp/out",
|
|
SessionID: "session-1",
|
|
TargetCodecVideo: "h264",
|
|
TargetCodecAudio: "aac",
|
|
SegmentDuration: 2,
|
|
HWAccel: "none",
|
|
FastStart: true,
|
|
TargetResolution: "1080p",
|
|
})
|
|
|
|
joined := strings.Join(args, " ")
|
|
if !strings.Contains(joined, "-preset superfast") {
|
|
t.Fatalf("CPU args should preserve superfast preset: %s", joined)
|
|
}
|
|
}
|
|
|
|
func TestBuildFFmpegArgs_CopyVideoFromStartUsesZeroBasedTimestamps(t *testing.T) {
|
|
args := buildFFmpegArgs(TranscodeOpts{
|
|
InputPath: "/media/movie.mkv",
|
|
OutputDir: "/tmp/out",
|
|
SessionID: "session-copy",
|
|
TargetCodecVideo: "copy",
|
|
TargetCodecAudio: "aac",
|
|
SegmentDuration: 2,
|
|
})
|
|
|
|
joined := strings.Join(args, " ")
|
|
if strings.Contains(joined, "-copyts") {
|
|
t.Fatalf("copy-video from-start should not preserve source timestamps: %s", joined)
|
|
}
|
|
if !strings.Contains(joined, "-avoid_negative_ts make_zero") {
|
|
t.Fatalf("copy-video from-start should zero-base timestamps: %s", joined)
|
|
}
|
|
}
|
|
|
|
func TestBuildFFmpegArgs_CopyVideoResumePreservesSourceTimestamps(t *testing.T) {
|
|
args := buildFFmpegArgs(TranscodeOpts{
|
|
InputPath: "/media/movie.mkv",
|
|
OutputDir: "/tmp/out",
|
|
SessionID: "session-copy-resume",
|
|
SeekSeconds: 478.0,
|
|
StartSegmentNumber: 239,
|
|
TargetCodecVideo: "copy",
|
|
TargetCodecAudio: "aac",
|
|
SegmentDuration: 2,
|
|
})
|
|
|
|
joined := strings.Join(args, " ")
|
|
// Resume must preserve source timestamps so TFDT in seg_K matches
|
|
// playlist time K*segDur (the EXT-X-START anchor). Without -copyts,
|
|
// strict players (ATV / ExoPlayer) treat the TFDT/playlist mismatch
|
|
// as a discontinuity and abort.
|
|
if !strings.Contains(joined, "-copyts") {
|
|
t.Fatalf("copy-video resume should preserve source timestamps: %s", joined)
|
|
}
|
|
if !strings.Contains(joined, "-avoid_negative_ts disabled") {
|
|
t.Fatalf("copy-video resume should disable negative-ts adjustment: %s", joined)
|
|
}
|
|
if strings.Contains(joined, "-avoid_negative_ts make_zero") {
|
|
t.Fatalf("copy-video resume must not zero-base timestamps (ATV resume regression): %s", joined)
|
|
}
|
|
}
|
|
|
|
func TestBuildFFmpegArgs_CopyVideoSeekPreservesCodecCopy(t *testing.T) {
|
|
args := buildFFmpegArgs(TranscodeOpts{
|
|
InputPath: "/media/movie.mkv",
|
|
OutputDir: "/tmp/out",
|
|
SessionID: "session-copy-seek",
|
|
SeekSeconds: 240.86,
|
|
StartSegmentNumber: 120,
|
|
TargetCodecVideo: "copy",
|
|
TargetCodecAudio: "aac",
|
|
SegmentDuration: 2,
|
|
})
|
|
|
|
joined := strings.Join(args, " ")
|
|
|
|
// Video must remain copy — no re-encoding.
|
|
if !strings.Contains(joined, "-c:v copy") {
|
|
t.Fatalf("copy-mode seek should preserve -c:v copy: %s", joined)
|
|
}
|
|
// Must not contain any video encoder.
|
|
for _, enc := range []string{"h264_qsv", "h264_vaapi", "libx264", "hevc_qsv"} {
|
|
if strings.Contains(joined, enc) {
|
|
t.Fatalf("copy-mode seek should not use encoder %s: %s", enc, joined)
|
|
}
|
|
}
|
|
// Seek must be before input.
|
|
ssIdx := strings.Index(joined, "-ss")
|
|
iIdx := strings.Index(joined, "-i ")
|
|
if ssIdx < 0 || iIdx < 0 || ssIdx > iIdx {
|
|
t.Fatalf("seek (-ss) should appear before input (-i): %s", joined)
|
|
}
|
|
// Audio should be transcoded to AAC.
|
|
if !strings.Contains(joined, "-c:a aac") {
|
|
t.Fatalf("copy-mode seek should transcode audio to AAC: %s", joined)
|
|
}
|
|
// Should use -noaccurate_seek for copy video + transcode audio.
|
|
if !strings.Contains(joined, "-noaccurate_seek") {
|
|
t.Fatalf("copy-mode seek with audio transcode should use -noaccurate_seek: %s", joined)
|
|
}
|
|
// Should use fMP4 segments.
|
|
if !strings.Contains(joined, "-hls_segment_type fmp4") {
|
|
t.Fatalf("copy-mode should use fMP4 segments: %s", joined)
|
|
}
|
|
// Should have start_number for seek alignment.
|
|
if !strings.Contains(joined, "-start_number 120") {
|
|
t.Fatalf("copy-mode seek should set start_number: %s", joined)
|
|
}
|
|
}
|
|
|
|
func TestBuildFFmpegArgs_MPEG2CopyVideoUsesMPEGTS(t *testing.T) {
|
|
args := buildFFmpegArgs(TranscodeOpts{
|
|
InputPath: "/media/movie.mkv",
|
|
OutputDir: "/tmp/out",
|
|
SessionID: "session-mpeg2-copy",
|
|
SourceVideoCodec: "mpeg2video",
|
|
TargetCodecVideo: "copy",
|
|
TargetCodecAudio: "aac",
|
|
SegmentDuration: 2,
|
|
})
|
|
|
|
joined := strings.Join(args, " ")
|
|
if !strings.Contains(joined, "-c:v copy") {
|
|
t.Fatalf("mpeg2 copy-mode should preserve video copy: %s", joined)
|
|
}
|
|
if !strings.Contains(joined, "-hls_segment_type mpegts") {
|
|
t.Fatalf("mpeg2 copy-mode should use MPEG-TS HLS segments: %s", joined)
|
|
}
|
|
if !strings.Contains(joined, "seg_%05d.ts") {
|
|
t.Fatalf("mpeg2 copy-mode should write .ts segments: %s", joined)
|
|
}
|
|
if strings.Contains(joined, "movflags=+frag_discont") {
|
|
t.Fatalf("mpeg2 MPEG-TS copy-mode should not use fMP4 movflags: %s", joined)
|
|
}
|
|
for _, enc := range []string{"h264_qsv", "h264_vaapi", "libx264", "hevc_qsv", "libx265"} {
|
|
if strings.Contains(joined, enc) {
|
|
t.Fatalf("mpeg2 copy-mode should not use encoder %s: %s", enc, joined)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildFFmpegArgs_MPEG4Part2DisablesHardwareDecode(t *testing.T) {
|
|
for _, hwAccel := range []string{"qsv", "vaapi"} {
|
|
t.Run(hwAccel, func(t *testing.T) {
|
|
args := buildFFmpegArgs(TranscodeOpts{
|
|
InputPath: "/media/xvid.avi",
|
|
OutputDir: "/tmp/out",
|
|
SessionID: "session-xvid",
|
|
SourceVideoCodec: "mpeg4",
|
|
TargetCodecVideo: "h264",
|
|
TargetCodecAudio: "aac",
|
|
SegmentDuration: 2,
|
|
HWAccel: hwAccel,
|
|
TargetResolution: "420p",
|
|
TargetBitrateKbps: 720,
|
|
})
|
|
|
|
joined := strings.Join(args, " ")
|
|
for _, forbidden := range []string{
|
|
"-hwaccel vaapi",
|
|
"h264_qsv",
|
|
"h264_vaapi",
|
|
"scale_vaapi",
|
|
"hwmap=derive_device=qsv",
|
|
} {
|
|
if strings.Contains(joined, forbidden) {
|
|
t.Fatalf("mpeg4 part 2 source should use software transcode, found %q: %s", forbidden, joined)
|
|
}
|
|
}
|
|
if !strings.Contains(joined, "-c:v libx264") {
|
|
t.Fatalf("mpeg4 part 2 source should fall back to libx264: %s", joined)
|
|
}
|
|
if !strings.Contains(joined, "-vf scale=-2:420") {
|
|
t.Fatalf("mpeg4 part 2 software fallback should preserve requested scaling: %s", joined)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildFFmpegArgs_VAAPIScalingUsesHardwareFilter(t *testing.T) {
|
|
args := buildFFmpegArgs(TranscodeOpts{
|
|
InputPath: "/media/movie.mkv",
|
|
OutputDir: "/tmp/out",
|
|
SessionID: "session-vaapi",
|
|
SourceVideoCodec: "h264",
|
|
TargetCodecVideo: "h264",
|
|
TargetCodecAudio: "aac",
|
|
SegmentDuration: 2,
|
|
HWAccel: "vaapi",
|
|
TargetResolution: "720p",
|
|
TargetBitrateKbps: 2000,
|
|
})
|
|
|
|
joined := strings.Join(args, " ")
|
|
if !strings.Contains(joined, "-hwaccel vaapi") {
|
|
t.Fatalf("vaapi args should enable vaapi hwaccel: %s", joined)
|
|
}
|
|
if !strings.Contains(joined, "-vf scale_vaapi=w=-2:h=720:format=nv12") {
|
|
t.Fatalf("vaapi args should use scale_vaapi, not software scale: %s", joined)
|
|
}
|
|
if strings.Contains(joined, "-vf scale=-2:720") {
|
|
t.Fatalf("vaapi args must not use software scale on hardware frames: %s", joined)
|
|
}
|
|
}
|
|
|
|
func TestBuildFFmpegArgs_EncodedTranscodePreservesExistingTimestampPolicy(t *testing.T) {
|
|
args := buildFFmpegArgs(TranscodeOpts{
|
|
InputPath: "/media/movie.mkv",
|
|
OutputDir: "/tmp/out",
|
|
SessionID: "session-encoded",
|
|
SeekSeconds: 2780.63,
|
|
TargetCodecVideo: "h264",
|
|
TargetCodecAudio: "aac",
|
|
SegmentDuration: 2,
|
|
})
|
|
|
|
joined := strings.Join(args, " ")
|
|
if !strings.Contains(joined, "-copyts") {
|
|
t.Fatalf("encoded args should preserve original timestamps: %s", joined)
|
|
}
|
|
if !strings.Contains(joined, "-avoid_negative_ts disabled") {
|
|
t.Fatalf("encoded args should keep avoid_negative_ts disabled: %s", joined)
|
|
}
|
|
}
|