Skip to content

Commit b62d966

Browse files
committed
Fallback VAAPI decode failures to encode-only pipeline
1 parent 1ed63da commit b62d966

2 files changed

Lines changed: 70 additions & 9 deletions

File tree

internal/transcode/ffmpeg_args_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,30 @@ func TestBuildFFmpegArgsUsesVAAPILowPowerEncoding(t *testing.T) {
252252
}
253253
}
254254

255+
func TestEffectiveFFmpegOptionsUsesSessionPipelineOverride(t *testing.T) {
256+
options := effectiveFFmpegOptions(&Session{HardwarePipeline: "vaapi-encode"}, FFmpegOptions{
257+
HardwareDecode: "vaapi",
258+
HardwareDevice: "/dev/dri/renderD128",
259+
HardwarePipeline: "vaapi-full",
260+
})
261+
262+
if options.HardwarePipeline != "vaapi-encode" {
263+
t.Fatalf("pipeline = %q", options.HardwarePipeline)
264+
}
265+
266+
args := buildFFmpegArgs(&Session{ID: "item123", Dir: t.TempDir()}, Request{InputURL: "http://upstream/stream"}, options)
267+
if slices.Contains(args, "-hwaccel") {
268+
t.Fatalf("fallback pipeline should not use VAAPI hardware decode: %v", args)
269+
}
270+
if deviceIndex := slices.Index(args, "-vaapi_device"); deviceIndex < 0 || args[deviceIndex+1] != "/dev/dri/renderD128" {
271+
t.Fatalf("fallback pipeline should keep VAAPI encode device: %v", args)
272+
}
273+
vfIndex := slices.Index(args, "-vf")
274+
if vfIndex < 0 || !strings.Contains(args[vfIndex+1], "format=nv12,hwupload") {
275+
t.Fatalf("fallback pipeline should upload software-decoded frames: %v", args)
276+
}
277+
}
278+
255279
func TestBuildFFmpegArgsCapsVideoOutputTo1080p(t *testing.T) {
256280
session := &Session{
257281
ID: "item123",

internal/transcode/manager.go

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ type Session struct {
8888
PositionTicks int64
8989
Paused bool
9090
bufferPaused bool
91+
HardwarePipeline string
9192

9293
cancel context.CancelFunc
9394
process Process
@@ -102,10 +103,11 @@ type Runner interface {
102103
}
103104

104105
type Manager struct {
105-
mu sync.Mutex
106-
options Options
107-
sessions map[string]*Session
108-
media map[string]MediaInfo
106+
mu sync.Mutex
107+
options Options
108+
sessions map[string]*Session
109+
media map[string]MediaInfo
110+
vaapiEncodeFallback map[string]bool
109111
}
110112

111113
func NewManager(options Options) *Manager {
@@ -120,7 +122,7 @@ func NewManager(options Options) *Manager {
120122
Options: ffmpegOptions,
121123
}
122124
}
123-
return &Manager{options: options, sessions: map[string]*Session{}, media: map[string]MediaInfo{}}
125+
return &Manager{options: options, sessions: map[string]*Session{}, media: map[string]MediaInfo{}, vaapiEncodeFallback: map[string]bool{}}
124126
}
125127

126128
func NewManagerStrict(options Options) (*Manager, error) {
@@ -141,7 +143,7 @@ func NewManagerStrict(options Options) (*Manager, error) {
141143
Options: ffmpegOptions,
142144
}
143145
}
144-
return &Manager{options: options, sessions: map[string]*Session{}, media: map[string]MediaInfo{}}, nil
146+
return &Manager{options: options, sessions: map[string]*Session{}, media: map[string]MediaInfo{}, vaapiEncodeFallback: map[string]bool{}}, nil
145147
}
146148

147149
func normalizeManagerOptions(options Options) Options {
@@ -346,6 +348,10 @@ func (m *Manager) Ensure(id string, request Request) (*Session, error) {
346348
if existing, ok := m.sessions[id]; ok {
347349
if sessionProcessDone(existing) {
348350
delete(m.sessions, id)
351+
if shouldFallbackToVAAPIEncode(existing, m.options.Runner) {
352+
m.vaapiEncodeFallback[id] = true
353+
logging.Infof("hardware transcode fallback id=%s from=vaapi-full to=vaapi-encode reason=process_done", id)
354+
}
349355
stale = existing
350356
traceSwitch("manager_restart_done id=%s input=%s", id, redactURLString(existing.InputURL))
351357
} else if shouldRestart(existing, request) {
@@ -386,6 +392,12 @@ func (m *Manager) Ensure(id string, request Request) (*Session, error) {
386392
}
387393
ctx, cancel := context.WithCancel(context.Background())
388394
session := &Session{ID: id, Dir: dir, InputURL: request.InputURL, LastAccess: now, LastMediaAccess: now, cancel: cancel}
395+
if runner, ok := m.options.Runner.(FFmpegRunner); ok {
396+
session.HardwarePipeline = effectiveFFmpegOptions(session, runner.Options).HardwarePipeline
397+
}
398+
if m.vaapiEncodeFallback[id] {
399+
session.HardwarePipeline = "vaapi-encode"
400+
}
389401
session.SegmentTicks = m.segmentTicks()
390402
touchSession(session, request, now, true)
391403
if session.Media.IsZero() {
@@ -837,6 +849,20 @@ func sessionProcessDone(session *Session) bool {
837849
return ok && process.Done()
838850
}
839851

852+
func shouldFallbackToVAAPIEncode(session *Session, runner Runner) bool {
853+
if session == nil {
854+
return false
855+
}
856+
if strings.EqualFold(strings.TrimSpace(session.HardwarePipeline), "vaapi-encode") {
857+
return false
858+
}
859+
ffmpegRunner, ok := runner.(FFmpegRunner)
860+
if !ok {
861+
return false
862+
}
863+
return hardwarePipeline(ffmpegRunner.Options) == "vaapi-full"
864+
}
865+
840866
func formatTicks(ticks int64) string {
841867
if ticks <= 0 {
842868
return "0s"
@@ -1101,10 +1127,11 @@ func (r FFmpegRunner) Start(ctx context.Context, session *Session, request Reque
11011127
return nil, errors.New("ffmpeg path is required")
11021128
}
11031129

1104-
args := buildFFmpegArgs(session, request, r.Options)
1130+
options := effectiveFFmpegOptions(session, r.Options)
1131+
args := buildFFmpegArgs(session, request, options)
11051132
playlist := filepath.Join(session.Dir, "master.m3u8")
11061133
logPath := filepath.Join(session.Dir, "ffmpeg.log")
1107-
logging.Infof("transcode start id=%s segment=%d decode=%s audio_stream_index=%d audio_map=%s audio=optional-aac log=%s", session.ID, session.SegmentStartIndex, ffmpegOptionsSummary(r.Options), request.AudioStreamIndex, audioMapArg(session, request), logPath)
1134+
logging.Infof("transcode start id=%s segment=%d decode=%s audio_stream_index=%d audio_map=%s audio=optional-aac log=%s", session.ID, session.SegmentStartIndex, ffmpegOptionsSummary(options), request.AudioStreamIndex, audioMapArg(session, request), logPath)
11081135
logging.Debugf("ffmpeg start id=%s item=%s media_source=%s start_ticks=%d segment_start=%d path=%s input=%s playlist=%s media=%s args=%s", session.ID, session.ItemID, session.MediaSourceID, session.StartTimeTicks, session.SegmentStartIndex, r.Path, redactURLString(request.InputURL), playlist, session.Media.Summary(), redactFFmpegArgs(args))
11091136
cmd := exec.CommandContext(ctx, r.Path, args...)
11101137
stdin, err := cmd.StdinPipe()
@@ -1121,7 +1148,7 @@ func (r FFmpegRunner) Start(ctx context.Context, session *Session, request Reque
11211148
_ = logFile.Close()
11221149
return nil, fmt.Errorf("start ffmpeg: %w", err)
11231150
}
1124-
logging.Infof("ffmpeg started id=%s pid=%d decode=%s", session.ID, cmd.Process.Pid, ffmpegOptionsSummary(r.Options))
1151+
logging.Infof("ffmpeg started id=%s pid=%d decode=%s", session.ID, cmd.Process.Pid, ffmpegOptionsSummary(options))
11251152
process := &execProcess{cmd: cmd, logFile: logFile, stdin: stdin, doneCh: make(chan struct{})}
11261153
go func() {
11271154
err := cmd.Wait()
@@ -1144,6 +1171,16 @@ func (r FFmpegRunner) Start(ctx context.Context, session *Session, request Reque
11441171
return process, nil
11451172
}
11461173

1174+
func effectiveFFmpegOptions(session *Session, options FFmpegOptions) FFmpegOptions {
1175+
if session == nil {
1176+
return options
1177+
}
1178+
if pipeline := strings.TrimSpace(session.HardwarePipeline); pipeline != "" {
1179+
options.HardwarePipeline = pipeline
1180+
}
1181+
return options
1182+
}
1183+
11471184
func archiveFailedTranscodeLog(session *Session, logPath string) (string, error) {
11481185
if session == nil {
11491186
return "", errors.New("session is required")

0 commit comments

Comments
 (0)