First step of ADR-008. internal/transcode answers one question: can this host
hardware-transcode, and with which encoder.
The rule the package exists to enforce is that a device node is not proof. A
node can exist while the driver cannot encode, and that produces the worst
outcome ADR-008 describes: a transcode that starts, looks like it works, and
then cannot keep up. So every candidate runs a real two-frame probe encode and
is dropped unless that encode exits cleanly.
This host is a perfect illustration and the reason the rule is not theoretical:
it has /dev/dri/renderD128 AND Nvidia nodes. The render node exists, but VAAPI
fails to initialise against it ("Failed to initialise VAAPI connection"). A
detection that trusted the node would have picked VAAPI and been wrong. The
probe rejected it and selected NVENC, which was verified independently with
ffmpeg outside this code.
The probe is injected, so every branch is testable with no hardware present,
including the important one: device present, probe fails. Verified red-green -
ignoring the probe result fails TestDetectProbeFails with "got vaapi, want no
capability".
One real bug found while proving it: the probe used testsrc=size=64x48, below
NVENC's minimum frame size, so a host with working NVENC would have reported no
capability at all. Raised to 320x240, still two frames.
Also added internal/transcode to the arch layer map as a layer-1 primitive
beside probe and engine, which the arch rule caught immediately.
Not wired into the engine registry or playback yet - that is the next step.
174 lines
5 KiB
Go
174 lines
5 KiB
Go
package transcode
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
// TestDetectNoFFmpeg is the first gate: without ffmpeg there is no capability,
|
|
// full stop, and the candidates/probe are never consulted.
|
|
func TestDetectNoFFmpeg(t *testing.T) {
|
|
got := detect(checks{
|
|
ffmpeg: func() bool { return false },
|
|
candidates: func() []Encoder {
|
|
t.Fatal("candidates consulted without ffmpeg")
|
|
return nil
|
|
},
|
|
probe: func(Encoder) bool {
|
|
t.Fatal("probe run without ffmpeg")
|
|
return false
|
|
},
|
|
})
|
|
if got.Available() {
|
|
t.Errorf("no ffmpeg: got %+v, want no capability", got)
|
|
}
|
|
}
|
|
|
|
// TestDetectNoCandidates covers a host with ffmpeg but no device nodes at all.
|
|
func TestDetectNoCandidates(t *testing.T) {
|
|
got := detect(checks{
|
|
ffmpeg: func() bool { return true },
|
|
candidates: func() []Encoder { return nil },
|
|
probe: func(Encoder) bool {
|
|
t.Fatal("probe run with no candidates")
|
|
return false
|
|
},
|
|
})
|
|
if got.Available() {
|
|
t.Errorf("no candidates: got %+v, want no capability", got)
|
|
}
|
|
}
|
|
|
|
// TestDetectProbeFails is the branch ADR-008 exists for: a device node exists
|
|
// but the encode does not actually work. Reporting it would produce a transcode
|
|
// that starts and then cannot keep up.
|
|
func TestDetectProbeFails(t *testing.T) {
|
|
got := detect(checks{
|
|
ffmpeg: func() bool { return true },
|
|
candidates: func() []Encoder {
|
|
return []Encoder{{Name: EncoderVAAPI, Args: []string{"-c:v", "h264_vaapi"}}}
|
|
},
|
|
probe: func(Encoder) bool { return false },
|
|
})
|
|
if got.Available() {
|
|
t.Errorf("probe failed: got %+v, want no capability", got)
|
|
}
|
|
}
|
|
|
|
// TestDetectFirstWorkingWins checks the candidate order and that a later
|
|
// candidate is used when an earlier one fails.
|
|
func TestDetectFirstWorkingWins(t *testing.T) {
|
|
vaapi := Encoder{
|
|
Name: EncoderVAAPI,
|
|
Args: []string{"-vaapi_device", "/dev/dri/renderD128", "-vf", "format=nv12,hwupload", "-c:v", "h264_vaapi"},
|
|
}
|
|
nvenc := Encoder{Name: EncoderNVENC, Args: []string{"-c:v", "h264_nvenc"}}
|
|
qsv := Encoder{Name: EncoderQSV, Args: []string{"-c:v", "h264_qsv"}}
|
|
|
|
tests := []struct {
|
|
name string
|
|
ok map[string]bool
|
|
want Encoder
|
|
tried []string
|
|
}{
|
|
{
|
|
name: "vaapi works",
|
|
ok: map[string]bool{EncoderVAAPI: true},
|
|
want: vaapi,
|
|
tried: []string{EncoderVAAPI},
|
|
},
|
|
{
|
|
name: "vaapi fails, nvenc works",
|
|
ok: map[string]bool{EncoderNVENC: true},
|
|
want: nvenc,
|
|
tried: []string{EncoderVAAPI, EncoderNVENC},
|
|
},
|
|
{
|
|
name: "vaapi and nvenc fail, qsv works",
|
|
ok: map[string]bool{EncoderQSV: true},
|
|
want: qsv,
|
|
tried: []string{EncoderVAAPI, EncoderNVENC, EncoderQSV},
|
|
},
|
|
{
|
|
name: "none work",
|
|
ok: map[string]bool{},
|
|
want: Encoder{},
|
|
tried: []string{EncoderVAAPI, EncoderNVENC, EncoderQSV},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var tried []string
|
|
got := detect(checks{
|
|
ffmpeg: func() bool { return true },
|
|
candidates: func() []Encoder { return []Encoder{vaapi, nvenc, qsv} },
|
|
probe: func(e Encoder) bool {
|
|
tried = append(tried, e.Name)
|
|
return tc.ok[e.Name]
|
|
},
|
|
})
|
|
if tc.want.Name == "" {
|
|
if got.Available() {
|
|
t.Fatalf("got %+v, want no capability", got)
|
|
}
|
|
} else {
|
|
if got.Encoder != tc.want.Name {
|
|
t.Errorf("Encoder = %q, want %q", got.Encoder, tc.want.Name)
|
|
}
|
|
if !reflect.DeepEqual(got.Args, tc.want.Args) {
|
|
t.Errorf("Args = %q, want %q", got.Args, tc.want.Args)
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(tried, tc.tried) {
|
|
t.Errorf("probed %q, want %q", tried, tc.tried)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHostCandidatesOrder pins the candidate order the task requires:
|
|
// VAAPI, then NVENC, then QSV. It reads this host, so it only asserts on
|
|
// ordering, not on which nodes happen to exist.
|
|
func TestHostCandidatesOrder(t *testing.T) {
|
|
order := map[string]int{EncoderVAAPI: 0, EncoderNVENC: 1, EncoderQSV: 2}
|
|
prev := -1
|
|
for _, enc := range hostCandidates() {
|
|
i, ok := order[enc.Name]
|
|
if !ok {
|
|
t.Fatalf("hostCandidates returned unknown encoder %q", enc.Name)
|
|
}
|
|
if i <= prev {
|
|
t.Errorf("candidate %q out of order", enc.Name)
|
|
}
|
|
prev = i
|
|
if len(enc.Args) == 0 {
|
|
t.Errorf("candidate %q has no args", enc.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDetectReal runs the real detection once. It must not panic and must be
|
|
// consistent; on a host with no usable hardware it reports none. Cached, so
|
|
// calling it twice must return the same thing.
|
|
func TestDetectReal(t *testing.T) {
|
|
first := Detect()
|
|
t.Logf("real detection on this host: %+v", first)
|
|
if second := Detect(); !reflect.DeepEqual(first, second) {
|
|
t.Errorf("Detect not cached: %+v then %+v", first, second)
|
|
}
|
|
if first.Available() && first.Args == nil {
|
|
t.Error("Detect reported an encoder with nil args")
|
|
}
|
|
}
|
|
|
|
// TestProbeEncodeRejectsBogusEncoder proves the probe checks a real ffmpeg
|
|
// result rather than trusting the candidate's name. Skips without ffmpeg.
|
|
func TestProbeEncodeRejectsBogusEncoder(t *testing.T) {
|
|
if !ffmpegOnPath() {
|
|
t.Skip("ffmpeg not on PATH")
|
|
}
|
|
if probeEncode(Encoder{Name: "nope", Args: []string{"-c:v", "definitely_not_a_codec"}}) {
|
|
t.Error("probeEncode accepted a nonexistent encoder")
|
|
}
|
|
}
|