From fec6a2ccb78435186d1aa785822f84e252d5a0f9 Mon Sep 17 00:00:00 2001 From: Joss Gray Date: Fri, 10 Jul 2026 22:20:11 -0400 Subject: [PATCH 1/7] Add NotchLC decoder support NotchLC video is carried in native .mov files with FourCC 'nclc', which macOS demuxes but can't decode. Register the codec with the videodecoder extension and route it to FFmpeg's built-in notchlc decoder, following the existing native-container codec pattern (FLIC, HAP, etc.). No FFmpeg reconfigure needed since all decoders are compiled in by default. Co-Authored-By: Claude Opus 4.8 (1M context) --- videodecoder/Info.plist | 6 ++++++ videodecoder/videodecoder.swift | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/videodecoder/Info.plist b/videodecoder/Info.plist index 0747c39..3012690 100644 --- a/videodecoder/Info.plist +++ b/videodecoder/Info.plist @@ -111,6 +111,12 @@ CodecType AFLC + + CodecName + NotchLC + CodecType + nclc + CodecName Indeo 2 diff --git a/videodecoder/videodecoder.swift b/videodecoder/videodecoder.swift index 7f722ae..33823e1 100644 --- a/videodecoder/videodecoder.swift +++ b/videodecoder/videodecoder.swift @@ -42,6 +42,7 @@ class VideoDecoder: NSObject, MEVideoDecoder { 0x4458_4433: AV_CODEC_ID_DXV, // 'DXD3' 0x666C_6963: AV_CODEC_ID_FLIC, // 'flic' 0x4146_4C43: AV_CODEC_ID_FLIC, // 'AFLC' + 0x6e63_6c63: AV_CODEC_ID_NOTCHLC, // 'nclc' 0x5254_3231: AV_CODEC_ID_INDEO2, // 'RT21' 0x4956_3331: AV_CODEC_ID_INDEO3, // 'IV31' 0x4956_3332: AV_CODEC_ID_INDEO3, // 'IV32' @@ -153,7 +154,8 @@ class VideoDecoder: NSObject, MEVideoDecoder { switch codecID { // RGB - case AV_CODEC_ID_QTRLE, AV_CODEC_ID_RPZA, AV_CODEC_ID_CINEPAK, AV_CODEC_ID_HAP, AV_CODEC_ID_DXV, AV_CODEC_ID_FLIC: + case AV_CODEC_ID_QTRLE, AV_CODEC_ID_RPZA, AV_CODEC_ID_CINEPAK, AV_CODEC_ID_HAP, AV_CODEC_ID_DXV, AV_CODEC_ID_FLIC, + AV_CODEC_ID_NOTCHLC: // Pixel format is set from codec_tag and/or bits_per_coded_sample in codec _init() under avcodec_open2() params.pointee.color_range = AVCOL_RANGE_JPEG params.pointee.color_space = AVCOL_SPC_RGB From 57ee1fce6336d58885452a0ba10441e1f391125c Mon Sep 17 00:00:00 2001 From: Joss Gray Date: Fri, 10 Jul 2026 22:41:17 -0400 Subject: [PATCH 2/7] Give NotchLC its own decoder-setup case Unlike the RGB-group codecs it was grouped with, NotchLC's decode_init() ignores codec_tag and bits_per_coded_sample and unconditionally sets pix_fmt and full color info, so the shared color_range/color_space assignment (and its comment) don't apply. Split it into a standalone no-op case with an accurate comment. Co-Authored-By: Claude Opus 4.8 (1M context) --- videodecoder/videodecoder.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/videodecoder/videodecoder.swift b/videodecoder/videodecoder.swift index 33823e1..e183236 100644 --- a/videodecoder/videodecoder.swift +++ b/videodecoder/videodecoder.swift @@ -154,12 +154,15 @@ class VideoDecoder: NSObject, MEVideoDecoder { switch codecID { // RGB - case AV_CODEC_ID_QTRLE, AV_CODEC_ID_RPZA, AV_CODEC_ID_CINEPAK, AV_CODEC_ID_HAP, AV_CODEC_ID_DXV, AV_CODEC_ID_FLIC, - AV_CODEC_ID_NOTCHLC: + case AV_CODEC_ID_QTRLE, AV_CODEC_ID_RPZA, AV_CODEC_ID_CINEPAK, AV_CODEC_ID_HAP, AV_CODEC_ID_DXV, AV_CODEC_ID_FLIC: // Pixel format is set from codec_tag and/or bits_per_coded_sample in codec _init() under avcodec_open2() params.pointee.color_range = AVCOL_RANGE_JPEG params.pointee.color_space = AVCOL_SPC_RGB + case AV_CODEC_ID_NOTCHLC: + // Nothing to set: notchlc's decode_init() unconditionally sets pix_fmt (YUVA444P12) and full color info under avcodec_open2() + break + // YUV case AV_CODEC_ID_AIC: params.pointee.format = AV_PIX_FMT_YUV420P.rawValue From cb18a664572433f9bd04ae6c6a627ad1c1c3936c Mon Sep 17 00:00:00 2001 From: Joss Gray Date: Sat, 11 Jul 2026 16:29:49 -0400 Subject: [PATCH 3/7] NotchLC: relabel yuva444p12le output as gbrap12le so frames render ffmpeg's NotchLC decoder outputs a YUV-family planar format tagged with the RGB/identity matrix, which zscale/zimg rejects ("YUV color family cannot have RGB matrix coefficients"), dropping every frame to black. Relabel to the memory-identical GBR-planar format so it takes an RGB conversion path. INCOMPLETE: colour is still wrong (green cast). Co-Authored-By: Claude Opus 4.8 (1M context) --- videodecoder/videodecoder.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/videodecoder/videodecoder.swift b/videodecoder/videodecoder.swift index e183236..b2191ba 100644 --- a/videodecoder/videodecoder.swift +++ b/videodecoder/videodecoder.swift @@ -531,6 +531,13 @@ class VideoDecoder: NSObject, MEVideoDecoder { // RGB space. Set color info in case we somehow end up on the zscale path. if frame.pointee.colorspace == AVCOL_SPC_RGB { + // NotchLC decodes to a YUV-family planar format (yuva444p12le) but tags it with the + // RGB/identity matrix. Relabel to the memory-identical GBR-planar format so it takes an + // RGB conversion path instead of zscale, which rejects "YUV family + RGB matrix" (black). + // ponytail: NotchLC only ever emits yuva444p12le, so that's the only mapping needed. + if frame.pointee.format == AV_PIX_FMT_YUVA444P12LE.rawValue { + frame.pointee.format = AV_PIX_FMT_GBRAP12LE.rawValue + } frame.pointee.color_range = AVCOL_RANGE_JPEG frame.pointee.color_primaries = AVCOL_PRI_BT709 frame.pointee.color_trc = AVCOL_TRC_IEC61966_2_1 From f8337417d4f3bbe645a428305ae4d371fc360f97 Mon Sep 17 00:00:00 2001 From: Joss Gray Date: Sat, 11 Jul 2026 22:38:44 -0400 Subject: [PATCH 4/7] NotchLC: fix colour by tagging BT.709 matrix instead of relabeling to GBR FFmpeg's notchlc decoder emits full-range BT.709 YUV (yuva444p12le) but mislabels the matrix as RGB/identity (AVCOL_SPC_RGB). The previous commit worked around zscale's "YUV family + RGB matrix" rejection by relabeling the planes as the memory-identical gbrap12le. That routed Y->Green, Cb->Blue, Cr->Red with no matrix applied, scrambling the image (green/magenta cast). Instead, correct the matrix tag to AVCOL_SPC_BT709 and leave the format as yuv. The frame then takes the normal YUV->RGB path (zscale), which converts full-range 709 correctly. Verified against Big Buck Bunny (known reference) and a brand-logo sample: both render with natural colour. Still guarded to yuva444p12le so genuine RGB/GBR codecs (QTRLE, HAP, DXV, FLIC) keep taking the RGB path. Co-Authored-By: Claude Opus 4.8 (1M context) --- videodecoder/videodecoder.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/videodecoder/videodecoder.swift b/videodecoder/videodecoder.swift index b2191ba..2020856 100644 --- a/videodecoder/videodecoder.swift +++ b/videodecoder/videodecoder.swift @@ -531,12 +531,12 @@ class VideoDecoder: NSObject, MEVideoDecoder { // RGB space. Set color info in case we somehow end up on the zscale path. if frame.pointee.colorspace == AVCOL_SPC_RGB { - // NotchLC decodes to a YUV-family planar format (yuva444p12le) but tags it with the - // RGB/identity matrix. Relabel to the memory-identical GBR-planar format so it takes an - // RGB conversion path instead of zscale, which rejects "YUV family + RGB matrix" (black). - // ponytail: NotchLC only ever emits yuva444p12le, so that's the only mapping needed. + // NotchLC decodes to full-range BT.709 YUV (yuva444p12le) but FFmpeg's decoder mislabels + // the matrix as RGB/identity. Correct the matrix tag so the frame takes the YUV conversion + // path; leaving it as RGB either scrambles channels (GBR relabel) or makes zscale reject + // "YUV family + RGB matrix" (black). if frame.pointee.format == AV_PIX_FMT_YUVA444P12LE.rawValue { - frame.pointee.format = AV_PIX_FMT_GBRAP12LE.rawValue + frame.pointee.colorspace = AVCOL_SPC_BT709 } frame.pointee.color_range = AVCOL_RANGE_JPEG frame.pointee.color_primaries = AVCOL_PRI_BT709 From 41347f45cb31a2d93e2727e3a3e64ccce473428d Mon Sep 17 00:00:00 2001 From: Joss Gray Date: Sun, 12 Jul 2026 10:00:12 -0400 Subject: [PATCH 5/7] NotchLC: Move NotchLC case into YUV section and set params for consistency with other cases. --- videodecoder/videodecoder.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/videodecoder/videodecoder.swift b/videodecoder/videodecoder.swift index 2020856..c5dfc78 100644 --- a/videodecoder/videodecoder.swift +++ b/videodecoder/videodecoder.swift @@ -159,10 +159,6 @@ class VideoDecoder: NSObject, MEVideoDecoder { params.pointee.color_range = AVCOL_RANGE_JPEG params.pointee.color_space = AVCOL_SPC_RGB - case AV_CODEC_ID_NOTCHLC: - // Nothing to set: notchlc's decode_init() unconditionally sets pix_fmt (YUVA444P12) and full color info under avcodec_open2() - break - // YUV case AV_CODEC_ID_AIC: params.pointee.format = AV_PIX_FMT_YUV420P.rawValue @@ -232,6 +228,14 @@ class VideoDecoder: NSObject, MEVideoDecoder { } } } + case AV_CODEC_ID_NOTCHLC: + // NotchLC codec overwrites these with the same values in decode_init, but for consistency with + // other cases we set them here. + params.pointee.format = AV_PIX_FMT_YUVA444P.rawValue; + params.pointee.color_range = AVCOL_RANGE_JPEG + params.pointee.color_space = AVCOL_SPC_RGB + break + default: // Shouldn't get here logger.error( From 73c215a700340283aa61be475675eae9948606a3 Mon Sep 17 00:00:00 2001 From: Joss Gray Date: Sun, 12 Jul 2026 10:01:04 -0400 Subject: [PATCH 6/7] NotchLC: Add comment about incorrect colorspace --- videodecoder/videodecoder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/videodecoder/videodecoder.swift b/videodecoder/videodecoder.swift index c5dfc78..6e9a2a7 100644 --- a/videodecoder/videodecoder.swift +++ b/videodecoder/videodecoder.swift @@ -233,7 +233,7 @@ class VideoDecoder: NSObject, MEVideoDecoder { // other cases we set them here. params.pointee.format = AV_PIX_FMT_YUVA444P.rawValue; params.pointee.color_range = AVCOL_RANGE_JPEG - params.pointee.color_space = AVCOL_SPC_RGB + params.pointee.color_space = AVCOL_SPC_RGB // Note, should be BT709 but ffmpeg mislabels... break default: From 0ffc79bd11965cbb1c0f2f70654a7c3187c17a33 Mon Sep 17 00:00:00 2001 From: Joss Gray Date: Sun, 12 Jul 2026 10:12:57 -0400 Subject: [PATCH 7/7] NotchLC: Put frame colorspace fixup behing codec gate instead of pixel format --- videodecoder/videodecoder.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/videodecoder/videodecoder.swift b/videodecoder/videodecoder.swift index 6e9a2a7..d3596e6 100644 --- a/videodecoder/videodecoder.swift +++ b/videodecoder/videodecoder.swift @@ -454,7 +454,7 @@ class VideoDecoder: NSObject, MEVideoDecoder { } // Fix up color info on the decoded frame - VideoDecoder.fixupColors(frame: frame!) + fixupColors(frame: frame!) var pixelBuffer: CVPixelBuffer do { @@ -531,15 +531,12 @@ class VideoDecoder: NSObject, MEVideoDecoder { // Infer color info from the decoded frame. Make educated guesses for unspecified values. // Mutates the frame's color_primaries, color_trc and colorspace fields in place. - class func fixupColors(frame: UnsafeMutablePointer) { + func fixupColors(frame: UnsafeMutablePointer) { // RGB space. Set color info in case we somehow end up on the zscale path. if frame.pointee.colorspace == AVCOL_SPC_RGB { - // NotchLC decodes to full-range BT.709 YUV (yuva444p12le) but FFmpeg's decoder mislabels - // the matrix as RGB/identity. Correct the matrix tag so the frame takes the YUV conversion - // path; leaving it as RGB either scrambles channels (GBR relabel) or makes zscale reject - // "YUV family + RGB matrix" (black). - if frame.pointee.format == AV_PIX_FMT_YUVA444P12LE.rawValue { + // FFmpeg decoder mislabels notchlc as RGB. + if self.dec_ctx?.pointee.codec_id == AV_CODEC_ID_NOTCHLC { frame.pointee.colorspace = AVCOL_SPC_BT709 } frame.pointee.color_range = AVCOL_RANGE_JPEG