diff --git a/.gitignore b/.gitignore index bdb5246d4..4a45173f2 100644 --- a/.gitignore +++ b/.gitignore @@ -143,6 +143,9 @@ bazel* #Intellij IDEs .idea/ +# Plans (local only) +plans/ + # Rust build and MakeFiles (and CMake files) src/rust/CMakeFiles/ src/rust/CMakeCache.txt diff --git a/src/lib_ccx/ccx_decoders_common.c b/src/lib_ccx/ccx_decoders_common.c index ca323c371..d2b440e54 100644 --- a/src/lib_ccx/ccx_decoders_common.c +++ b/src/lib_ccx/ccx_decoders_common.c @@ -224,7 +224,12 @@ int do_cb(struct lib_cc_decode *ctx, unsigned char *cc_block, struct cc_subtitle void dinit_cc_decode(struct lib_cc_decode **ctx) { struct lib_cc_decode *lctx = *ctx; +#ifndef DISABLE_RUST + ccxr_dtvcc_free(lctx->dtvcc_rust); + lctx->dtvcc_rust = NULL; +#else dtvcc_free(&lctx->dtvcc); +#endif dinit_avc(&lctx->avc_ctx); ccx_decoder_608_dinit_library(&lctx->context_cc608_field_1); ccx_decoder_608_dinit_library(&lctx->context_cc608_field_2); @@ -294,10 +299,16 @@ struct lib_cc_decode *init_cc_decode(struct ccx_decoders_common_settings_t *sett ctx->no_rollup = setting->no_rollup; ctx->noscte20 = setting->noscte20; +#ifndef DISABLE_RUST + ctx->dtvcc_rust = ccxr_dtvcc_init(setting->settings_dtvcc); + ctx->dtvcc = NULL; // Not used when Rust is enabled +#else ctx->dtvcc = dtvcc_init(setting->settings_dtvcc); if (!ctx->dtvcc) fatal(EXIT_NOT_ENOUGH_MEMORY, "In init_cc_decode: Out of memory initializing dtvcc."); ctx->dtvcc->is_active = setting->settings_dtvcc->enabled; + ctx->dtvcc_rust = NULL; +#endif if (setting->codec == CCX_CODEC_ATSC_CC) { @@ -477,6 +488,13 @@ void flush_cc_decode(struct lib_cc_decode *ctx, struct cc_subtitle *sub) } } } +#ifndef DISABLE_RUST + if (ccxr_dtvcc_is_active(ctx->dtvcc_rust)) + { + ctx->current_field = 3; + ccxr_flush_active_decoders(ctx->dtvcc_rust); + } +#else if (ctx->dtvcc->is_active) { for (int i = 0; i < CCX_DTVCC_MAX_SERVICES; i++) @@ -491,6 +509,7 @@ void flush_cc_decode(struct lib_cc_decode *ctx, struct cc_subtitle *sub) } } } +#endif } struct encoder_ctx *copy_encoder_context(struct encoder_ctx *ctx) { diff --git a/src/lib_ccx/ccx_decoders_common.h b/src/lib_ccx/ccx_decoders_common.h index ff1d9d03e..b4b3769f0 100644 --- a/src/lib_ccx/ccx_decoders_common.h +++ b/src/lib_ccx/ccx_decoders_common.h @@ -32,4 +32,10 @@ struct cc_subtitle *copy_subtitle(struct cc_subtitle *sub); void free_encoder_context(struct encoder_ctx *ctx); void free_decoder_context(struct lib_cc_decode *ctx); void free_subtitle(struct cc_subtitle *sub); + +#ifndef DISABLE_RUST +// Rust FFI function to flush active CEA-708 service decoders +extern void ccxr_flush_active_decoders(void *dtvcc_rust); +#endif + #endif diff --git a/src/lib_ccx/ccx_decoders_structs.h b/src/lib_ccx/ccx_decoders_structs.h index 398a6baa1..4b5334ac9 100644 --- a/src/lib_ccx/ccx_decoders_structs.h +++ b/src/lib_ccx/ccx_decoders_structs.h @@ -208,6 +208,7 @@ struct lib_cc_decode int false_pict_header; dtvcc_ctx *dtvcc; + void *dtvcc_rust; // Persistent Rust CEA-708 decoder context int current_field; // Analyse/use the picture information int maxtref; // Use to remember the temporal reference number diff --git a/src/lib_ccx/ccx_dtvcc.h b/src/lib_ccx/ccx_dtvcc.h index b5c71ca88..446064a1d 100644 --- a/src/lib_ccx/ccx_dtvcc.h +++ b/src/lib_ccx/ccx_dtvcc.h @@ -10,4 +10,13 @@ void dtvcc_process_data(struct dtvcc_ctx *dtvcc, dtvcc_ctx *dtvcc_init(ccx_decoder_dtvcc_settings *opts); void dtvcc_free(dtvcc_ctx **); +#ifndef DISABLE_RUST +// Rust FFI functions for persistent CEA-708 decoder +extern void *ccxr_dtvcc_init(struct ccx_decoder_dtvcc_settings *settings_dtvcc); +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); +#endif + #endif // CCEXTRACTOR_CCX_DTVCC_H diff --git a/src/lib_ccx/general_loop.c b/src/lib_ccx/general_loop.c index d82a03314..6550dd15b 100644 --- a/src/lib_ccx/general_loop.c +++ b/src/lib_ccx/general_loop.c @@ -1054,7 +1054,11 @@ int process_non_multiprogram_general_loop(struct lib_ccx_ctx *ctx, cinfo = get_cinfo(ctx->demux_ctx, pid); *enc_ctx = update_encoder_list_cinfo(ctx, cinfo); *dec_ctx = update_decoder_list_cinfo(ctx, cinfo); +#ifndef DISABLE_RUST + ccxr_dtvcc_set_encoder((*dec_ctx)->dtvcc_rust, *enc_ctx); +#else (*dec_ctx)->dtvcc->encoder = (void *)(*enc_ctx); +#endif if ((*dec_ctx)->timing->min_pts == 0x01FFFFFFFFLL) // if we didn't set the min_pts of the program { @@ -1278,7 +1282,11 @@ int general_loop(struct lib_ccx_ctx *ctx) enc_ctx = update_encoder_list_cinfo(ctx, cinfo); dec_ctx = update_decoder_list_cinfo(ctx, cinfo); +#ifndef DISABLE_RUST + ccxr_dtvcc_set_encoder(dec_ctx->dtvcc_rust, enc_ctx); +#else dec_ctx->dtvcc->encoder = (void *)enc_ctx; // WARN: otherwise cea-708 will not work +#endif if (dec_ctx->timing->min_pts == 0x01FFFFFFFFLL) // if we didn't set the min_pts of the program { @@ -1488,7 +1496,11 @@ int rcwt_loop(struct lib_ccx_ctx *ctx) } dec_ctx = update_decoder_list(ctx); +#ifndef DISABLE_RUST + ccxr_dtvcc_set_encoder(dec_ctx->dtvcc_rust, enc_ctx); +#else dec_ctx->dtvcc->encoder = (void *)enc_ctx; // WARN: otherwise cea-708 will not work +#endif if (parsebuf[6] == 0 && parsebuf[7] == 2) { dec_ctx->codec = CCX_CODEC_TELETEXT; diff --git a/src/lib_ccx/lib_ccx.h b/src/lib_ccx/lib_ccx.h index c97691bed..6044f367d 100644 --- a/src/lib_ccx/lib_ccx.h +++ b/src/lib_ccx/lib_ccx.h @@ -341,4 +341,9 @@ int process_non_multiprogram_general_loop(struct lib_ccx_ctx *ctx, void segment_output_file(struct lib_ccx_ctx *ctx, struct lib_cc_decode *dec_ctx); int decode_vbi(struct lib_cc_decode *dec_ctx, uint8_t field, unsigned char *buffer, size_t len, struct cc_subtitle *sub); +#ifndef DISABLE_RUST +// Rust FFI function to set encoder on persistent CEA-708 decoder +void ccxr_dtvcc_set_encoder(void *dtvcc_rust, struct encoder_ctx *encoder); +#endif + #endif diff --git a/src/lib_ccx/mp4.c b/src/lib_ccx/mp4.c index c9b7970d1..e0c03a79e 100644 --- a/src/lib_ccx/mp4.c +++ b/src/lib_ccx/mp4.c @@ -749,7 +749,11 @@ static int process_clcp(struct lib_ccx_ctx *ctx, struct encoder_ctx *enc_ctx, dbg_print(CCX_DMT_PARSE, "MP4-708: atom skipped (cc_type < 2)\n"); continue; } +#ifndef DISABLE_RUST + ccxr_dtvcc_process_data(dec_ctx->dtvcc_rust, cc_valid, cc_type, temp[2], temp[3]); +#else dtvcc_process_data(dec_ctx->dtvcc, (unsigned char *)temp); +#endif cb_708++; } if (ctx->write_format == CCX_OF_MCC) @@ -887,8 +891,12 @@ int processmp4(struct lib_ccx_ctx *ctx, struct ccx_s_mp4Cfg *cfg, char *file) if (enc_ctx) enc_ctx->timing = dec_ctx->timing; - // WARN: otherwise cea-708 will not work + // WARN: otherwise cea-708 will not work +#ifndef DISABLE_RUST + ccxr_dtvcc_set_encoder(dec_ctx->dtvcc_rust, enc_ctx); +#else dec_ctx->dtvcc->encoder = (void *)enc_ctx; +#endif memset(&dec_sub, 0, sizeof(dec_sub)); mprint("Opening \'%s\': ", file); diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index a91553fb1..c0603230f 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -163,9 +163,9 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "camino" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" [[package]] name = "ccx_rust" @@ -645,9 +645,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "lazy_static" @@ -978,9 +978,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] @@ -1099,9 +1099,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags 2.10.0", "errno", @@ -1323,14 +1323,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -1447,18 +1447,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.9" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", "toml_datetime", @@ -1468,9 +1468,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] diff --git a/src/rust/src/avc/core.rs b/src/rust/src/avc/core.rs index 845555ac8..1f5d0a3f9 100644 --- a/src/rust/src/avc/core.rs +++ b/src/rust/src/avc/core.rs @@ -453,7 +453,7 @@ pub fn hex_dump(data: &[u8]) { // Print hex bytes for byte in chunk { - print!("{:02X} ", byte); + print!("{byte:02X} "); } // Pad if less than 16 bytes diff --git a/src/rust/src/decoder/mod.rs b/src/rust/src/decoder/mod.rs index 92e608cb8..4c9498d9c 100644 --- a/src/rust/src/decoder/mod.rs +++ b/src/rust/src/decoder/mod.rs @@ -10,7 +10,10 @@ mod timing; mod tv_screen; mod window; +use log::debug as log_debug; + use lib_ccxr::{ + common::DTVCC_MAX_SERVICES, debug, fatal, util::log::{DebugMessageFlag, ExitCause}, }; @@ -208,6 +211,359 @@ impl<'a> Dtvcc<'a> { } } +// ============================================================================= +// DtvccRust: Persistent CEA-708 decoder context for Rust-owned state +// ============================================================================= +// +// This struct is designed to be created once and persist throughout the program's +// lifetime, solving the issue where state was being reset on each call. +// See: https://github.com/CCExtractor/ccextractor/issues/1499 + +/// Persistent CEA-708 decoder context that owns its data. +/// +/// Unlike `Dtvcc` which borrows from C structures, `DtvccRust` owns all its +/// decoder state and is designed to persist across multiple processing calls. +/// This is created once via `ccxr_dtvcc_init()` and freed via `ccxr_dtvcc_free()`. +pub struct DtvccRust { + pub is_active: bool, + pub active_services_count: u8, + pub services_active: Vec, + pub report_enabled: bool, + pub report: *mut ccx_decoder_dtvcc_report, + pub decoders: [Option>; DTVCC_MAX_SERVICES], + pub packet: Vec, + pub packet_length: u8, + pub is_header_parsed: bool, + pub last_sequence: i32, + pub encoder: *mut encoder_ctx, + pub no_rollup: bool, + pub timing: *mut ccx_common_timing_ctx, +} + +impl DtvccRust { + /// Create a new persistent dtvcc context from settings. + /// + /// This closely follows `dtvcc_init` at `src/lib_ccx/ccx_dtvcc.c:82` + /// + /// # Safety + /// The following pointers in `opts` must not be null: + /// - `opts.report` + /// - `opts.timing` + pub fn new(opts: &ccx_decoder_dtvcc_settings) -> Self { + let is_active = is_true(opts.enabled); + let active_services_count = opts.active_services_count as u8; + let services_active = opts.services_enabled.to_vec(); + let report_enabled = is_true(opts.print_file_reports); + + // Reset the report counter + if !opts.report.is_null() { + unsafe { + (*opts.report).reset_count = 0; + } + } + + // Initialize packet state (equivalent to dtvcc_clear_packet) + let packet_length = 0; + let is_header_parsed = false; + let packet = vec![0u8; CCX_DTVCC_MAX_PACKET_LENGTH as usize]; + + let last_sequence = CCX_DTVCC_NO_LAST_SEQUENCE; + let no_rollup = is_true(opts.no_rollup); + + // Initialize decoders - only for active services + // Note: dtvcc_service_decoder is a large struct, so we must allocate it + // directly on the heap to avoid stack overflow. + let decoders = { + const INIT: Option> = None; + let mut decoders = [INIT; DTVCC_MAX_SERVICES]; + + for (i, d) in decoders.iter_mut().enumerate() { + if i >= opts.services_enabled.len() || !is_true(opts.services_enabled[i]) { + continue; + } + + // Create owned tv_screen on the heap using zeroed allocation + // to avoid stack overflow (dtvcc_tv_screen is also large) + let tv_layout = std::alloc::Layout::new::(); + let tv_ptr = unsafe { std::alloc::alloc_zeroed(tv_layout) } as *mut dtvcc_tv_screen; + if tv_ptr.is_null() { + panic!("Failed to allocate dtvcc_tv_screen"); + } + let mut tv_screen = unsafe { Box::from_raw(tv_ptr) }; + tv_screen.cc_count = 0; + tv_screen.service_number = i as i32 + 1; + + // Allocate decoder directly on heap using zeroed memory to avoid + // stack overflow (dtvcc_service_decoder is very large) + let decoder_layout = std::alloc::Layout::new::(); + let decoder_ptr = unsafe { std::alloc::alloc_zeroed(decoder_layout) } + as *mut dtvcc_service_decoder; + if decoder_ptr.is_null() { + panic!("Failed to allocate dtvcc_service_decoder"); + } + + let mut decoder = unsafe { Box::from_raw(decoder_ptr) }; + + // Set the tv pointer + decoder.tv = Box::into_raw(tv_screen); + + // Initialize windows + for window in decoder.windows.iter_mut() { + window.memory_reserved = 0; + } + + // Call reset handler + decoder.handle_reset(); + + *d = Some(decoder); + } + + decoders + }; + + // Encoder is set later via set_encoder() + let encoder = std::ptr::null_mut(); + + DtvccRust { + is_active, + active_services_count, + services_active, + report_enabled, + report: opts.report, + decoders, + packet, + packet_length, + is_header_parsed, + last_sequence, + no_rollup, + timing: opts.timing, + encoder, + } + } + + /// Set the encoder for this context. + /// + /// The encoder is typically not available at initialization time, + /// so it must be set separately before processing. + pub fn set_encoder(&mut self, encoder: *mut encoder_ctx) { + self.encoder = encoder; + } + + /// Process cc data and add it to the dtvcc packet. + /// + /// This is the main entry point for CEA-708 data processing. + pub fn process_cc_data(&mut self, cc_valid: u8, cc_type: u8, data1: u8, data2: u8) { + if !self.is_active && !self.report_enabled { + return; + } + + match cc_type { + // type 0 and 1 are for CEA 608 data and are handled before calling this function + // valid types for CEA 708 data are only 2 and 3 + 2 => { + log_debug!("dtvcc_process_data: DTVCC Channel Packet Data"); + if cc_valid == 1 && self.is_header_parsed { + if self.packet_length > 253 { + log_debug!("dtvcc_process_data: Warning: Legal packet size exceeded (1), data not added."); + } else { + self.add_data_to_packet(data1, data2); + + let mut max_len = self.packet[0] & 0x3F; + + if max_len == 0 { + // This is well defined in EIA-708; no magic. + max_len = 128; + } else { + max_len *= 2; + } + + // If packet is complete then process the packet + if self.packet_length >= max_len { + self.process_current_packet(max_len); + } + } + } + } + 3 => { + log_debug!("dtvcc_process_data: DTVCC Channel Packet Start"); + if cc_valid == 1 { + if self.packet_length > (CCX_DTVCC_MAX_PACKET_LENGTH - 1) { + log_debug!("dtvcc_process_data: Warning: Legal packet size exceeded (2), data not added."); + } else { + if self.is_header_parsed { + log_debug!("dtvcc_process_data: Warning: Incorrect packet length specified. Packet will be skipped."); + self.clear_packet(); + } + self.add_data_to_packet(data1, data2); + self.is_header_parsed = true; + } + } + } + _ => fatal!(cause = ExitCause::Bug; + "dtvcc_process_data: shouldn't be here - cc_type: {}", + cc_type + ), + } + } + + /// Add data to the packet + fn add_data_to_packet(&mut self, data1: u8, data2: u8) { + self.packet[self.packet_length as usize] = data1; + self.packet_length += 1; + self.packet[self.packet_length as usize] = data2; + self.packet_length += 1; + } + + /// Process current packet into service blocks + fn process_current_packet(&mut self, len: u8) { + let seq = (self.packet[0] & 0xC0) >> 6; + log_debug!("dtvcc_process_current_packet: Sequence: {seq}, packet length: {len}"); + if self.packet_length == 0 { + return; + } + + // Check if current sequence is correct + // Sequence number is a 2 bit rolling sequence from (0-3) + if self.last_sequence != CCX_DTVCC_NO_LAST_SEQUENCE + && (self.last_sequence + 1) % 4 != seq as i32 + { + log_debug!( + "dtvcc_process_current_packet: Unexpected sequence number, it is {} but should be {}", + seq, (self.last_sequence + 1) % 4 + ); + } + self.last_sequence = seq as i32; + + let mut pos: u8 = 1; + while pos < len { + let mut service_number = (self.packet[pos as usize] & 0xE0) >> 5; // 3 more significant bits + let block_length = self.packet[pos as usize] & 0x1F; // 5 less significant bits + log_debug!("dtvcc_process_current_packet: Standard header Service number: {service_number}, Block length: {block_length}"); + + if service_number == 7 { + // There is an extended header + // CEA-708-E 6.2.2 Extended Service Block Header + pos += 1; + service_number = self.packet[pos as usize] & 0x3F; // 6 more significant bits + if service_number > 7 { + log_debug!("dtvcc_process_current_packet: Illegal service number in extended header: {service_number}"); + } + } + + pos += 1; + + if service_number == 0 && block_length != 0 { + // Illegal, but specs say what to do... + pos = len; // Move to end + break; + } + + if block_length != 0 && !self.report.is_null() { + unsafe { + (*self.report).services[service_number as usize] = 1; + } + } + + if service_number > 0 && is_true(self.services_active[(service_number - 1) as usize]) { + if let Some(decoder) = &mut self.decoders[(service_number - 1) as usize] { + // Get encoder and timing references + if !self.encoder.is_null() && !self.timing.is_null() { + let encoder = unsafe { &mut *self.encoder }; + let timing = unsafe { &mut *self.timing }; + decoder.process_service_block( + &self.packet[pos as usize..(pos + block_length) as usize], + encoder, + timing, + self.no_rollup, + ); + } + } + } + + pos += block_length // Skip data + } + + self.clear_packet(); + + if len < 128 && self.packet[pos as usize] != 0 { + // Null header is mandatory if there is room + log_debug!( + "dtvcc_process_current_packet: Warning: Null header expected but not found." + ); + } + } + + /// Clear current packet + fn clear_packet(&mut self) { + self.packet_length = 0; + self.is_header_parsed = false; + self.packet.iter_mut().for_each(|x| *x = 0); + } + + /// Flush all active service decoders. + /// + /// This writes out any pending caption data from all active services. + /// Called when processing is complete or when switching contexts. + pub fn flush_active_decoders(&mut self) { + if !self.is_active { + return; + } + + for i in 0..DTVCC_MAX_SERVICES { + if i >= self.services_active.len() || !is_true(self.services_active[i]) { + continue; + } + + if let Some(decoder) = &mut self.decoders[i] { + if decoder.cc_count > 0 { + // Flush this decoder + self.flush_decoder(i); + } + } + } + } + + /// Flush a specific service decoder by index. + fn flush_decoder(&mut self, service_index: usize) { + log_debug!( + "dtvcc_decoder_flush: Flushing decoder for service {}", + service_index + 1 + ); + + // Need encoder and timing to flush + if self.encoder.is_null() || self.timing.is_null() { + log_debug!("dtvcc_decoder_flush: Cannot flush - encoder or timing is null"); + return; + } + + if let Some(decoder) = &mut self.decoders[service_index] { + let timing = unsafe { &mut *self.timing }; + let encoder = unsafe { &mut *self.encoder }; + + let mut screen_content_changed = false; + + // Process all visible windows + for i in 0..CCX_DTVCC_MAX_WINDOWS { + let window = &mut decoder.windows[i as usize]; + if is_true(window.visible) { + screen_content_changed = true; + window.update_time_hide(timing); + // Copy window content to screen + decoder.copy_to_screen(&decoder.windows[i as usize]); + decoder.windows[i as usize].visible = 0; + } + } + + if screen_content_changed { + decoder.screen_print(encoder, timing); + } + decoder.flush(encoder); + } + } +} + +const CCX_DTVCC_MAX_WINDOWS: u8 = 8; + /// A single character symbol /// /// sym stores the symbol @@ -361,4 +717,130 @@ pub mod test { assert_eq!(decoder.report.services[8], 1); assert_eq!(decoder.packet_length, 0); // due to `clear_packet()` fn call } + + // ========================================================================= + // Tests for DtvccRust (persistent CEA-708 decoder) + // ========================================================================= + + /// Helper function to create a test ccx_decoder_dtvcc_settings + /// Uses heap allocation to avoid stack overflow with large structs + pub fn create_test_dtvcc_settings() -> Box { + let mut settings = get_zero_allocated_obj::(); + + // Initialize required pointers using heap allocation + let report = get_zero_allocated_obj::(); + settings.report = Box::into_raw(report); + + let timing = get_zero_allocated_obj::(); + settings.timing = Box::into_raw(timing); + + // Enable the decoder and first service + settings.enabled = 1; + settings.active_services_count = 1; + settings.services_enabled[0] = 1; + + settings + } + + #[test] + fn test_dtvcc_rust_new() { + let settings = create_test_dtvcc_settings(); + let dtvcc = DtvccRust::new(&settings); + + // Verify basic initialization + assert!(dtvcc.is_active); + assert_eq!(dtvcc.active_services_count, 1); + assert_eq!(dtvcc.packet_length, 0); + assert!(!dtvcc.is_header_parsed); + assert_eq!(dtvcc.last_sequence, CCX_DTVCC_NO_LAST_SEQUENCE); + + // Verify encoder is initially null (set later) + assert!(dtvcc.encoder.is_null()); + + // Verify first decoder is created (service 0 is enabled) + assert!(dtvcc.decoders[0].is_some()); + + // Verify other decoders are not created + assert!(dtvcc.decoders[1].is_none()); + } + + #[test] + fn test_dtvcc_rust_set_encoder() { + let settings = create_test_dtvcc_settings(); + let mut dtvcc = DtvccRust::new(&settings); + + // Initially null + assert!(dtvcc.encoder.is_null()); + + // Create an encoder and set it + let mut encoder = Box::new(encoder_ctx::default()); + let encoder_ptr = &mut *encoder as *mut encoder_ctx; + dtvcc.set_encoder(encoder_ptr); + + // Verify encoder is set + assert!(!dtvcc.encoder.is_null()); + assert_eq!(dtvcc.encoder, encoder_ptr); + } + + #[test] + fn test_dtvcc_rust_process_cc_data() { + let settings = create_test_dtvcc_settings(); + let mut dtvcc = DtvccRust::new(&settings); + + // Process cc_type = 3 (packet start) - should set is_header_parsed + dtvcc.process_cc_data(1, 3, 0xC2, 0x00); + + assert!(dtvcc.is_header_parsed); + assert_eq!(dtvcc.packet_length, 2); + assert_eq!(dtvcc.packet[0], 0xC2); + assert_eq!(dtvcc.packet[1], 0x00); + } + + #[test] + fn test_dtvcc_rust_clear_packet() { + let settings = create_test_dtvcc_settings(); + let mut dtvcc = DtvccRust::new(&settings); + + // Add some data + dtvcc.process_cc_data(1, 3, 0xC2, 0x00); + assert!(dtvcc.is_header_parsed); + assert_eq!(dtvcc.packet_length, 2); + + // Process more data that triggers clear (when packet is malformed) + // Simulate by directly testing the packet processing + dtvcc.is_header_parsed = true; + dtvcc.packet[0] = 0x02; // Very short packet length (2*1 = 2 bytes) + dtvcc.packet_length = 2; + + // This should process and clear the packet + dtvcc.process_cc_data(1, 2, 0x00, 0x00); + + // After processing a complete packet, it should be cleared + assert_eq!(dtvcc.packet_length, 0); + assert!(!dtvcc.is_header_parsed); + } + + #[test] + fn test_dtvcc_rust_state_persistence() { + // This test verifies the key fix: state persists across calls + let settings = create_test_dtvcc_settings(); + let mut dtvcc = DtvccRust::new(&settings); + + // First call: start a packet + dtvcc.process_cc_data(1, 3, 0xC4, 0x00); // Packet with length 4*2=8 bytes + assert!(dtvcc.is_header_parsed); + assert_eq!(dtvcc.packet_length, 2); + + // Second call: add more data (this is where the old code would fail) + dtvcc.process_cc_data(1, 2, 0x21, 0x00); + assert_eq!(dtvcc.packet_length, 4); + + // Third call: add more data + dtvcc.process_cc_data(1, 2, 0x00, 0x00); + assert_eq!(dtvcc.packet_length, 6); + + // State is preserved across all calls! + assert!(dtvcc.is_header_parsed); + assert_eq!(dtvcc.last_sequence, CCX_DTVCC_NO_LAST_SEQUENCE); // Not processed yet + } } diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index 9f267c7b3..acd02a417 100644 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -38,7 +38,7 @@ use bindings::*; use cfg_if::cfg_if; use clap::{error::ErrorKind, Parser}; use common::{copy_from_rust, CType, CType2}; -use decoder::Dtvcc; +use decoder::{Dtvcc, DtvccRust}; use lib_ccxr::{common::Options, teletext::TeletextConfig, util::log::ExitCause}; use parser::OptionsExt; use utils::is_true; @@ -210,11 +210,175 @@ pub extern "C" fn ccxr_init_logger() { .init(); } +// ============================================================================= +// FFI functions for persistent DtvccRust context +// ============================================================================= +// +// These functions provide a C-compatible interface for managing the persistent +// Rust CEA-708 decoder context. They are designed to be called from C code +// and will be used in Phase 2-3 of the implementation. +// See: https://github.com/CCExtractor/ccextractor/issues/1499 + +/// Create a new persistent DtvccRust context. +/// +/// This function allocates and initializes a new `DtvccRust` struct on the heap +/// and returns an opaque pointer to it. The context persists until freed with +/// `ccxr_dtvcc_free()`. +/// +/// # Safety +/// - `opts_ptr` must be a valid pointer to `ccx_decoder_dtvcc_settings` +/// - `opts.report` and `opts.timing` must not be null +/// - The returned pointer must be freed with `ccxr_dtvcc_free()` when done +/// +/// # Returns +/// An opaque pointer to the DtvccRust context, or null if opts_ptr is null. +#[no_mangle] +pub unsafe extern "C" fn ccxr_dtvcc_init( + opts_ptr: *const ccx_decoder_dtvcc_settings, +) -> *mut std::ffi::c_void { + if opts_ptr.is_null() { + return std::ptr::null_mut(); + } + let opts = &*opts_ptr; + let dtvcc = Box::new(DtvccRust::new(opts)); + Box::into_raw(dtvcc) as *mut std::ffi::c_void +} + +/// Free a DtvccRust context. +/// +/// This function properly frees all memory associated with the DtvccRust context, +/// including owned decoders and their tv_screens. +/// +/// # Safety +/// - `dtvcc_ptr` must be a valid pointer returned by `ccxr_dtvcc_init()` +/// - `dtvcc_ptr` must not be used after this call +/// - It is safe to call with a null pointer (no-op) +#[no_mangle] +pub extern "C" fn ccxr_dtvcc_free(dtvcc_ptr: *mut std::ffi::c_void) { + if dtvcc_ptr.is_null() { + return; + } + + let dtvcc = unsafe { Box::from_raw(dtvcc_ptr as *mut DtvccRust) }; + + // Free owned decoders and their tv_screens + for (i, decoder_opt) in dtvcc.decoders.iter().enumerate() { + if i >= dtvcc.services_active.len() || !is_true(dtvcc.services_active[i]) { + continue; + } + + if let Some(decoder) = decoder_opt { + // Free windows rows if memory was reserved + for window in decoder.windows.iter() { + if is_true(window.memory_reserved) { + for row_ptr in window.rows.iter() { + if !row_ptr.is_null() { + unsafe { + drop(Box::from_raw(*row_ptr)); + } + } + } + } + } + + // Free the tv_screen + if !decoder.tv.is_null() { + unsafe { + drop(Box::from_raw(decoder.tv)); + } + } + } + } + + // The Box containing dtvcc will be dropped here, freeing the DtvccRust struct + drop(dtvcc); +} + +/// Set the encoder for a DtvccRust context. +/// +/// The encoder is typically not available at initialization time, so it must +/// be set separately before processing begins. +/// +/// # Safety +/// - `dtvcc_ptr` must be a valid pointer returned by `ccxr_dtvcc_init()` +/// - `encoder` can be null (processing will skip service blocks if so) +#[no_mangle] +pub extern "C" fn ccxr_dtvcc_set_encoder( + dtvcc_ptr: *mut std::ffi::c_void, + encoder: *mut encoder_ctx, +) { + if dtvcc_ptr.is_null() { + return; + } + let dtvcc = unsafe { &mut *(dtvcc_ptr as *mut DtvccRust) }; + dtvcc.set_encoder(encoder); +} + +/// Process CEA-708 CC data using the persistent DtvccRust context. +/// +/// This function processes a single CC data unit (cc_valid, cc_type, data1, data2) +/// using the persistent context, maintaining state across calls. +/// +/// # Safety +/// - `dtvcc_ptr` must be a valid pointer returned by `ccxr_dtvcc_init()` +#[no_mangle] +pub extern "C" fn ccxr_dtvcc_process_data( + dtvcc_ptr: *mut std::ffi::c_void, + cc_valid: u8, + cc_type: u8, + data1: u8, + data2: u8, +) { + if dtvcc_ptr.is_null() { + return; + } + let dtvcc = unsafe { &mut *(dtvcc_ptr as *mut DtvccRust) }; + dtvcc.process_cc_data(cc_valid, cc_type, data1, data2); +} + +/// Flush all active service decoders in the DtvccRust context. +/// +/// This writes out any pending caption data from all active services. +/// Should be called when processing is complete or when switching contexts. +/// +/// # Safety +/// - `dtvcc_ptr` must be a valid pointer returned by `ccxr_dtvcc_init()` +/// - It is safe to call with a null pointer (no-op) +#[no_mangle] +pub extern "C" fn ccxr_flush_active_decoders(dtvcc_ptr: *mut std::ffi::c_void) { + if dtvcc_ptr.is_null() { + return; + } + let dtvcc = unsafe { &mut *(dtvcc_ptr as *mut DtvccRust) }; + dtvcc.flush_active_decoders(); +} + +/// Check if the DtvccRust context is active. +/// +/// # Safety +/// - `dtvcc_ptr` must be a valid pointer returned by `ccxr_dtvcc_init()` +/// +/// # Returns +/// 1 if active, 0 if not active or if pointer is null. +#[no_mangle] +pub extern "C" fn ccxr_dtvcc_is_active(dtvcc_ptr: *mut std::ffi::c_void) -> i32 { + if dtvcc_ptr.is_null() { + return 0; + } + let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) }; + if dtvcc.is_active { + 1 + } else { + 0 + } +} + /// Process cc_data /// /// # Safety /// dec_ctx should not be a null pointer /// data should point to cc_data of length cc_count +/// dec_ctx.dtvcc_rust must point to a valid DtvccRust instance #[no_mangle] extern "C" fn ccxr_process_cc_data( dec_ctx: *mut lib_cc_decode, @@ -228,22 +392,22 @@ extern "C" fn ccxr_process_cc_data( let dec_ctx = unsafe { &mut *dec_ctx }; - // Check dtvcc pointer before dereferencing - if dec_ctx.dtvcc.is_null() { - return -1; - } - let mut ret = -1; let mut cc_data: Vec = (0..cc_count * 3) .map(|x| unsafe { *data.add(x as usize) }) .collect(); - let dtvcc_ctx = unsafe { &mut *dec_ctx.dtvcc }; - let mut dtvcc = Dtvcc::new(dtvcc_ctx); + // Use the persistent DtvccRust context from dtvcc_rust + let dtvcc_rust = dec_ctx.dtvcc_rust as *mut DtvccRust; + if dtvcc_rust.is_null() { + warn!("ccxr_process_cc_data: dtvcc_rust is null"); + return ret; + } + let dtvcc = unsafe { &mut *dtvcc_rust }; for cc_block in cc_data.chunks_exact_mut(3) { if !validate_cc_pair(cc_block) { continue; } - let success = do_cb_dtvcc(dec_ctx, &mut dtvcc, cc_block); + let success = do_cb_dtvcc_rust(dec_ctx, dtvcc, cc_block); if success { ret = 0; } @@ -286,7 +450,7 @@ pub fn verify_parity(data: u8) -> bool { false } -/// Process CC data according to its type +/// Process CC data according to its type (using Dtvcc) pub fn do_cb_dtvcc(ctx: &mut lib_cc_decode, dtvcc: &mut Dtvcc, cc_block: &[u8]) -> bool { let cc_valid = (cc_block[0] & 4) >> 2; let cc_type = cc_block[0] & 3; @@ -337,6 +501,59 @@ pub fn do_cb_dtvcc(ctx: &mut lib_cc_decode, dtvcc: &mut Dtvcc, cc_block: &[u8]) true } +/// Process CC data according to its type (using DtvccRust - persistent context) +pub fn do_cb_dtvcc_rust(ctx: &mut lib_cc_decode, dtvcc: &mut DtvccRust, cc_block: &[u8]) -> bool { + let cc_valid = (cc_block[0] & 4) >> 2; + let cc_type = cc_block[0] & 3; + let mut timeok = true; + + if ctx.write_format != ccx_output_format::CCX_OF_DVDRAW + && ctx.write_format != ccx_output_format::CCX_OF_RAW + && (cc_block[0] == 0xFA || cc_block[0] == 0xFC || cc_block[0] == 0xFD) + && (cc_block[1] & 0x7F) == 0 + && (cc_block[2] & 0x7F) == 0 + { + return true; + } + + if cc_valid == 1 || cc_type == 3 { + ctx.cc_stats[cc_type as usize] += 1; + match cc_type { + // Type 0 and 1 are for CEA-608 data. Handled by C code, do nothing + 0 | 1 => {} + // Type 2 and 3 are for CEA-708 data. + 2 | 3 => { + let current_time = if ctx.timing.is_null() { + 0 + } else { + unsafe { (*ctx.timing).get_fts(ctx.current_field as u8) } + }; + ctx.current_field = 3; + + // Check whether current time is within start and end bounds + if is_true(ctx.extraction_start.set) + && current_time < ctx.extraction_start.time_in_ms + { + timeok = false; + } + if is_true(ctx.extraction_end.set) && current_time > ctx.extraction_end.time_in_ms { + timeok = false; + ctx.processed_enough = 1; + } + + if timeok && ctx.write_format != ccx_output_format::CCX_OF_RAW { + dtvcc.process_cc_data(cc_valid, cc_type, cc_block[1], cc_block[2]); + } + // Note: cb_708 is incremented by the C code in do_cb(), not here. + // Previously incrementing here caused a double-increment bug that + // resulted in incorrect start timestamps. + } + _ => warn!("Invalid cc_type"), + } + } + true +} + /// Close a Windows handle by wrapping it in a File and dropping it. /// /// # Safety