diff --git a/Dockerfile.transcode b/Dockerfile.transcode index c14774e..ec3912d 100644 --- a/Dockerfile.transcode +++ b/Dockerfile.transcode @@ -9,6 +9,7 @@ RUN git clone https://github.com/m1k1o/go-transcode.git . \ COPY patches/go-transcode-vaapi.patch /tmp/go-transcode-vaapi.patch RUN git apply /tmp/go-transcode-vaapi.patch \ + && gofmt -w hlsvod/manager.go hlsvod/probe.go hlsvod/transcode.go \ && CGO_ENABLED=0 go build -o /go-transcode . # Keep Alpine 3.17: upstream documents an FFmpeg 6 segmenting regression with diff --git a/config.example.yaml b/config.example.yaml index 941bb91..7dd10c3 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -6,7 +6,8 @@ vod: transcode-dir: /tmp/go-transcode cache: true cache-dir: /cache - video-keyframes: true + # Avoid expensive full-file keyframe scans. FFmpeg forces HLS boundaries. + video-keyframes: false video-profiles: 480p: width: 854 diff --git a/docker-compose.yml b/docker-compose.yml index e41e023..ba98f93 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,13 +20,14 @@ services: - /dev/dri:/dev/dri environment: - TRANSCODE_VAAPI_DEVICE=/dev/dri/renderD128 + - LIBVA_DRIVER_NAME=i965 volumes: - /mnt/user/plex:/media:ro - ./transcode-config.yaml:/app/config.yaml:ro - - transcode-cache:/cache + - transcode-cache-v3:/cache ports: - "8090:8080" restart: unless-stopped volumes: - transcode-cache: + transcode-cache-v3: diff --git a/patches/go-transcode-vaapi.patch b/patches/go-transcode-vaapi.patch index 437595f..3705833 100644 --- a/patches/go-transcode-vaapi.patch +++ b/patches/go-transcode-vaapi.patch @@ -1,35 +1,88 @@ +diff --git a/hlsvod/manager.go b/hlsvod/manager.go +index 662c41d..51460f8 100644 +--- a/hlsvod/manager.go ++++ b/hlsvod/manager.go +@@ -378,12 +378,17 @@ func (m *ManagerCtx) waitForSegment(index int) (chan struct{}, bool) { + + func (m *ManagerCtx) transcodeSegments(offset, limit int) error { + logger := m.logger.With().Int("offset", offset).Int("limit", limit).Logger() ++ inputCodec := "" ++ if m.metadata.Video != nil { ++ inputCodec = m.metadata.Video.CodecName ++ } + + segmentTimes := m.breakpoints[offset : offset+limit+1] + logger.Info().Interface("segments-times", segmentTimes).Msg("transcoding segments") + + segments, err := TranscodeSegments(m.ctx, m.config.FFmpegBinary, TranscodeConfig{ + InputFilePath: m.config.MediaPath, ++ InputCodec: inputCodec, + OutputDirPath: m.config.TranscodeDir, + SegmentPrefix: m.config.SegmentPrefix, // This does not need to match. + +diff --git a/hlsvod/probe.go b/hlsvod/probe.go +index c39461c..58c3edd 100644 +--- a/hlsvod/probe.go ++++ b/hlsvod/probe.go +@@ -83,6 +83,7 @@ func ProbeMedia(ctx context.Context, ffprobeBinary string, inputFilePath string) + } + + data.Video = &ProbeVideoData{ ++ CodecName: stream.CodecName, + Width: stream.Width, + Height: stream.Height, + Duration: duration, +@@ -118,6 +119,7 @@ func ProbeMedia(ctx context.Context, ffprobeBinary string, inputFilePath string) + } + + type ProbeVideoData struct { ++ CodecName string + Width int + Height int + Duration time.Duration diff --git a/hlsvod/transcode.go b/hlsvod/transcode.go -index 1b04b00..12eb935 100644 +index 9bbe037..c0297fe 100644 --- a/hlsvod/transcode.go +++ b/hlsvod/transcode.go -@@ -6,5 +6,6 @@ import ( +@@ -5,6 +5,7 @@ import ( + "context" "fmt" "log" + "os" "os/exec" "path" "strings" -@@ -56,6 +57,16 @@ func TranscodeSegments(ctx context.Context, ffmpegBinary string, config Transcod +@@ -13,6 +14,7 @@ import ( + + type TranscodeConfig struct { + InputFilePath string // Transcoded video input. ++ InputCodec string // ffprobe codec name, e.g. h264 or hevc. + OutputDirPath string // Segments output path. + SegmentPrefix string // e.g. prefix-000001.ts + SegmentOffset int // Start segment number. +@@ -69,6 +71,21 @@ func TranscodeSegments(ctx context.Context, ffmpegBinary string, config Transcod }...) } -+ // Decode and filter on the GPU when a VAAPI render device is configured. -+ // This is opt-in so upstream's software behavior remains available. ++ // Haswell can decode H.264 in hardware, but not HEVC Main 10. Use full ++ // hardware transcoding for H.264 and software decode plus VAAPI upload for ++ // other codecs. Scaling and H.264 encoding always remain on the GPU. + vaapiDevice := os.Getenv("TRANSCODE_VAAPI_DEVICE") ++ hardwareDecode := vaapiDevice != "" && config.InputCodec == "h264" + if vaapiDevice != "" { + args = append(args, []string{ + "-init_hw_device", "vaapi=va:" + vaapiDevice, -+ "-filter_hw_device", "va", "-hwaccel", "vaapi", "-hwaccel_output_format", "vaapi", ++ "-filter_hw_device", "va", + }...) ++ if hardwareDecode { ++ args = append(args, "-hwaccel", "vaapi", "-hwaccel_output_format", "vaapi") ++ } + } + // Input specs args = append(args, []string{ "-i", config.InputFilePath, // Input file -@@ -79,17 +89,26 @@ func TranscodeSegments(ctx context.Context, ffmpegBinary string, config Transcod - if profile.Width >= profile.Height { - scale = fmt.Sprintf("scale=-2:%d", profile.Height) - } else { +@@ -89,14 +106,26 @@ func TranscodeSegments(ctx context.Context, ffmpegBinary string, config Transcod scale = fmt.Sprintf("scale=%d:-2", profile.Width) } @@ -47,6 +100,9 @@ index 1b04b00..12eb935 100644 + } else { + scale = fmt.Sprintf("scale_vaapi=w=%d:h=-2:format=nv12", profile.Width) + } ++ if !hardwareDecode { ++ scale = "format=nv12,hwupload," + scale ++ } + args = append(args, []string{ + "-vf", scale, "-c:v", "h264_vaapi", "-profile:v", "high", + "-level:v", "4.0", "-b:v", fmt.Sprintf("%dk", profile.Bitrate), diff --git a/transcode-config.yaml b/transcode-config.yaml index 3737f33..c53709e 100644 --- a/transcode-config.yaml +++ b/transcode-config.yaml @@ -4,7 +4,9 @@ vod: media-dir: /media cache: true cache-dir: /cache - video-keyframes: true + # Full-file keyframe probing is expensive and can be OOM-killed on Unraid. + # FFmpeg will force segment-boundary keyframes during transcoding instead. + video-keyframes: false video-profiles: 1080p: