Skip to content

Commit 809120b

Browse files
committed
Use VAAPI decode and encode without scaling
1 parent 0436891 commit 809120b

5 files changed

Lines changed: 120 additions & 156 deletions

File tree

internal/transcode/ffmpeg_args_test.go

Lines changed: 14 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"os"
77
"path/filepath"
8+
"runtime"
89
"slices"
910
"strings"
1011
"testing"
@@ -158,15 +159,14 @@ func TestBuildFFmpegArgsUsesFullVAAPITranscodePipeline(t *testing.T) {
158159
if outputFormatIndex < 0 || args[outputFormatIndex+1] != "vaapi" {
159160
t.Fatalf("missing VAAPI hardware frame output: %v", args)
160161
}
161-
vfIndex := slices.Index(args, "-vf")
162-
if vfIndex < 0 || !strings.Contains(args[vfIndex+1], "scale_vaapi=") || !strings.Contains(args[vfIndex+1], "format=nv12") {
163-
t.Fatalf("expected VAAPI scale filter, args=%v", args)
164-
}
165162
codecIndex := slices.Index(args, "-c:v")
166163
if codecIndex < 0 || args[codecIndex+1] != "h264_vaapi" {
167164
t.Fatalf("expected VAAPI H.264 encoder, args=%v", args)
168165
}
169-
for _, softwareOnly := range []string{"libx264", "-preset", "-tune"} {
166+
if slices.Contains(args, "-vf") {
167+
t.Fatalf("full VAAPI pipeline should not scale, args=%v", args)
168+
}
169+
for _, softwareOnly := range []string{"libx264", "-preset", "-tune", "-level"} {
170170
if slices.Contains(args, softwareOnly) {
171171
t.Fatalf("VAAPI pipeline should not include software encoder arg %q: %v", softwareOnly, args)
172172
}
@@ -278,7 +278,7 @@ func TestResolveHardwareDecodeKeepsVAAPIWhenProbePasses(t *testing.T) {
278278
return nil
279279
})
280280

281-
if options.HardwareDecode != "vaapi" || options.HardwareDevice != "/dev/dri/renderD128" {
281+
if options.HardwareDecode != "vaapi" || options.HardwareDevice != "/dev/dri/renderD128" || options.HardwarePipeline != "vaapi-full" {
282282
t.Fatalf("options = %+v", options)
283283
}
284284
}
@@ -316,69 +316,10 @@ func TestResolveHardwareDecodeFalseSkipsHardwareProbe(t *testing.T) {
316316
}
317317
}
318318

319-
func TestResolveHardwareDecodeFallsBackToVAAPIEncodeWhenFullPipelineUnsupported(t *testing.T) {
320-
tempDir := t.TempDir()
321-
ffmpegPath := filepath.Join(tempDir, "ffmpeg")
322-
callsPath := filepath.Join(tempDir, "calls")
323-
script := fmt.Sprintf(`#!/bin/sh
324-
echo "$@" >> %q
325-
case " $* " in
326-
*" -hwaccels "*)
327-
printf 'Hardware acceleration methods:\nvaapi\n'
328-
exit 0
329-
;;
330-
*" -encoders "*)
331-
printf ' V....D h264_vaapi H.264/AVC (VAAPI)\n'
332-
exit 0
333-
;;
334-
*" -filters "*)
335-
printf ' ... scale_vaapi V->V Scale to/from VAAPI surfaces.\n'
336-
exit 0
337-
;;
338-
*" -init_hw_device "*"scale_vaapi="*)
339-
echo 'Failed to create processing pipeline config: 12 (the requested VAProfile is not supported).' >&2
340-
exit 1
341-
;;
342-
*" -vaapi_device "*"hwupload"*)
343-
exit 0
344-
;;
345-
*" -init_hw_device "*)
346-
exit 0
347-
;;
348-
esac
349-
exit 0
350-
`, callsPath)
351-
if err := os.WriteFile(ffmpegPath, []byte(script), 0o755); err != nil {
352-
t.Fatal(err)
353-
}
354-
devicePath := filepath.Join(tempDir, "renderD128")
355-
if err := os.WriteFile(devicePath, nil, 0o600); err != nil {
356-
t.Fatal(err)
357-
}
358-
359-
options, err := resolveHardwareDecodeOptionsStrict(ffmpegPath, FFmpegOptions{
360-
HardwareDecode: "vaapi",
361-
HardwareDevice: devicePath,
362-
}, nil)
363-
if err != nil {
364-
t.Fatal(err)
365-
}
366-
367-
if options.HardwarePipeline != "vaapi-encode" {
368-
t.Fatalf("hardware pipeline = %q", options.HardwarePipeline)
369-
}
370-
calls, err := os.ReadFile(callsPath)
371-
if err != nil {
372-
t.Fatal(err)
373-
}
374-
for _, want := range []string{"scale_vaapi=", "-vaapi_device " + devicePath, "hwupload", "-c:v h264_vaapi"} {
375-
if !strings.Contains(string(calls), want) {
376-
t.Fatalf("expected probe call containing %q, calls=%s", want, calls)
377-
}
378-
}
379-
}
380-
381319
func TestDefaultHardwareProbeRejectsVAAPIWhenDeviceInitializationFails(t *testing.T) {
320+
if runtime.GOOS == "windows" {
321+
t.Skip("linux-only ffmpeg probe test")
322+
}
382323
tempDir := t.TempDir()
383324
ffmpegPath := filepath.Join(tempDir, "ffmpeg")
384325
callsPath := filepath.Join(tempDir, "calls")
@@ -393,10 +334,6 @@ case " $* " in
393334
printf ' V....D h264_vaapi H.264/AVC (VAAPI)\n'
394335
exit 0
395336
;;
396-
*" -filters "*)
397-
printf ' ... scale_vaapi V->V Scale to/from VAAPI surfaces.\n'
398-
exit 0
399-
;;
400337
*" -init_hw_device "*)
401338
echo 'Failed to initialise VAAPI connection' >&2
402339
exit 1
@@ -432,7 +369,10 @@ exit 0
432369
}
433370
}
434371

435-
func TestDefaultHardwareProbeRequiresFullVAAPIPipeline(t *testing.T) {
372+
func TestDefaultHardwareProbeUsesVAAPIBaseOnly(t *testing.T) {
373+
if runtime.GOOS == "windows" {
374+
t.Skip("linux-only ffmpeg probe test")
375+
}
436376
tempDir := t.TempDir()
437377
ffmpegPath := filepath.Join(tempDir, "ffmpeg")
438378
callsPath := filepath.Join(tempDir, "calls")
@@ -447,10 +387,6 @@ case " $* " in
447387
printf ' V....D h264_vaapi H.264/AVC (VAAPI)\n'
448388
exit 0
449389
;;
450-
*" -filters "*)
451-
printf ' ... scale_vaapi V->V Scale to/from VAAPI surfaces.\n'
452-
exit 0
453-
;;
454390
*" -init_hw_device "*)
455391
exit 0
456392
;;
@@ -476,7 +412,7 @@ exit 0
476412
if err != nil {
477413
t.Fatal(err)
478414
}
479-
for _, want := range []string{"-hwaccels", "-encoders", "-filters", "-init_hw_device vaapi=probe:" + devicePath, "-filter_hw_device probe", "scale_vaapi=", "-c:v h264_vaapi"} {
415+
for _, want := range []string{"-hwaccels", "-encoders", "-init_hw_device vaapi=probe:" + devicePath} {
480416
if !strings.Contains(string(calls), want) {
481417
t.Fatalf("expected hardware probe call containing %q, calls=%s", want, calls)
482418
}

internal/transcode/manager.go

Lines changed: 7 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"strings"
1515
"sync"
1616
"sync/atomic"
17-
"syscall"
1817
"time"
1918

2019
"emby-transcoder/internal/logging"
@@ -893,14 +892,13 @@ func resolveHardwareDecodeOptionsStrict(ffmpegPath string, options FFmpegOptions
893892
}
894893

895894
if probe == nil {
896-
return resolveDefaultHardwareDecodeOptions(ffmpegPath, options)
897-
}
898-
if err := probe(ffmpegPath, options); err != nil {
895+
if err := probeVAAPIBase(ffmpegPath, options); err != nil {
896+
return FFmpegOptions{}, fmt.Errorf("hardware decode unavailable mode=%s device=%s: %w", options.HardwareDecode, options.HardwareDevice, err)
897+
}
898+
} else if err := probe(ffmpegPath, options); err != nil {
899899
return FFmpegOptions{}, fmt.Errorf("hardware decode unavailable mode=%s device=%s: %w", options.HardwareDecode, options.HardwareDevice, err)
900900
}
901-
if options.HardwarePipeline == "" {
902-
options.HardwarePipeline = "vaapi-full"
903-
}
901+
options.HardwarePipeline = "vaapi-full"
904902
return options, nil
905903
}
906904

@@ -910,16 +908,7 @@ func resolveDefaultHardwareDecodeOptions(ffmpegPath string, options FFmpegOption
910908
if err := probeVAAPIBase(ffmpegPath, options); err != nil {
911909
return FFmpegOptions{}, fmt.Errorf("hardware decode unavailable mode=%s device=%s: %w", options.HardwareDecode, options.HardwareDevice, err)
912910
}
913-
if err := probeFFmpegVAAPIFullPipeline(ffmpegPath, options.HardwareDevice); err == nil {
914-
options.HardwarePipeline = "vaapi-full"
915-
return options, nil
916-
} else {
917-
logging.Infof("hardware transcode full vaapi unavailable device=%s reason=%v fallback=vaapi-encode", options.HardwareDevice, err)
918-
}
919-
if err := probeFFmpegVAAPIEncodePipeline(ffmpegPath, options.HardwareDevice); err != nil {
920-
return FFmpegOptions{}, fmt.Errorf("hardware decode unavailable mode=%s device=%s: %w", options.HardwareDecode, options.HardwareDevice, err)
921-
}
922-
options.HardwarePipeline = "vaapi-encode"
911+
options.HardwarePipeline = "vaapi-full"
923912
return options, nil
924913
default:
925914
return FFmpegOptions{}, fmt.Errorf("unsupported hardware decode mode %q", options.HardwareDecode)
@@ -932,15 +921,7 @@ func defaultHardwareProbe(ffmpegPath string, options FFmpegOptions) error {
932921
}
933922
switch options.HardwareDecode {
934923
case "vaapi":
935-
if err := probeVAAPIBase(ffmpegPath, options); err != nil {
936-
return err
937-
}
938-
if err := probeFFmpegVAAPIFullPipeline(ffmpegPath, options.HardwareDevice); err == nil {
939-
return nil
940-
} else {
941-
logging.Infof("hardware transcode full vaapi unavailable device=%s reason=%v fallback=vaapi-encode", options.HardwareDevice, err)
942-
}
943-
return probeFFmpegVAAPIEncodePipeline(ffmpegPath, options.HardwareDevice)
924+
return probeVAAPIBase(ffmpegPath, options)
944925
default:
945926
return fmt.Errorf("unsupported hardware decode mode %q", options.HardwareDecode)
946927
}
@@ -953,9 +934,6 @@ func probeVAAPIBase(ffmpegPath string, options FFmpegOptions) error {
953934
if err := probeFFmpegEncoder(ffmpegPath, "h264_vaapi"); err != nil {
954935
return err
955936
}
956-
if err := probeFFmpegFilter(ffmpegPath, "scale_vaapi"); err != nil {
957-
return err
958-
}
959937
if err := probeDevice(options.HardwareDevice); err != nil {
960938
return err
961939
}
@@ -1270,10 +1248,8 @@ func appendVideoTranscodeArgs(args []string, options FFmpegOptions) []string {
12701248
switch hardwarePipeline(options) {
12711249
case "vaapi-full":
12721250
return append(args,
1273-
"-vf", vaapiScaleFilter(maxTranscodeWidth, maxTranscodeHeight),
12741251
"-c:v", "h264_vaapi",
12751252
"-profile:v", "high",
1276-
"-level", "4.1",
12771253
"-g", strconv.Itoa(lowLatencyGOP),
12781254
"-keyint_min", strconv.Itoa(lowLatencyGOP),
12791255
"-bf", "0",
@@ -1368,31 +1344,6 @@ func (p *execProcess) Stop() error {
13681344
return p.StopWithGrace(5 * time.Second)
13691345
}
13701346

1371-
func (p *execProcess) StopWithGrace(grace time.Duration) error {
1372-
if p.cmd == nil || p.cmd.Process == nil {
1373-
return nil
1374-
}
1375-
if p.Done() {
1376-
return nil
1377-
}
1378-
if p.paused.Load() && p.cmd != nil && p.cmd.Process != nil {
1379-
_ = p.cmd.Process.Signal(syscall.SIGCONT)
1380-
p.paused.Store(false)
1381-
}
1382-
if p.stdin != nil {
1383-
_, _ = io.WriteString(p.stdin, "q\n")
1384-
_ = p.stdin.Close()
1385-
}
1386-
if p.waitForExit(grace) {
1387-
return nil
1388-
}
1389-
err := p.cmd.Process.Kill()
1390-
if p.waitForExit(2 * time.Second) {
1391-
return nil
1392-
}
1393-
return err
1394-
}
1395-
13961347
func (p *execProcess) Done() bool {
13971348
return p.done.Load()
13981349
}
@@ -1414,28 +1365,6 @@ func (p *execProcess) waitForExit(timeout time.Duration) bool {
14141365
}
14151366
}
14161367

1417-
func (p *execProcess) Pause() error {
1418-
if p.cmd == nil || p.cmd.Process == nil || p.Done() || p.paused.Load() {
1419-
return nil
1420-
}
1421-
if err := p.cmd.Process.Signal(syscall.SIGSTOP); err != nil {
1422-
return err
1423-
}
1424-
p.paused.Store(true)
1425-
return nil
1426-
}
1427-
1428-
func (p *execProcess) Resume() error {
1429-
if p.cmd == nil || p.cmd.Process == nil || p.Done() || !p.paused.Load() {
1430-
return nil
1431-
}
1432-
if err := p.cmd.Process.Signal(syscall.SIGCONT); err != nil {
1433-
return err
1434-
}
1435-
p.paused.Store(false)
1436-
return nil
1437-
}
1438-
14391368
func ffmpegHeaders(headers http.Header) string {
14401369
var lines []string
14411370
for _, key := range []string{"Authorization", "X-Emby-Authorization", "X-Emby-Token", "User-Agent"} {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//go:build !windows
2+
3+
package transcode
4+
5+
import (
6+
"io"
7+
"syscall"
8+
"time"
9+
)
10+
11+
func (p *execProcess) StopWithGrace(grace time.Duration) error {
12+
if p.cmd == nil || p.cmd.Process == nil {
13+
return nil
14+
}
15+
if p.Done() {
16+
return nil
17+
}
18+
if p.paused.Load() && p.cmd != nil && p.cmd.Process != nil {
19+
_ = p.cmd.Process.Signal(syscall.SIGCONT)
20+
p.paused.Store(false)
21+
}
22+
if p.stdin != nil {
23+
_, _ = io.WriteString(p.stdin, "q\n")
24+
_ = p.stdin.Close()
25+
}
26+
if p.waitForExit(grace) {
27+
return nil
28+
}
29+
err := p.cmd.Process.Kill()
30+
if p.waitForExit(2 * time.Second) {
31+
return nil
32+
}
33+
return err
34+
}
35+
36+
func (p *execProcess) Pause() error {
37+
if p.cmd == nil || p.cmd.Process == nil || p.Done() || p.paused.Load() {
38+
return nil
39+
}
40+
if err := p.cmd.Process.Signal(syscall.SIGSTOP); err != nil {
41+
return err
42+
}
43+
p.paused.Store(true)
44+
return nil
45+
}
46+
47+
func (p *execProcess) Resume() error {
48+
if p.cmd == nil || p.cmd.Process == nil || p.Done() || !p.paused.Load() {
49+
return nil
50+
}
51+
if err := p.cmd.Process.Signal(syscall.SIGCONT); err != nil {
52+
return err
53+
}
54+
p.paused.Store(false)
55+
return nil
56+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//go:build windows
2+
3+
package transcode
4+
5+
import (
6+
"io"
7+
"time"
8+
)
9+
10+
func (p *execProcess) StopWithGrace(grace time.Duration) error {
11+
if p.cmd == nil || p.cmd.Process == nil {
12+
return nil
13+
}
14+
if p.Done() {
15+
return nil
16+
}
17+
if p.stdin != nil {
18+
_, _ = io.WriteString(p.stdin, "q\n")
19+
_ = p.stdin.Close()
20+
}
21+
if p.waitForExit(grace) {
22+
return nil
23+
}
24+
err := p.cmd.Process.Kill()
25+
if p.waitForExit(2 * time.Second) {
26+
return nil
27+
}
28+
return err
29+
}
30+
31+
func (p *execProcess) Pause() error {
32+
p.paused.Store(true)
33+
return nil
34+
}
35+
36+
func (p *execProcess) Resume() error {
37+
p.paused.Store(false)
38+
return nil
39+
}

0 commit comments

Comments
 (0)