mirror of
https://github.com/CCExtractor/ccextractor.git
synced 2026-02-05 21:24:00 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1789dc6d4c | ||
|
|
e45b2528b7 | ||
|
|
a2a844da5c | ||
|
|
85263161cc | ||
|
|
473b3b4c46 | ||
|
|
a6596ad65b | ||
|
|
2180ab5bfa | ||
|
|
ad3b1decc6 | ||
|
|
693c3aaba7 | ||
|
|
e53921ef2c | ||
|
|
0f01679ca3 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -145,6 +145,9 @@ bazel*
|
||||
#Intellij IDEs
|
||||
.idea/
|
||||
|
||||
# Plans (local only)
|
||||
plans/
|
||||
|
||||
# Rust build and MakeFiles (and CMake files)
|
||||
src/rust/CMakeFiles/
|
||||
src/rust/CMakeCache.txt
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1050,7 +1050,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
|
||||
{
|
||||
@@ -1274,7 +1278,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
|
||||
{
|
||||
@@ -1484,7 +1492,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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
22
src/rust/Cargo.lock
generated
22
src/rust/Cargo.lock
generated
@@ -129,26 +129,6 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash 2.1.1",
|
||||
"shlex",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@@ -171,7 +151,7 @@ checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609"
|
||||
name = "ccx_rust"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bindgen 0.72.1",
|
||||
"bindgen 0.64.0",
|
||||
"cfg-if",
|
||||
"clap",
|
||||
"encoding_rs",
|
||||
|
||||
@@ -455,7 +455,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
|
||||
|
||||
@@ -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<i32>,
|
||||
pub report_enabled: bool,
|
||||
pub report: *mut ccx_decoder_dtvcc_report,
|
||||
pub decoders: [Option<Box<dtvcc_service_decoder>>; DTVCC_MAX_SERVICES],
|
||||
pub packet: Vec<u8>,
|
||||
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<Box<dtvcc_service_decoder>> = 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::<dtvcc_tv_screen>();
|
||||
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::<dtvcc_service_decoder>();
|
||||
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<ccx_decoder_dtvcc_settings> {
|
||||
let mut settings = get_zero_allocated_obj::<ccx_decoder_dtvcc_settings>();
|
||||
|
||||
// Initialize required pointers using heap allocation
|
||||
let report = get_zero_allocated_obj::<ccx_decoder_dtvcc_report>();
|
||||
settings.report = Box::into_raw(report);
|
||||
|
||||
let timing = get_zero_allocated_obj::<ccx_common_timing_ctx>();
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +392,9 @@ 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() {
|
||||
// Check dtvcc_rust pointer before dereferencing (not dtvcc!)
|
||||
// When Rust is enabled, dtvcc is NULL and dtvcc_rust holds the actual context
|
||||
if dec_ctx.dtvcc_rust.is_null() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -237,13 +402,19 @@ extern "C" fn ccxr_process_cc_data(
|
||||
let mut cc_data: Vec<u8> = (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;
|
||||
}
|
||||
@@ -310,7 +481,7 @@ pub fn verify_parity(data: u8) -> bool {
|
||||
/// This isn't related to the "solid blank" character - it's just that the mask happens to have the same value.
|
||||
const PARITY_BIT_MASK: u8 = 0x7F;
|
||||
|
||||
/// 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;
|
||||
@@ -361,6 +532,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] & PARITY_BIT_MASK) == 0
|
||||
&& (cc_block[2] & PARITY_BIT_MASK) == 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
|
||||
@@ -654,4 +878,349 @@ mod test {
|
||||
// Double dash alone (end of options marker)
|
||||
assert_eq!(normalize_legacy_option("--".to_string()), "--".to_string());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// FFI Integration Tests
|
||||
// =========================================================================
|
||||
//
|
||||
// These tests verify the FFI boundary - the extern "C" functions that are
|
||||
// called from C code. They test the actual C→Rust call path with realistic
|
||||
// struct states.
|
||||
|
||||
mod ffi_integration_tests {
|
||||
use super::*;
|
||||
use crate::decoder::test::create_test_dtvcc_settings;
|
||||
use crate::utils::get_zero_allocated_obj;
|
||||
|
||||
/// Helper to create a lib_cc_decode struct configured for Rust-enabled path
|
||||
/// (dtvcc=NULL, dtvcc_rust=valid pointer)
|
||||
fn create_rust_enabled_decode_ctx() -> (Box<lib_cc_decode>, *mut std::ffi::c_void) {
|
||||
let mut ctx = get_zero_allocated_obj::<lib_cc_decode>();
|
||||
|
||||
// Create the DtvccRust context via the FFI function
|
||||
let settings = create_test_dtvcc_settings();
|
||||
let dtvcc_rust = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
|
||||
|
||||
// Set up the timing context (required for processing)
|
||||
let timing = get_zero_allocated_obj::<ccx_common_timing_ctx>();
|
||||
ctx.timing = Box::into_raw(timing);
|
||||
|
||||
// Simulate Rust-enabled mode: dtvcc is NULL, dtvcc_rust is set
|
||||
ctx.dtvcc = std::ptr::null_mut();
|
||||
ctx.dtvcc_rust = dtvcc_rust;
|
||||
|
||||
(ctx, dtvcc_rust)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// ccxr_dtvcc_init / ccxr_dtvcc_free lifecycle tests
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_ffi_dtvcc_init_creates_valid_context() {
|
||||
let settings = create_test_dtvcc_settings();
|
||||
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
|
||||
|
||||
// Should return a valid (non-null) pointer
|
||||
assert!(!dtvcc_ptr.is_null());
|
||||
|
||||
// Verify we can check if it's active
|
||||
let is_active = unsafe { ccxr_dtvcc_is_active(dtvcc_ptr) };
|
||||
assert_eq!(is_active, 1);
|
||||
|
||||
// Clean up
|
||||
ccxr_dtvcc_free(dtvcc_ptr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ffi_dtvcc_init_with_null_returns_null() {
|
||||
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(std::ptr::null()) };
|
||||
assert!(dtvcc_ptr.is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ffi_dtvcc_free_with_null_is_safe() {
|
||||
// Should not crash when called with null
|
||||
ccxr_dtvcc_free(std::ptr::null_mut());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ffi_dtvcc_is_active_with_null_returns_zero() {
|
||||
let result = unsafe { ccxr_dtvcc_is_active(std::ptr::null_mut()) };
|
||||
assert_eq!(result, 0);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// ccxr_dtvcc_set_encoder tests
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_ffi_set_encoder_with_valid_context() {
|
||||
let settings = create_test_dtvcc_settings();
|
||||
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
|
||||
|
||||
// Create an encoder
|
||||
let encoder = Box::new(encoder_ctx::default());
|
||||
let encoder_ptr = Box::into_raw(encoder);
|
||||
|
||||
// Set the encoder
|
||||
unsafe { ccxr_dtvcc_set_encoder(dtvcc_ptr, encoder_ptr) };
|
||||
|
||||
// Verify by checking the internal state
|
||||
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
|
||||
assert_eq!(dtvcc.encoder, encoder_ptr);
|
||||
|
||||
// Clean up
|
||||
ccxr_dtvcc_free(dtvcc_ptr);
|
||||
unsafe { drop(Box::from_raw(encoder_ptr)) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ffi_set_encoder_with_null_context_is_safe() {
|
||||
let encoder = Box::new(encoder_ctx::default());
|
||||
let encoder_ptr = Box::into_raw(encoder);
|
||||
|
||||
// Should not crash
|
||||
unsafe { ccxr_dtvcc_set_encoder(std::ptr::null_mut(), encoder_ptr) };
|
||||
|
||||
// Clean up
|
||||
unsafe { drop(Box::from_raw(encoder_ptr)) };
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// ccxr_dtvcc_process_data tests
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_ffi_process_data_packet_start() {
|
||||
let settings = create_test_dtvcc_settings();
|
||||
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
|
||||
|
||||
// Process a packet start (cc_type = 3)
|
||||
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 3, 0xC2, 0x00) };
|
||||
|
||||
// Verify state changed
|
||||
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
|
||||
assert!(dtvcc.is_header_parsed);
|
||||
assert_eq!(dtvcc.packet_length, 2);
|
||||
|
||||
// Clean up
|
||||
ccxr_dtvcc_free(dtvcc_ptr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ffi_process_data_with_null_is_safe() {
|
||||
// Should not crash
|
||||
unsafe { ccxr_dtvcc_process_data(std::ptr::null_mut(), 1, 3, 0xC2, 0x00) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ffi_process_data_state_persists_across_calls() {
|
||||
// This is THE key test - verifying the fix for issue #1499
|
||||
let settings = create_test_dtvcc_settings();
|
||||
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
|
||||
|
||||
// First call: start a packet (packet length = 8 bytes)
|
||||
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 3, 0xC4, 0x00) };
|
||||
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
|
||||
assert!(dtvcc.is_header_parsed);
|
||||
assert_eq!(dtvcc.packet_length, 2);
|
||||
|
||||
// Second call: add more data (cc_type = 2)
|
||||
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x21, 0x00) };
|
||||
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
|
||||
assert_eq!(dtvcc.packet_length, 4);
|
||||
|
||||
// Third call: add more data
|
||||
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x00, 0x00) };
|
||||
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
|
||||
assert_eq!(dtvcc.packet_length, 6);
|
||||
|
||||
// State persisted across all calls!
|
||||
assert!(dtvcc.is_header_parsed);
|
||||
|
||||
// Clean up
|
||||
ccxr_dtvcc_free(dtvcc_ptr);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// ccxr_process_cc_data tests (the main FFI entry point from C)
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_ffi_ccxr_process_cc_data_with_null_ctx_returns_error() {
|
||||
let data: [u8; 3] = [0x97, 0x1F, 0x3C];
|
||||
let result = unsafe { ccxr_process_cc_data(std::ptr::null_mut(), data.as_ptr(), 1) };
|
||||
assert_eq!(result, -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ffi_ccxr_process_cc_data_with_null_data_returns_error() {
|
||||
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
|
||||
let result =
|
||||
unsafe { ccxr_process_cc_data(Box::into_raw(ctx), std::ptr::null(), 1) };
|
||||
assert_eq!(result, -1);
|
||||
|
||||
// Clean up
|
||||
ccxr_dtvcc_free(dtvcc_ptr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ffi_ccxr_process_cc_data_with_zero_count_returns_error() {
|
||||
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
|
||||
let data: [u8; 3] = [0x97, 0x1F, 0x3C];
|
||||
let result =
|
||||
unsafe { ccxr_process_cc_data(Box::into_raw(ctx), data.as_ptr(), 0) };
|
||||
assert_eq!(result, -1);
|
||||
|
||||
// Clean up
|
||||
ccxr_dtvcc_free(dtvcc_ptr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ffi_ccxr_process_cc_data_with_null_dtvcc_rust_returns_error() {
|
||||
let mut ctx = get_zero_allocated_obj::<lib_cc_decode>();
|
||||
|
||||
// Set up timing but leave dtvcc_rust as null
|
||||
let timing = get_zero_allocated_obj::<ccx_common_timing_ctx>();
|
||||
ctx.timing = Box::into_raw(timing);
|
||||
ctx.dtvcc_rust = std::ptr::null_mut();
|
||||
|
||||
let data: [u8; 3] = [0x97, 0x1F, 0x3C];
|
||||
let result =
|
||||
unsafe { ccxr_process_cc_data(Box::into_raw(ctx), data.as_ptr(), 1) };
|
||||
assert_eq!(result, -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ffi_ccxr_process_cc_data_processes_708_data() {
|
||||
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
|
||||
|
||||
// Set an encoder so processing can complete
|
||||
let encoder = get_zero_allocated_obj::<encoder_ctx>();
|
||||
ccxr_dtvcc_set_encoder(dtvcc_ptr, Box::into_raw(encoder));
|
||||
|
||||
// CEA-708 packet start (cc_type=3, cc_valid=1)
|
||||
// Header byte breakdown: cc_valid is bit 2, cc_type is bits 0-1
|
||||
// For cc_type=3 (bits 0-1 = 11) and cc_valid=1 (bit 2 = 1):
|
||||
// 0xFF = 11111111 -> cc_valid=1, cc_type=3
|
||||
let data: [u8; 3] = [0xFF, 0xC2, 0x00];
|
||||
let ctx_ptr = Box::into_raw(ctx);
|
||||
let result = ccxr_process_cc_data(ctx_ptr, data.as_ptr(), 1);
|
||||
|
||||
// Should return 0 (success) for valid 708 data
|
||||
assert_eq!(result, 0);
|
||||
|
||||
// Verify the context was updated
|
||||
let ctx = unsafe { &*ctx_ptr };
|
||||
assert_eq!(ctx.cc_stats[3], 1); // cc_type 3 was processed
|
||||
assert_eq!(ctx.current_field, 3); // Field set to 3 for 708 data
|
||||
|
||||
// Clean up
|
||||
ccxr_dtvcc_free(dtvcc_ptr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ffi_ccxr_process_cc_data_skips_invalid_pairs() {
|
||||
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
|
||||
|
||||
// Invalid pair (cc_valid = 0)
|
||||
// Header byte: 0xF9 = 11111001 -> cc_valid=0, cc_type=1
|
||||
let data: [u8; 3] = [0xF9, 0x00, 0x00];
|
||||
let ctx_ptr = Box::into_raw(ctx);
|
||||
let result = unsafe { ccxr_process_cc_data(ctx_ptr, data.as_ptr(), 1) };
|
||||
|
||||
// Should return -1 (no valid data processed)
|
||||
assert_eq!(result, -1);
|
||||
|
||||
// Clean up
|
||||
ccxr_dtvcc_free(dtvcc_ptr);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// ccxr_flush_active_decoders tests
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_ffi_flush_with_null_is_safe() {
|
||||
// Should not crash
|
||||
unsafe { ccxr_flush_active_decoders(std::ptr::null_mut()) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ffi_flush_with_valid_context() {
|
||||
let settings = create_test_dtvcc_settings();
|
||||
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
|
||||
|
||||
// Set an encoder (required for flushing)
|
||||
let encoder = Box::new(encoder_ctx::default());
|
||||
unsafe { ccxr_dtvcc_set_encoder(dtvcc_ptr, Box::into_raw(encoder)) };
|
||||
|
||||
// Should not crash
|
||||
unsafe { ccxr_flush_active_decoders(dtvcc_ptr) };
|
||||
|
||||
// Clean up
|
||||
ccxr_dtvcc_free(dtvcc_ptr);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Full lifecycle integration test
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_ffi_complete_lifecycle() {
|
||||
// This test simulates the complete lifecycle as it would be used from C code:
|
||||
// 1. Initialize context
|
||||
// 2. Set encoder
|
||||
// 3. Process multiple CC data packets
|
||||
// 4. Flush
|
||||
// 5. Free
|
||||
|
||||
// 1. Initialize
|
||||
let settings = create_test_dtvcc_settings();
|
||||
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
|
||||
assert!(!dtvcc_ptr.is_null());
|
||||
|
||||
// Verify initial state
|
||||
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
|
||||
assert!(dtvcc.is_active, "DtvccRust should be active after init");
|
||||
assert_eq!(dtvcc.packet_length, 0);
|
||||
assert!(!dtvcc.is_header_parsed);
|
||||
|
||||
// 2. Set encoder
|
||||
let encoder = get_zero_allocated_obj::<encoder_ctx>();
|
||||
let encoder_ptr = Box::into_raw(encoder);
|
||||
ccxr_dtvcc_set_encoder(dtvcc_ptr, encoder_ptr);
|
||||
|
||||
// 3. Process a packet start (cc_type=3) - this should set is_header_parsed
|
||||
// Use 0xC4 for packet header: 0xC4 & 0x3F = 4, so max_len = 4*2 = 8 bytes
|
||||
// This way the packet won't be completed until we've added enough data
|
||||
ccxr_dtvcc_process_data(dtvcc_ptr, 1, 3, 0xC4, 0x00);
|
||||
|
||||
// Verify packet start was processed
|
||||
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
|
||||
assert!(
|
||||
dtvcc.is_header_parsed,
|
||||
"is_header_parsed should be true after packet start"
|
||||
);
|
||||
assert_eq!(dtvcc.packet_length, 2, "packet_length should be 2");
|
||||
|
||||
// Process packet data (cc_type=2) - packet is not complete yet (need 8 bytes)
|
||||
ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x21, 0x00);
|
||||
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
|
||||
assert_eq!(dtvcc.packet_length, 4, "packet_length should be 4");
|
||||
|
||||
// Add more data
|
||||
ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x00, 0x00);
|
||||
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
|
||||
assert_eq!(dtvcc.packet_length, 6, "packet_length should be 6");
|
||||
|
||||
// 4. Flush
|
||||
ccxr_flush_active_decoders(dtvcc_ptr);
|
||||
|
||||
// 5. Free
|
||||
ccxr_dtvcc_free(dtvcc_ptr);
|
||||
unsafe { drop(Box::from_raw(encoder_ptr)) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user