From ddf29672fd5aad970a2c832fc62487eedda378a6 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Sun, 4 Jan 2026 08:51:13 +0100 Subject: [PATCH 1/5] Fix MXF files containing CEA-708 captions not being detected/extracted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: CCX_RAW_TYPE data from MXF demuxer was not being passed to the DTVCC decoder, only to the legacy 608 decoder via process_raw_with_field. Changes: - general_loop.c: Changed CCX_RAW_TYPE handling to use process_cc_data instead of process_raw_with_field to properly invoke DTVCC decoder - general_loop.c: Added DTVCC activation for MXF/GXF sources since they may contain 708 captions - general_loop.c: Initialize timing from caption PTS when not set - ccx_dtvcc.h: Added ccxr_dtvcc_set_active FFI declaration - lib.rs: Added ccxr_dtvcc_set_active function to enable DTVCC decoder - decoder/mod.rs: Fixed flush logic to always process visible windows - ccx_demuxer_mxf.c: Fixed PTS calculation to use 90kHz units based on edit_rate, and changed verbose logging to debug() Fixes #1647 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/lib_ccx/ccx_demuxer_mxf.c | 82 +++++++++++++++++++++++++++++++++-- src/lib_ccx/ccx_dtvcc.h | 1 + src/lib_ccx/general_loop.c | 30 ++++++++++++- src/rust/src/decoder/mod.rs | 9 +++- src/rust/src/lib.rs | 15 +++++++ 5 files changed, 130 insertions(+), 7 deletions(-) diff --git a/src/lib_ccx/ccx_demuxer_mxf.c b/src/lib_ccx/ccx_demuxer_mxf.c index 07326d328..35856e86b 100644 --- a/src/lib_ccx/ccx_demuxer_mxf.c +++ b/src/lib_ccx/ccx_demuxer_mxf.c @@ -75,12 +75,15 @@ enum MXFLocalTag void update_tid_lut(struct MXFContext *ctx, uint32_t track_id, uint8_t *track_number, struct ccx_rational edit_rate) { int i; + debug("update_tid_lut: track_id=%u (0x%x), track_number=%02X%02X%02X%02X, cap_track_id=%u\n", + track_id, track_id, track_number[0], track_number[1], track_number[2], track_number[3], ctx->cap_track_id); // Update essence element key if we have track Id of caption if (ctx->cap_track_id == track_id) { memcpy(ctx->cap_essence_key, mxf_essence_element_key, 12); memcpy(ctx->cap_essence_key + 12, track_number, 4); ctx->edit_rate = edit_rate; + debug("MXF: Found caption track, track_id=%u\n", track_id); } for (i = 0; i < ctx->nb_tracks; i++) @@ -248,6 +251,7 @@ static int mxf_read_vanc_vbi_desc(struct ccx_demuxer *demux, uint64_t size) { case MXF_TAG_LTRACK_ID: ctx->cap_track_id = buffered_get_be32(demux); + debug("MXF: VANC/VBI descriptor found, Linked Track ID = %u\n", ctx->cap_track_id); update_cap_essence_key(ctx, ctx->cap_track_id); break; default: @@ -304,6 +308,17 @@ static int mxf_read_cdp_data(struct ccx_demuxer *demux, int size, struct demuxer log("Incomplete CDP packet\n"); ret = buffered_read(demux, data->buffer + data->len, cc_count * 3); + // Log first few bytes of cc_data for debugging + if (cc_count > 0) + { + unsigned char *cc_ptr = data->buffer + data->len; + debug("cc_data (first 6 triplets): "); + for (int j = 0; j < (cc_count < 6 ? cc_count : 6); j++) + { + debug("%02X%02X%02X ", cc_ptr[j * 3], cc_ptr[j * 3 + 1], cc_ptr[j * 3 + 2]); + } + debug("\n"); + } data->len += cc_count * 3; demux->past += cc_count * 3; len += ret; @@ -361,7 +376,10 @@ static int mxf_read_vanc_data(struct ccx_demuxer *demux, uint64_t size, struct d // uint8_t count; /* Currently unused */ if (size < 19) + { + debug("VANC data too small: %" PRIu64 " < 19\n", size); goto error; + } ret = buffered_read(demux, vanc_header, 16); @@ -370,31 +388,39 @@ static int mxf_read_vanc_data(struct ccx_demuxer *demux, uint64_t size, struct d return CCX_EOF; len += ret; + debug("VANC header: num_packets=%d, line=0x%02x%02x, wrap_type=0x%02x, sample_config=0x%02x\n", + vanc_header[1], vanc_header[2], vanc_header[3], vanc_header[4], vanc_header[5]); + for (int i = 0; i < vanc_header[1]; i++) { DID = buffered_get_byte(demux); len++; + debug("VANC packet %d: DID=0x%02x\n", i, DID); if (!(DID == 0x61 || DID == 0x80)) { + debug("DID 0x%02x not recognized as caption DID\n", DID); goto error; } SDID = buffered_get_byte(demux); len++; + debug("VANC packet %d: SDID=0x%02x\n", i, SDID); if (SDID == 0x01) debug("Caption Type 708\n"); else if (SDID == 0x02) debug("Caption Type 608\n"); cdp_size = buffered_get_byte(demux); + debug("VANC packet %d: cdp_size=%d\n", i, cdp_size); if (cdp_size + 19 > size) { - debug("Incomplete cdp(%d) in anc data(%d)\n", cdp_size, size); + log("Incomplete cdp(%d) in anc data(%" PRIu64 ")\n", cdp_size, size); goto error; } len++; ret = mxf_read_cdp_data(demux, cdp_size, data); + debug("mxf_read_cdp_data returned %d, data->len=%d\n", ret, data->len); len += ret; // len += (3 + count + 4); } @@ -411,15 +437,33 @@ static int mxf_read_essence_element(struct ccx_demuxer *demux, uint64_t size, st int ret; struct MXFContext *ctx = demux->private_data; + debug("mxf_read_essence_element: ctx->type=%d (ANC=%d, VBI=%d), size=%" PRIu64 "\n", + ctx->type, MXF_CT_ANC, MXF_CT_VBI, size); + if (ctx->type == MXF_CT_ANC) { data->bufferdatatype = CCX_RAW_TYPE; ret = mxf_read_vanc_data(demux, size, data); - data->pts = ctx->cap_count; + debug("mxf_read_vanc_data returned %d, data->len=%d\n", ret, data->len); + // Calculate PTS in 90kHz units from frame count and edit rate + // edit_rate is frames per second (e.g., 25/1 for 25fps) + // PTS = frame_count * 90000 / fps = frame_count * 90000 * edit_rate.den / edit_rate.num + if (ctx->edit_rate.num > 0 && ctx->edit_rate.den > 0) + { + data->pts = (int64_t)ctx->cap_count * 90000 * ctx->edit_rate.den / ctx->edit_rate.num; + } + else + { + // Fallback to 25fps if edit_rate not set + data->pts = (int64_t)ctx->cap_count * 90000 / 25; + } + debug("Frame %d, PTS=%" PRId64 " (edit_rate=%d/%d)\n", + ctx->cap_count, data->pts, ctx->edit_rate.num, ctx->edit_rate.den); ctx->cap_count++; } else { + debug("Skipping essence element (not ANC type)\n"); ret = buffered_skip(demux, size); demux->past += ret; } @@ -514,6 +558,7 @@ static int read_packet(struct ccx_demuxer *demux, struct demuxer_data *data) KLVPacket klv; const MXFReadTableEntry *reader; struct MXFContext *ctx = demux->private_data; + static int first_essence_logged = 0; while ((ret = klv_read_packet(&klv, demux)) == 0) { debug("Key %02X%02X%02X%02X%02X%02X%02X%02X.%02X%02X%02X%02X%02X%02X%02X%02X size %" PRIu64 "\n", @@ -523,8 +568,25 @@ static int read_packet(struct ccx_demuxer *demux, struct demuxer_data *data) klv.key[12], klv.key[13], klv.key[14], klv.key[15], klv.length); + // Check if this is an essence element key (first 12 bytes match) + if (IS_KLV_KEY(klv.key, mxf_essence_element_key) && !first_essence_logged) + { + debug("MXF: First essence element key: %02X%02X%02X%02X%02X%02X%02X%02X.%02X%02X%02X%02X%02X%02X%02X%02X\n", + klv.key[0], klv.key[1], klv.key[2], klv.key[3], + klv.key[4], klv.key[5], klv.key[6], klv.key[7], + klv.key[8], klv.key[9], klv.key[10], klv.key[11], + klv.key[12], klv.key[13], klv.key[14], klv.key[15]); + debug("MXF: cap_essence_key: %02X%02X%02X%02X%02X%02X%02X%02X.%02X%02X%02X%02X%02X%02X%02X%02X\n", + ctx->cap_essence_key[0], ctx->cap_essence_key[1], ctx->cap_essence_key[2], ctx->cap_essence_key[3], + ctx->cap_essence_key[4], ctx->cap_essence_key[5], ctx->cap_essence_key[6], ctx->cap_essence_key[7], + ctx->cap_essence_key[8], ctx->cap_essence_key[9], ctx->cap_essence_key[10], ctx->cap_essence_key[11], + ctx->cap_essence_key[12], ctx->cap_essence_key[13], ctx->cap_essence_key[14], ctx->cap_essence_key[15]); + first_essence_logged = 1; + } + if (IS_KLV_KEY(klv.key, ctx->cap_essence_key)) { + debug("MXF: Found ANC essence element, size=%" PRIu64 "\n", klv.length); mxf_read_essence_element(demux, klv.length, data); if (data->len > 0) break; @@ -566,8 +628,15 @@ int ccx_mxf_getmoredata(struct lib_ccx_ctx *ctx, struct demuxer_data **ppdata) data->program_number = 1; data->stream_pid = 1; data->codec = CCX_CODEC_ATSC_CC; - data->tb.num = 1001; - data->tb.den = 30000; + // PTS is already calculated in 90kHz units by mxf_read_essence_element + data->tb.num = 1; + data->tb.den = 90000; + + // Enable CEA-708 (DTVCC) decoder for MXF files with VANC captions + if (ctx->dec_global_setting && ctx->dec_global_setting->settings_dtvcc) + { + ctx->dec_global_setting->settings_dtvcc->enabled = 1; + } } else { @@ -576,6 +645,11 @@ int ccx_mxf_getmoredata(struct lib_ccx_ctx *ctx, struct demuxer_data **ppdata) ret = read_packet(ctx->demux_ctx, data); + // Ensure timebase is 90kHz since PTS is calculated in 90kHz units + // CDP parsing may have set a frame-based timebase which would cause incorrect conversion + data->tb.num = 1; + data->tb.den = 90000; + return ret; } diff --git a/src/lib_ccx/ccx_dtvcc.h b/src/lib_ccx/ccx_dtvcc.h index 446064a1d..4f9255550 100644 --- a/src/lib_ccx/ccx_dtvcc.h +++ b/src/lib_ccx/ccx_dtvcc.h @@ -17,6 +17,7 @@ extern void ccxr_dtvcc_free(void *dtvcc_rust); extern void ccxr_dtvcc_process_data(void *dtvcc_rust, const unsigned char cc_valid, const unsigned char cc_type, const unsigned char data1, const unsigned char data2); extern int ccxr_dtvcc_is_active(void *dtvcc_rust); +extern void ccxr_dtvcc_set_active(void *dtvcc_rust, int active); #endif #endif // CCEXTRACTOR_CCX_DTVCC_H diff --git a/src/lib_ccx/general_loop.c b/src/lib_ccx/general_loop.c index 40730063d..3a26f5b17 100644 --- a/src/lib_ccx/general_loop.c +++ b/src/lib_ccx/general_loop.c @@ -18,6 +18,7 @@ #include "ccx_gxf.h" #include "dvd_subtitle_decoder.h" #include "ccx_demuxer_mxf.h" +#include "ccx_dtvcc.h" int end_of_file = 0; // End of file? @@ -861,7 +862,34 @@ int process_data(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, str } else if (data_node->bufferdatatype == CCX_RAW_TYPE) { - got = process_raw_with_field(dec_ctx, dec_sub, data_node->buffer, data_node->len); + // CCX_RAW_TYPE contains cc_data triplets (cc_type + 2 data bytes each) + // Used by MXF and GXF demuxers + + // Initialize timing if not set (use caption PTS as reference) + if (dec_ctx->timing->pts_set == 0 && data_node->pts != CCX_NOPTS) + { + dec_ctx->timing->min_pts = data_node->pts; + dec_ctx->timing->pts_set = 2; // MinPtsSet + dec_ctx->timing->sync_pts = data_node->pts; + set_fts(dec_ctx->timing); + } + +#ifndef DISABLE_RUST + // Enable DTVCC decoder for CEA-708 captions from MXF/GXF + if (dec_ctx->dtvcc_rust) + { + int is_active = ccxr_dtvcc_is_active(dec_ctx->dtvcc_rust); + if (!is_active) + { + ccxr_dtvcc_set_active(dec_ctx->dtvcc_rust, 1); + } + } +#endif + + // Use process_cc_data to properly invoke DTVCC decoder for 708 captions + int cc_count = data_node->len / 3; + process_cc_data(enc_ctx, dec_ctx, data_node->buffer, cc_count, dec_sub); + got = data_node->len; } else if (data_node->bufferdatatype == CCX_ISDB_SUBTITLE) { diff --git a/src/rust/src/decoder/mod.rs b/src/rust/src/decoder/mod.rs index 4c9498d9c..6f27b4894 100644 --- a/src/rust/src/decoder/mod.rs +++ b/src/rust/src/decoder/mod.rs @@ -515,8 +515,13 @@ impl DtvccRust { } if let Some(decoder) = &mut self.decoders[i] { - if decoder.cc_count > 0 { - // Flush this decoder + // Check if there's content to flush: either cc_count > 0 (already printed) + // or any window has visible content (needs to be printed during flush) + let has_visible_windows = decoder + .windows + .iter() + .any(|w| is_true(w.visible)); + if decoder.cc_count > 0 || has_visible_windows { self.flush_decoder(i); } } diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index 87e4fa369..7a12d3e9f 100644 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -373,6 +373,20 @@ pub extern "C" fn ccxr_dtvcc_is_active(dtvcc_ptr: *mut std::ffi::c_void) -> i32 } } +/// Enable or disable the DTVCC decoder +/// This allows enabling the decoder after initialization +/// +/// # Safety +/// dtvcc_ptr must be a valid pointer to a DtvccRust struct or null +#[no_mangle] +pub extern "C" fn ccxr_dtvcc_set_active(dtvcc_ptr: *mut std::ffi::c_void, active: i32) { + if dtvcc_ptr.is_null() { + return; + } + let dtvcc = unsafe { &mut *(dtvcc_ptr as *mut DtvccRust) }; + dtvcc.is_active = active != 0; +} + /// Process cc_data /// /// # Safety @@ -402,6 +416,7 @@ extern "C" fn ccxr_process_cc_data( let mut cc_data: Vec = (0..cc_count * 3) .map(|x| unsafe { *data.add(x as usize) }) .collect(); + // Use the persistent DtvccRust context from dtvcc_rust let dtvcc_rust = dec_ctx.dtvcc_rust as *mut DtvccRust; if dtvcc_rust.is_null() { From 04ed95f8b59fa327c30380ecdef8a7649b4a02ba Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Sun, 4 Jan 2026 08:51:13 +0100 Subject: [PATCH 2/5] Fix MXF files containing CEA-708 captions not being detected/extracted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: CCX_RAW_TYPE data from MXF demuxer was not being passed to the DTVCC decoder, only to the legacy 608 decoder via process_raw_with_field. Changes: - general_loop.c: Changed CCX_RAW_TYPE handling to use process_cc_data instead of process_raw_with_field to properly invoke DTVCC decoder - general_loop.c: Added DTVCC activation for MXF/GXF sources since they may contain 708 captions - general_loop.c: Initialize timing from caption PTS when not set - ccx_dtvcc.h: Added ccxr_dtvcc_set_active FFI declaration - lib.rs: Added ccxr_dtvcc_set_active function to enable DTVCC decoder - decoder/mod.rs: Fixed flush logic to always process visible windows - ccx_demuxer_mxf.c: Fixed PTS calculation to use 90kHz units based on edit_rate, and changed verbose logging to debug() Fixes #1647 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/rust/src/decoder/mod.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/rust/src/decoder/mod.rs b/src/rust/src/decoder/mod.rs index 6f27b4894..7a3ecd42f 100644 --- a/src/rust/src/decoder/mod.rs +++ b/src/rust/src/decoder/mod.rs @@ -514,16 +514,9 @@ impl DtvccRust { continue; } - if let Some(decoder) = &mut self.decoders[i] { - // Check if there's content to flush: either cc_count > 0 (already printed) - // or any window has visible content (needs to be printed during flush) - let has_visible_windows = decoder - .windows - .iter() - .any(|w| is_true(w.visible)); - if decoder.cc_count > 0 || has_visible_windows { - self.flush_decoder(i); - } + if let Some(_decoder) = &mut self.decoders[i] { + // Always flush for visible windows + self.flush_decoder(i); } } } From 41fb966f6f71e999e7a1791ec1a2af29d6ef3c4f Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Sun, 4 Jan 2026 09:07:43 +0100 Subject: [PATCH 3/5] Add support for raw CDP (Caption Distribution Packet) files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds support for processing raw CDP files captured from SDI VANC (e.g., from Blackmagic Decklink capture cards). CDP packets are automatically detected by their 0x9669 identifier when using -in=raw. Changes: - Added process_raw_cdp() function to parse concatenated CDP packets - Added CDP format detection in raw_loop() (checks for 0x9669 header) - Extracts cc_data triplets from CDP packets and processes them through process_cc_data() for both CEA-608 and CEA-708 support - Calculates timing based on CDP frame rate and packet count Usage: ccextractor -in=raw captured_vanc.bin -o output.srt Fixes #1406 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/lib_ccx/general_loop.c | 116 +++++++++++++++++++++++++++++++++++- src/rust/src/decoder/mod.rs | 13 +++- 2 files changed, 125 insertions(+), 4 deletions(-) diff --git a/src/lib_ccx/general_loop.c b/src/lib_ccx/general_loop.c index 3a26f5b17..070866bb4 100644 --- a/src/lib_ccx/general_loop.c +++ b/src/lib_ccx/general_loop.c @@ -567,6 +567,104 @@ static size_t process_raw_for_mcc(struct encoder_ctx *enc_ctx, struct lib_cc_dec } // Raw file process +// Parse raw CDP (Caption Distribution Packet) data +// Returns number of bytes processed +static size_t process_raw_cdp(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, + struct cc_subtitle *sub, unsigned char *buffer, size_t len) +{ + size_t pos = 0; + int cdp_count = 0; + + while (pos + 10 < len) // Minimum CDP size + { + // Check for CDP identifier + if (buffer[pos] != 0x96 || buffer[pos + 1] != 0x69) + { + pos++; + continue; + } + + unsigned char cdp_length = buffer[pos + 2]; + if (pos + cdp_length > len) + break; // Incomplete CDP packet + + unsigned char framerate_byte = buffer[pos + 3]; + int framerate_code = framerate_byte >> 4; + + // Skip to find cc_data section (0x72) + size_t cdp_pos = pos + 4; // After identifier, length, framerate + int cc_count = 0; + unsigned char *cc_data = NULL; + + // Skip header sequence counter (2 bytes) + cdp_pos += 2; + + // Look for cc_data section (0x72) within CDP + while (cdp_pos < pos + cdp_length - 4) + { + if (buffer[cdp_pos] == 0x72) // cc_data section + { + cc_count = buffer[cdp_pos + 1] & 0x1F; + cc_data = buffer + cdp_pos + 2; + break; + } + else if (buffer[cdp_pos] == 0x71) // time code section + { + cdp_pos += 5; // Skip time code section + } + else if (buffer[cdp_pos] == 0x73) // service info section + { + break; // Past cc_data + } + else if (buffer[cdp_pos] == 0x74) // footer + { + break; + } + else + { + cdp_pos++; + } + } + + if (cc_count > 0 && cc_data != NULL) + { + // Calculate PTS based on CDP frame count and frame rate + static const int fps_table[] = {0, 24, 24, 25, 30, 30, 50, 60, 60}; + int fps = (framerate_code < 9) ? fps_table[framerate_code] : 30; + LLONG pts = (LLONG)cdp_count * 90000 / fps; + + // Set timing if not already set + if (dec_ctx->timing->pts_set == 0) + { + dec_ctx->timing->min_pts = pts; + dec_ctx->timing->pts_set = 2; + dec_ctx->timing->sync_pts = pts; + } + set_current_pts(dec_ctx->timing, pts); + set_fts(dec_ctx->timing); + +#ifndef DISABLE_RUST + // Enable DTVCC decoder for CEA-708 captions + if (dec_ctx->dtvcc_rust) + { + int is_active = ccxr_dtvcc_is_active(dec_ctx->dtvcc_rust); + if (!is_active) + { + ccxr_dtvcc_set_active(dec_ctx->dtvcc_rust, 1); + } + } +#endif + // Process cc_data triplets through process_cc_data for 708 support + process_cc_data(enc_ctx, dec_ctx, cc_data, cc_count, sub); + cdp_count++; + } + + pos += cdp_length; + } + + return pos; +} + int raw_loop(struct lib_ccx_ctx *ctx) { LLONG ret; @@ -577,6 +675,7 @@ int raw_loop(struct lib_ccx_ctx *ctx) int caps = 0; int is_dvdraw = 0; // Flag to track if this is DVD raw format int is_scc = 0; // Flag to track if this is SCC format + int is_cdp = 0; // Flag to track if this is raw CDP format int is_mcc_output = 0; // Flag for MCC output format dec_ctx = update_decoder_list(ctx); @@ -622,7 +721,15 @@ int raw_loop(struct lib_ccx_ctx *ctx) mprint("Detected SCC (Scenarist Closed Caption) format\n"); } - if (is_mcc_output && !is_dvdraw && !is_scc) + // Check if this is raw CDP format (starts with 0x9669) + if (!is_cdp && !is_scc && !is_dvdraw && data->len >= 2 && + data->buffer[0] == 0x96 && data->buffer[1] == 0x69) + { + is_cdp = 1; + mprint("Detected raw CDP (Caption Distribution Packet) format\n"); + } + + if (is_mcc_output && !is_dvdraw && !is_scc && !is_cdp) { // For MCC output, encode raw data directly without decoding // This preserves the original CEA-608 byte pairs in CDP format @@ -640,6 +747,13 @@ int raw_loop(struct lib_ccx_ctx *ctx) // Use Rust SCC implementation - handles timing internally via SMPTE timecodes ret = ccxr_process_scc(dec_ctx, dec_sub, data->buffer, (unsigned int)data->len, ccx_options.scc_framerate); } + else if (is_cdp) + { + // Process raw CDP packets (e.g., from SDI VANC capture) + ret = process_raw_cdp(enc_ctx, dec_ctx, dec_sub, data->buffer, data->len); + if (ret > 0) + caps = 1; + } else { ret = process_raw(dec_ctx, dec_sub, data->buffer, data->len); diff --git a/src/rust/src/decoder/mod.rs b/src/rust/src/decoder/mod.rs index 7a3ecd42f..6f27b4894 100644 --- a/src/rust/src/decoder/mod.rs +++ b/src/rust/src/decoder/mod.rs @@ -514,9 +514,16 @@ impl DtvccRust { continue; } - if let Some(_decoder) = &mut self.decoders[i] { - // Always flush for visible windows - self.flush_decoder(i); + if let Some(decoder) = &mut self.decoders[i] { + // Check if there's content to flush: either cc_count > 0 (already printed) + // or any window has visible content (needs to be printed during flush) + let has_visible_windows = decoder + .windows + .iter() + .any(|w| is_true(w.visible)); + if decoder.cc_count > 0 || has_visible_windows { + self.flush_decoder(i); + } } } } From 90041554a375a6f651b493772ebf5088f3b82a47 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Sun, 4 Jan 2026 12:55:35 +0100 Subject: [PATCH 4/5] Fix Rust formatting and clippy issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Apply cargo fmt to decoder/mod.rs - Fix clippy manual_flatten warning in build.rs by using .flatten() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/rust/build.rs | 13 +++++-------- src/rust/src/decoder/mod.rs | 5 +---- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/rust/build.rs b/src/rust/build.rs index e58e0f582..6cce8fc38 100644 --- a/src/rust/build.rs +++ b/src/rust/build.rs @@ -103,14 +103,11 @@ fn main() { if std::path::Path::new(cellar_ffmpeg).exists() { // Find the FFmpeg version directory if let Ok(entries) = std::fs::read_dir(cellar_ffmpeg) { - for entry in entries { - if let Ok(entry) = entry { - let include_path = entry.path().join("include"); - if include_path.exists() { - builder = - builder.clang_arg(format!("-I{}", include_path.display())); - break; - } + for entry in entries.flatten() { + let include_path = entry.path().join("include"); + if include_path.exists() { + builder = builder.clang_arg(format!("-I{}", include_path.display())); + break; } } } diff --git a/src/rust/src/decoder/mod.rs b/src/rust/src/decoder/mod.rs index 6f27b4894..ff1439722 100644 --- a/src/rust/src/decoder/mod.rs +++ b/src/rust/src/decoder/mod.rs @@ -517,10 +517,7 @@ impl DtvccRust { if let Some(decoder) = &mut self.decoders[i] { // Check if there's content to flush: either cc_count > 0 (already printed) // or any window has visible content (needs to be printed during flush) - let has_visible_windows = decoder - .windows - .iter() - .any(|w| is_true(w.visible)); + let has_visible_windows = decoder.windows.iter().any(|w| is_true(w.visible)); if decoder.cc_count > 0 || has_visible_windows { self.flush_decoder(i); } From b4d8e0ffaf14bcd69f24e8ecb59d8b1e7d903a0c Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Sun, 4 Jan 2026 14:08:26 +0100 Subject: [PATCH 5/5] Trigger CI