127 lines
4.1 KiB
Go
127 lines
4.1 KiB
Go
package engine
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
func TestDoubleBitrate(t *testing.T) {
|
||
cases := map[string]string{
|
||
"6000k": "12000k",
|
||
"25000k": "50000k",
|
||
"1500k": "3000k",
|
||
"5M": "10M",
|
||
"1.5M": "3M",
|
||
"2.5m": "5m",
|
||
"800000": "1600000",
|
||
"": "",
|
||
"garbage": "garbage", // unparseable → unchanged (1× bufsize fallback)
|
||
"-5M": "-5M", // non-positive → unchanged
|
||
}
|
||
for in, want := range cases {
|
||
if got := doubleBitrate(in); got != want {
|
||
t.Errorf("doubleBitrate(%q) = %q, want %q", in, got, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// segmentIdxForTime must be the exact inverse of segmentStartSec so the
|
||
// resume-aware first spawn (HLSSessionConfig.StartSec) lands on the same
|
||
// segment the player's hls.js startPosition will request.
|
||
func TestSegmentIdxForTime(t *testing.T) {
|
||
cases := map[float64]int{
|
||
0: 0,
|
||
-3: 0,
|
||
0.5: 0,
|
||
1.99: 0,
|
||
2: 1,
|
||
3.9: 1,
|
||
60: 30,
|
||
3599.9: 1799,
|
||
}
|
||
for sec, want := range cases {
|
||
if got := segmentIdxForTime(sec); got != want {
|
||
t.Errorf("segmentIdxForTime(%v) = %d, want %d", sec, got, want)
|
||
}
|
||
}
|
||
// Round-trip: the start time of the segment we resolve must never be
|
||
// AFTER the requested position (the player would miss its first frames).
|
||
for _, sec := range []float64{0, 1, 2, 7.3, 119.9, 4321} {
|
||
idx := segmentIdxForTime(sec)
|
||
if start := segmentStartSec(idx); start > sec {
|
||
t.Errorf("segmentStartSec(segmentIdxForTime(%v)) = %v > %v", sec, start, sec)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Capped constant-quality rate control: libx264 gets -crf (no -b:v), NVENC
|
||
// gets -cq with -b:v 0, both keep -maxrate at the level-coherent cap and a
|
||
// 2× -bufsize. VAAPI (and the other vendor encoders) keep the proven
|
||
// fixed-bitrate triple untouched.
|
||
func TestBuildHLSFFmpegArgsRateControl(t *testing.T) {
|
||
probe := &StreamProbe{Width: 1920, Height: 1080, DurationSec: 100}
|
||
base := HLSSessionConfig{
|
||
SessionID: "test",
|
||
SourcePath: "/media/Movie.mkv",
|
||
Quality: "1080p",
|
||
Transcode: TranscodeRuntime{
|
||
FFmpegPath: "/usr/bin/ffmpeg",
|
||
FFprobePath: "/usr/bin/ffprobe",
|
||
},
|
||
}
|
||
|
||
t.Run("libx264 capped CRF", func(t *testing.T) {
|
||
cfg := base
|
||
cfg.Transcode.HWAccel = HWAccelNone
|
||
got := strings.Join(buildHLSFFmpegArgsAt(cfg, probe, "/tmp/tmpdir", 0, 0), " ")
|
||
for _, want := range []string{"-crf 23", "-maxrate 6000k", "-bufsize 12000k"} {
|
||
if !strings.Contains(got, want) {
|
||
t.Errorf("libx264 argv missing %q\n%s", want, got)
|
||
}
|
||
}
|
||
if strings.Contains(got, "-b:v 6000k") {
|
||
t.Errorf("libx264 argv must not carry -b:v alongside -crf\n%s", got)
|
||
}
|
||
})
|
||
|
||
t.Run("nvenc constant-quality VBR", func(t *testing.T) {
|
||
cfg := base
|
||
cfg.Transcode.HWAccel = HWAccelNVENC
|
||
got := strings.Join(buildHLSFFmpegArgsAt(cfg, probe, "/tmp/tmpdir", 0, 0), " ")
|
||
// -forced-idr 1 is load-bearing: without it NVENC emits the forced
|
||
// keyframes as non-IDR and every HLS segment stretches to the full
|
||
// GOP, desyncing the playlist timeline (subs/seeks).
|
||
for _, want := range []string{"-rc vbr", "-cq 23", "-b:v 0", "-maxrate 6000k", "-bufsize 12000k", "-forced-idr 1"} {
|
||
if !strings.Contains(got, want) {
|
||
t.Errorf("nvenc argv missing %q\n%s", want, got)
|
||
}
|
||
}
|
||
})
|
||
|
||
t.Run("qsv keeps bitrate + forced_idr", func(t *testing.T) {
|
||
cfg := base
|
||
cfg.Transcode.HWAccel = HWAccelQSV
|
||
got := strings.Join(buildHLSFFmpegArgsAt(cfg, probe, "/tmp/tmpdir", 0, 0), " ")
|
||
// -forced_idr 1 (QSV's spelling): same non-IDR forced-keyframe failure
|
||
// mode as NVENC — without it segments stretch to the full GOP.
|
||
for _, want := range []string{"-look_ahead 0", "-forced_idr 1", "-b:v 6000k"} {
|
||
if !strings.Contains(got, want) {
|
||
t.Errorf("qsv argv missing %q\n%s", want, got)
|
||
}
|
||
}
|
||
})
|
||
|
||
t.Run("vaapi keeps fixed-bitrate triple", func(t *testing.T) {
|
||
cfg := base
|
||
cfg.Transcode.HWAccel = HWAccelVAAPI
|
||
got := strings.Join(buildHLSFFmpegArgsAt(cfg, probe, "/tmp/tmpdir", 0, 0), " ")
|
||
for _, want := range []string{"-b:v 6000k", "-maxrate 6000k", "-bufsize 6000k"} {
|
||
if !strings.Contains(got, want) {
|
||
t.Errorf("vaapi argv missing %q\n%s", want, got)
|
||
}
|
||
}
|
||
if strings.Contains(got, "-crf") || strings.Contains(got, "-cq") {
|
||
t.Errorf("vaapi argv must not carry constant-quality flags\n%s", got)
|
||
}
|
||
})
|
||
}
|