Skip to content

Commit 51daebf

Browse files
KillerXclaude
andcommitted
Normalize reserved color_trc in Preview transcoder
Same swscaler failure that broke VideoH264 (7ec46fe) also hit the lowres preview transcoder on ProRes inputs with reserved/unknown color_transfer ("Operation not supported", exit 161). Insert the setparams=color_trc=bt709 filter ahead of scale in both Preview filter graphs — the video-only branch and the video+audio branch via buildVUMeterFilters. Collapse the two NormalizeColorTRCFilter variants into a single helper that takes a StreamInfo and picks the first video stream internally, so VideoH264 and Preview share one code path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1e8ea08 commit 51daebf

6 files changed

Lines changed: 38 additions & 16 deletions

File tree

potential_improvements.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Potential improvements
2+
3+
- `services/transcode/preview.go::GrowingPreview` has the same `trc:reserved` swscaler-failure exposure that was just fixed in `Preview` and `VideoH264`. The function reads from a pipe (`pipe:0`) so it cannot probe upfront. Options to consider: probe `input.FilePath` once before starting (race-y while the file is still growing), unconditionally insert `setparams=color_trc=bt709` since the workflow is SDR-only, or accept the trc explicitly from the caller.

services/ffmpeg/probe.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,19 @@ type FFProbeResult struct {
102102
} `json:"format"`
103103
}
104104

105-
// NormalizeColorTRCFilter returns a setparams filter clause when the source
106-
// stream's color_transfer is unset/unknown/reserved, or "" when the existing
107-
// tag is already valid.
105+
// NormalizeColorTRCFilter returns a setparams filter clause when the first
106+
// video stream's color_transfer is unset/unknown/reserved, or "" when the
107+
// existing tag is already valid (or there is no video stream).
108108
//
109109
// Why: ProRes files written with reserved/unknown trc make swscaler refuse
110110
// pix_fmt conversions ("Operation not supported"), which breaks the
111-
// scale->libx264 pipeline in VideoH264. bt709 is a safe SDR default for the
112-
// content this repo handles.
113-
func NormalizeColorTRCFilter(s FFProbeStream) string {
114-
switch strings.ToLower(strings.TrimSpace(s.ColorTransfer)) {
111+
// scale->libx264 pipeline in VideoH264 and the lowres preview transcoder.
112+
// bt709 is a safe SDR default for the content this repo handles.
113+
func NormalizeColorTRCFilter(info StreamInfo) string {
114+
if len(info.VideoStreams) == 0 {
115+
return ""
116+
}
117+
switch strings.ToLower(strings.TrimSpace(info.VideoStreams[0].ColorTransfer)) {
115118
case "", "unknown", "reserved":
116119
return "setparams=color_trc=bt709"
117120
}

services/ffmpeg/probe_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,13 @@ func TestNormalizeColorTRCFilter(t *testing.T) {
2525

2626
for _, tc := range cases {
2727
t.Run(tc.name, func(t *testing.T) {
28-
got := NormalizeColorTRCFilter(FFProbeStream{ColorTransfer: tc.trc})
28+
info := StreamInfo{VideoStreams: []FFProbeStream{{ColorTransfer: tc.trc}}}
29+
got := NormalizeColorTRCFilter(info)
2930
assert.Equal(t, tc.expected, got)
3031
})
3132
}
33+
34+
t.Run("no video stream returns empty", func(t *testing.T) {
35+
assert.Equal(t, "", NormalizeColorTRCFilter(StreamInfo{}))
36+
})
3237
}

services/transcode/preview.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ type audioPreviewData struct {
4545
}
4646

4747
// buildVUMeterFilters generates ffmpeg filter steps for compact VU meters for each audio track.
48-
func buildVUMeterFilters(audioTracks int) (string, string) {
48+
func buildVUMeterFilters(audioTracks int, trcPrefix string) (string, string) {
4949
meterW := 200
5050
meterH := 20
5151
meterAlpha := 0.5
5252
spacing := 10
53-
parts := []string{"[0:v]scale=1280:720[vmain]"}
53+
parts := []string{fmt.Sprintf("[0:v]%sscale=1280:720[vmain]", trcPrefix)}
5454
lastVid := "[vmain]"
5555
for i := 0; i < audioTracks; i++ {
5656
y := 10 + i*(meterH+spacing)
@@ -201,6 +201,11 @@ func Preview(input PreviewInput, progressCallback ffmpeg.ProgressCallback) (*Pre
201201
}
202202
}
203203

204+
var trcPrefix string
205+
if trcFix := ffmpeg.NormalizeColorTRCFilter(ffmpeg.ProbeResultToInfo(info)); trcFix != "" {
206+
trcPrefix = trcFix + ","
207+
}
208+
204209
filename := filepath.Base(input.FilePath)[:len(filepath.Base(input.FilePath))-len(filepath.Ext(input.FilePath))]
205210
if hasVideo {
206211
filename += "_lowres.mp4"
@@ -225,7 +230,7 @@ func Preview(input PreviewInput, progressCallback ffmpeg.ProgressCallback) (*Pre
225230
"-i", input.FilePath,
226231
"-ss", "0.0",
227232
"-i", watermark,
228-
"-filter_complex", "sws_flags=bicubic;[0:v]split=1[VIDEO-main-.mp4];[VIDEO-main-.mp4]scale=-2:540,null[temp];[temp][1:v]overlay=0:0:eof_action=repeat[VIDEO-.mp4]",
233+
"-filter_complex", fmt.Sprintf("sws_flags=bicubic;[0:v]%ssplit=1[VIDEO-main-.mp4];[VIDEO-main-.mp4]scale=-2:540,null[temp];[temp][1:v]overlay=0:0:eof_action=repeat[VIDEO-.mp4]", trcPrefix),
229234
"-map", "[VIDEO-.mp4]",
230235
"-c:v", encoder,
231236
)
@@ -238,7 +243,7 @@ func Preview(input PreviewInput, progressCallback ffmpeg.ProgressCallback) (*Pre
238243
"-ss", "0.0",
239244
"-i", watermark,
240245
)
241-
vuFilters, lastVid := buildVUMeterFilters(audioTracks)
246+
vuFilters, lastVid := buildVUMeterFilters(audioTracks, trcPrefix)
242247
// Compose filter graph: scale, vumeters, watermark, stereo audio
243248
var audioFilter string
244249
if audioTracks == 1 {

services/transcode/preview_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ func Test_AudioAdioPreviewGenerator(t *testing.T) {
6363

6464
}
6565

66+
func TestBuildVUMeterFilters_TRCPrefix(t *testing.T) {
67+
head, _ := buildVUMeterFilters(2, "setparams=color_trc=bt709,")
68+
assert.Contains(t, head, "[0:v]setparams=color_trc=bt709,scale=1280:720[vmain]")
69+
70+
head2, _ := buildVUMeterFilters(2, "")
71+
assert.Contains(t, head2, "[0:v]scale=1280:720[vmain]")
72+
assert.NotContains(t, head2, "setparams")
73+
}
74+
6675
func TestPreview_VUMeters_MultipleAudioTracks(t *testing.T) {
6776
t.Parallel()
6877
trackCounts := []int{1, 2, 4, 16}

services/transcode/video.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,7 @@ func VideoH264(input common.VideoInput, cb ffmpeg.ProgressCallback) (*common.Vid
5454

5555
var filterComplex string
5656

57-
var trcFix string
58-
if len(info.VideoStreams) > 0 {
59-
trcFix = ffmpeg.NormalizeColorTRCFilter(info.VideoStreams[0])
60-
}
57+
trcFix := ffmpeg.NormalizeColorTRCFilter(info)
6158

6259
switch {
6360
case input.WatermarkPath != nil && trcFix != "":

0 commit comments

Comments
 (0)