diff --git a/src/lib_ccx/ccx_common_constants.c b/src/lib_ccx/ccx_common_constants.c index a8417d211..fd2fc1a68 100644 --- a/src/lib_ccx/ccx_common_constants.c +++ b/src/lib_ccx/ccx_common_constants.c @@ -17,8 +17,8 @@ const unsigned char UTF8_BOM[] = {0xef, 0xbb, 0xbf}; const unsigned char DVD_HEADER[8] = {0x00, 0x00, 0x01, 0xb2, 0x43, 0x43, 0x01, 0xf8}; const unsigned char lc1[1] = {0x8a}; const unsigned char lc2[1] = {0x8f}; -const unsigned char lc3[2] = {0x16, 0xfe}; -const unsigned char lc4[2] = {0x1e, 0xfe}; +const unsigned char lc3[1] = {0x16}; // McPoodle uses single-byte loop markers +const unsigned char lc4[1] = {0x1e}; const unsigned char lc5[1] = {0xff}; const unsigned char lc6[1] = {0xfe}; diff --git a/src/lib_ccx/ccx_common_constants.h b/src/lib_ccx/ccx_common_constants.h index b8aad3325..9e554b0f9 100644 --- a/src/lib_ccx/ccx_common_constants.h +++ b/src/lib_ccx/ccx_common_constants.h @@ -22,8 +22,8 @@ extern const unsigned char UTF8_BOM[3]; extern const unsigned char DVD_HEADER[8]; extern const unsigned char lc1[1]; extern const unsigned char lc2[1]; -extern const unsigned char lc3[2]; -extern const unsigned char lc4[2]; +extern const unsigned char lc3[1]; +extern const unsigned char lc4[1]; extern const unsigned char lc5[1]; extern const unsigned char lc6[1]; diff --git a/src/lib_ccx/general_loop.c b/src/lib_ccx/general_loop.c index 1be5eedf4..459063ba7 100644 --- a/src/lib_ccx/general_loop.c +++ b/src/lib_ccx/general_loop.c @@ -529,6 +529,7 @@ int raw_loop(struct lib_ccx_ctx *ctx) struct encoder_ctx *enc_ctx = update_encoder_list(ctx); struct lib_cc_decode *dec_ctx = NULL; int caps = 0; + int is_dvdraw = 0; // Flag to track if this is DVD raw format dec_ctx = update_decoder_list(ctx); dec_sub = &dec_ctx->dec_sub; @@ -545,7 +546,26 @@ int raw_loop(struct lib_ccx_ctx *ctx) if (ret == CCX_EOF) break; - ret = process_raw(dec_ctx, dec_sub, data->buffer, data->len); + // Check if this is DVD raw format using Rust detection + if (!is_dvdraw && ccxr_is_dvdraw_header(data->buffer, (unsigned int)data->len)) + { + is_dvdraw = 1; + mprint("Detected McPoodle's DVD raw format\n"); + } + + if (is_dvdraw) + { + // Use Rust implementation - handles timing internally + ret = ccxr_process_dvdraw(dec_ctx, dec_sub, data->buffer, (unsigned int)data->len); + } + else + { + ret = process_raw(dec_ctx, dec_sub, data->buffer, data->len); + // For regular raw format, advance timing based on field 1 blocks + add_current_pts(dec_ctx->timing, cb_field1 * 1001 / 30 * (MPEG_CLOCK_FREQ / 1000)); + set_fts(dec_ctx->timing); + } + if (dec_sub->got_output) { caps = 1; @@ -553,10 +573,6 @@ int raw_loop(struct lib_ccx_ctx *ctx) dec_sub->got_output = 0; } - // int ccblocks = cb_field1; - add_current_pts(dec_ctx->timing, cb_field1 * 1001 / 30 * (MPEG_CLOCK_FREQ / 1000)); - set_fts(dec_ctx->timing); // Now set the FTS related variables including fts_max - } while (data->len); free(data); return caps; @@ -618,6 +634,11 @@ size_t process_raw(struct lib_cc_decode *ctx, struct cc_subtitle *sub, unsigned return len; } +/* NOTE: process_dvdraw() has been migrated to Rust. + * The implementation is now in src/rust/src/demuxer/dvdraw.rs + * and exported via ccxr_process_dvdraw() in src/rust/src/libccxr_exports/demuxer.rs + */ + void delete_datalist(struct demuxer_data *list) { struct demuxer_data *slist = list; diff --git a/src/lib_ccx/lib_ccx.h b/src/lib_ccx/lib_ccx.h index f0a99c6b3..0695c9ff3 100644 --- a/src/lib_ccx/lib_ccx.h +++ b/src/lib_ccx/lib_ccx.h @@ -172,6 +172,11 @@ int ps_get_more_data(struct lib_ccx_ctx *ctx, struct demuxer_data **ppdata); int general_get_more_data(struct lib_ccx_ctx *ctx, struct demuxer_data **data); int raw_loop(struct lib_ccx_ctx *ctx); size_t process_raw(struct lib_cc_decode *ctx, struct cc_subtitle *sub, unsigned char *buffer, size_t len); + +// Rust FFI: McPoodle DVD raw format processing (see src/rust/src/demuxer/dvdraw.rs) +unsigned int ccxr_process_dvdraw(struct lib_cc_decode *ctx, struct cc_subtitle *sub, const unsigned char *buffer, unsigned int len); +int ccxr_is_dvdraw_header(const unsigned char *buffer, unsigned int len); + int general_loop(struct lib_ccx_ctx *ctx); void process_hex(struct lib_ccx_ctx *ctx, char *filename); int rcwt_loop(struct lib_ccx_ctx *ctx); diff --git a/src/lib_ccx/output.c b/src/lib_ccx/output.c index fcda3940f..7cab09691 100644 --- a/src/lib_ccx/output.c +++ b/src/lib_ccx/output.c @@ -119,51 +119,87 @@ void writeDVDraw(const unsigned char *data1, int length1, struct cc_subtitle *sub) { /* these are only used by DVD raw mode: */ - static int loopcount = 1; /* loop 1: 5 elements, loop 2: 8 elements, - loop 3: 11 elements, rest: 15 elements */ - static int datacount = 0; /* counts within loop */ + static int loopcount = 1; /* loop 1: 5 elements, loop 2: 8 elements, + loop 3: 11 elements, rest: 15 elements */ + static int datacount = 0; /* counts within loop */ + static int waiting_for_field2 = 0; /* track if we're waiting for field 2 data */ - if (datacount == 0) + /* printdata() is called separately for field 1 and field 2 data. + * Field 1: data1 set, data2 NULL + * Field 2: data1 NULL, data2 set + * We need to combine them into the DVD raw format: ff [d1] fe [d2] + */ + + /* If we have data1 only (field 1), write ff + data1 and wait for field 2 */ + if (data1 && length1 && (!data2 || !length2)) { - writeraw(DVD_HEADER, sizeof(DVD_HEADER), NULL, sub); - if (loopcount == 1) - writeraw(lc1, sizeof(lc1), NULL, sub); - if (loopcount == 2) - writeraw(lc2, sizeof(lc2), NULL, sub); - if (loopcount == 3) - { - writeraw(lc3, sizeof(lc3), NULL, sub); - if (data2 && length2) - writeraw(data2, length2, NULL, sub); - } - if (loopcount > 3) + if (datacount == 0) { - writeraw(lc4, sizeof(lc4), NULL, sub); - if (data2 && length2) - writeraw(data2, length2, NULL, sub); + writeraw(DVD_HEADER, sizeof(DVD_HEADER), NULL, sub); + if (loopcount == 1) + writeraw(lc1, sizeof(lc1), NULL, sub); + else if (loopcount == 2) + writeraw(lc2, sizeof(lc2), NULL, sub); + else if (loopcount == 3) + writeraw(lc3, sizeof(lc3), NULL, sub); + else + writeraw(lc4, sizeof(lc4), NULL, sub); } - } - datacount++; - writeraw(lc5, sizeof(lc5), NULL, sub); - if (data1 && length1) + writeraw(lc5, sizeof(lc5), NULL, sub); /* ff */ writeraw(data1, length1, NULL, sub); - if (((loopcount == 1) && (datacount < 5)) || ((loopcount == 2) && (datacount < 8)) || ((loopcount == 3) && (datacount < 11)) || - ((loopcount > 3) && (datacount < 15))) + waiting_for_field2 = 1; + return; + } + + /* If we have data2 only (field 2), write fe + data2 */ + if ((!data1 || !length1) && data2 && length2) { - writeraw(lc6, sizeof(lc6), NULL, sub); - if (data2 && length2) - writeraw(data2, length2, NULL, sub); + writeraw(lc6, sizeof(lc6), NULL, sub); /* fe */ + writeraw(data2, length2, NULL, sub); + waiting_for_field2 = 0; + datacount++; + + /* Check if we've completed a loop */ + int max_count = (loopcount == 1) ? 5 : (loopcount == 2) ? 8 + : (loopcount == 3) ? 11 + : 15; + if (datacount >= max_count) + { + loopcount++; + datacount = 0; + } + return; } - else + + /* If we have both data1 and data2 (legacy behavior, just in case) */ + if (data1 && length1 && data2 && length2) { - if (loopcount == 1) + if (datacount == 0) + { + writeraw(DVD_HEADER, sizeof(DVD_HEADER), NULL, sub); + if (loopcount == 1) + writeraw(lc1, sizeof(lc1), NULL, sub); + else if (loopcount == 2) + writeraw(lc2, sizeof(lc2), NULL, sub); + else if (loopcount == 3) + writeraw(lc3, sizeof(lc3), NULL, sub); + else + writeraw(lc4, sizeof(lc4), NULL, sub); + } + writeraw(lc5, sizeof(lc5), NULL, sub); /* ff */ + writeraw(data1, length1, NULL, sub); + writeraw(lc6, sizeof(lc6), NULL, sub); /* fe */ + writeraw(data2, length2, NULL, sub); + datacount++; + + int max_count = (loopcount == 1) ? 5 : (loopcount == 2) ? 8 + : (loopcount == 3) ? 11 + : 15; + if (datacount >= max_count) { - writeraw(lc6, sizeof(lc6), NULL, sub); - if (data2 && length2) - writeraw(data2, length2, NULL, sub); + loopcount++; + datacount = 0; } - loopcount++; - datacount = 0; } } diff --git a/src/lib_ccx/stream_functions.c b/src/lib_ccx/stream_functions.c index 14d2372e8..6f0a7402e 100644 --- a/src/lib_ccx/stream_functions.c +++ b/src/lib_ccx/stream_functions.c @@ -65,6 +65,19 @@ void detect_stream_type(struct ccx_demuxer *ctx) ctx->startbytes[3] == 0x20) ctx->stream_mode = CCX_SM_WTV; } + // Check for McPoodle DVD raw format: 00 00 01 B2 43 43 ("CC") 01 F8 + if (ctx->stream_mode == CCX_SM_ELEMENTARY_OR_NOT_FOUND && ctx->startbytes_avail >= 8) + { + if (ctx->startbytes[0] == 0x00 && + ctx->startbytes[1] == 0x00 && + ctx->startbytes[2] == 0x01 && + ctx->startbytes[3] == 0xb2 && + ctx->startbytes[4] == 0x43 && // 'C' + ctx->startbytes[5] == 0x43 && // 'C' + ctx->startbytes[6] == 0x01 && + ctx->startbytes[7] == 0xf8) + ctx->stream_mode = CCX_SM_MCPOODLESRAW; + } #ifdef WTV_DEBUG if (ctx->stream_mode == CCX_SM_ELEMENTARY_OR_NOT_FOUND && ctx->startbytes_avail >= 6) { diff --git a/src/rust/lib_ccxr/src/common/constants.rs b/src/rust/lib_ccxr/src/common/constants.rs index d33d51d16..a93bf2399 100644 --- a/src/rust/lib_ccxr/src/common/constants.rs +++ b/src/rust/lib_ccxr/src/common/constants.rs @@ -66,8 +66,8 @@ pub const UTF8_BOM: [u8; 3] = [0xef, 0xbb, 0xbf]; pub const DVD_HEADER: [u8; 8] = [0x00, 0x00, 0x01, 0xb2, 0x43, 0x43, 0x01, 0xf8]; pub const LC1: [u8; 1] = [0x8a]; pub const LC2: [u8; 1] = [0x8f]; -pub const LC3: [u8; 2] = [0x16, 0xfe]; -pub const LC4: [u8; 2] = [0x1e, 0xfe]; +pub const LC3: [u8; 1] = [0x16]; // McPoodle uses single-byte loop markers +pub const LC4: [u8; 1] = [0x1e]; pub const LC5: [u8; 1] = [0xff]; pub const LC6: [u8; 1] = [0xfe]; diff --git a/src/rust/src/demuxer/dvdraw.rs b/src/rust/src/demuxer/dvdraw.rs new file mode 100644 index 000000000..86a98e81c --- /dev/null +++ b/src/rust/src/demuxer/dvdraw.rs @@ -0,0 +1,477 @@ +//! McPoodle DVD Raw format parser +//! +//! This module provides functionality to parse McPoodle's DVD raw caption format. +//! The format stores CEA-608 closed caption data in a specific binary structure. +//! +//! # Format Specification +//! +//! ```text +//! [DVD_HEADER][loop_marker][ff d1 d1 fe d2 d2]... +//! +//! DVD_HEADER: 00 00 01 B2 43 43 01 F8 (8 bytes, "CC" at bytes 4-5) +//! +//! Loop markers (vary by loop count): +//! - Loop 1: 8a (1 byte) - 5 caption pairs +//! - Loop 2: 8f (1 byte) - 8 caption pairs +//! - Loop 3: 16 or 16 fe (1-2 bytes) - 11 caption pairs +//! - Loop 4+: 1e or 1e fe (1-2 bytes) - 15 caption pairs +//! +//! Caption data: +//! - ff [byte1] [byte2] = Field 1 (CC1) data +//! - fe [byte1] [byte2] = Field 2 (CC2/XDS) data +//! ``` + +/// DVD raw format header: 00 00 01 B2 43 43 01 F8 +pub const DVD_HEADER: [u8; 8] = [0x00, 0x00, 0x01, 0xB2, 0x43, 0x43, 0x01, 0xF8]; + +/// Loop marker for loop 1 (5 caption pairs) +pub const LOOP_MARKER_1: u8 = 0x8A; + +/// Loop marker for loop 2 (8 caption pairs) +pub const LOOP_MARKER_2: u8 = 0x8F; + +/// Loop marker for loop 3+ (first byte) +pub const LOOP_MARKER_3_FIRST: u8 = 0x16; + +/// Loop marker for loop 4+ (first byte) +pub const LOOP_MARKER_4_FIRST: u8 = 0x1E; + +/// Second byte of 2-byte loop markers +pub const LOOP_MARKER_SECOND: u8 = 0xFE; + +/// Field 1 marker +pub const FIELD1_MARKER: u8 = 0xFF; + +/// Field 2 marker +pub const FIELD2_MARKER: u8 = 0xFE; + +/// Frame duration in 90kHz clock ticks (1001/30 * 90 = 3003 for 29.97fps NTSC) +pub const FRAME_DURATION_TICKS: i64 = 3003; + +/// Caption block type for Field 1 +pub const CC_TYPE_FIELD1: u8 = 0x04; + +/// Caption block type for Field 2 +pub const CC_TYPE_FIELD2: u8 = 0x05; + +/// Result of parsing a DVD raw buffer +#[derive(Debug, Clone, PartialEq)] +pub struct DvdRawParseResult { + /// Caption blocks extracted (each is 3 bytes: type, data1, data2) + pub caption_blocks: Vec<[u8; 3]>, + /// Number of bytes consumed from the input + pub bytes_consumed: usize, +} + +/// Checks if the buffer starts with the DVD raw header +/// +/// # Arguments +/// * `buffer` - The byte buffer to check +/// +/// # Returns +/// `true` if the buffer starts with the DVD_HEADER signature +pub fn is_dvdraw_header(buffer: &[u8]) -> bool { + buffer.len() >= DVD_HEADER.len() && buffer[..DVD_HEADER.len()] == DVD_HEADER +} + +/// Checks if a byte is a 1-byte loop marker +fn is_single_byte_loop_marker(byte: u8) -> bool { + byte == LOOP_MARKER_1 || byte == LOOP_MARKER_2 +} + +/// Checks if bytes form a 2-byte loop marker +#[cfg(test)] +fn is_two_byte_loop_marker(byte1: u8, byte2: u8) -> bool { + (byte1 == LOOP_MARKER_3_FIRST || byte1 == LOOP_MARKER_4_FIRST) && byte2 == LOOP_MARKER_SECOND +} + +/// Parse McPoodle's DVD raw format and extract caption blocks. +/// +/// This function parses the DVD raw binary format and extracts individual +/// caption blocks that can be passed to the 608 decoder. Each caption block +/// is 3 bytes: [cc_type, data1, data2]. +/// +/// # Arguments +/// * `buffer` - The raw bytes to parse +/// +/// # Returns +/// A `DvdRawParseResult` containing the extracted caption blocks +/// +/// # Example +/// ``` +/// use ccx_rust::demuxer::dvdraw::{parse_dvdraw, DVD_HEADER, CC_TYPE_FIELD1}; +/// +/// let mut data = DVD_HEADER.to_vec(); +/// data.push(0x8A); // Loop marker 1 +/// data.extend_from_slice(&[0xFF, 0x80, 0x80]); // Field 1 padding +/// data.extend_from_slice(&[0xFE, 0x80, 0x80]); // Field 2 padding +/// +/// let result = parse_dvdraw(&data); +/// assert_eq!(result.caption_blocks.len(), 2); +/// assert_eq!(result.caption_blocks[0][0], CC_TYPE_FIELD1); +/// ``` +pub fn parse_dvdraw(buffer: &[u8]) -> DvdRawParseResult { + let mut caption_blocks = Vec::new(); + let mut i = 0; + let len = buffer.len(); + + while i < len { + // Check for DVD_HEADER + if i + 8 <= len && buffer[i..i + 8] == DVD_HEADER { + i += 8; // Skip header + + // Skip loop marker + // Note: McPoodle's format uses single-byte markers (0x16, 0x1E) while + // CCExtractor output may use 2-byte markers (0x16 0xFE, 0x1E 0xFE). + // To distinguish: if 0x16/0x1E is followed by 0xFE and then by non-marker + // data (not 0xFF or 0xFE), it's McPoodle's format where 0xFE is field 2 data. + if i < len { + if is_single_byte_loop_marker(buffer[i]) { + i += 1; // 1-byte marker (0x8A or 0x8F) + } else if buffer[i] == LOOP_MARKER_3_FIRST || buffer[i] == LOOP_MARKER_4_FIRST { + // Check if this is a 2-byte marker or McPoodle's single-byte format + // In 2-byte format: 0x16 0xFE followed by field marker (0xFF/0xFE) + // In McPoodle's format: 0x16 followed by 0xFE (field 2 data start) + if i + 2 < len + && buffer[i + 1] == LOOP_MARKER_SECOND + && (buffer[i + 2] == FIELD1_MARKER || buffer[i + 2] == FIELD2_MARKER) + { + // CCExtractor format: 2-byte marker followed by field marker + i += 2; + } else { + // McPoodle's format: single-byte marker, 0xFE is field 2 data + i += 1; + } + } + } + continue; + } + + // Look for ff marker (field 1 data follows) + if buffer[i] == FIELD1_MARKER && i + 3 <= len { + caption_blocks.push([CC_TYPE_FIELD1, buffer[i + 1], buffer[i + 2]]); + i += 3; + continue; + } + + // Look for fe marker (field 2 data follows) + if buffer[i] == FIELD2_MARKER && i + 3 <= len { + caption_blocks.push([CC_TYPE_FIELD2, buffer[i + 1], buffer[i + 2]]); + i += 3; + continue; + } + + // Unknown byte, skip it + i += 1; + } + + DvdRawParseResult { + caption_blocks, + bytes_consumed: i, + } +} + +/// Parse DVD raw format with timing callback. +/// +/// This is a higher-level function that parses the DVD raw format and calls +/// a callback for each caption block, advancing timing appropriately. +/// +/// # Arguments +/// * `buffer` - The raw bytes to parse +/// * `callback` - Callback function called for each caption block with (cc_type, data1, data2) +/// * `timing_callback` - Callback function called before each field 1 block to advance timing +/// +/// # Returns +/// The number of bytes consumed +pub fn parse_dvdraw_with_callbacks( + buffer: &[u8], + mut callback: F, + mut timing_callback: T, +) -> usize +where + F: FnMut(u8, u8, u8), + T: FnMut(), +{ + let mut i = 0; + let len = buffer.len(); + + while i < len { + // Check for DVD_HEADER + if i + 8 <= len && buffer[i..i + 8] == DVD_HEADER { + i += 8; // Skip header + + // Skip loop marker (same logic as parse_dvdraw) + if i < len { + if is_single_byte_loop_marker(buffer[i]) { + i += 1; // 1-byte marker (0x8A or 0x8F) + } else if buffer[i] == LOOP_MARKER_3_FIRST || buffer[i] == LOOP_MARKER_4_FIRST { + // Check if this is a 2-byte marker or McPoodle's single-byte format + if i + 2 < len + && buffer[i + 1] == LOOP_MARKER_SECOND + && (buffer[i + 2] == FIELD1_MARKER || buffer[i + 2] == FIELD2_MARKER) + { + // CCExtractor format: 2-byte marker followed by field marker + i += 2; + } else { + // McPoodle's format: single-byte marker + i += 1; + } + } + } + continue; + } + + // Look for ff marker (field 1 data follows) + if buffer[i] == FIELD1_MARKER && i + 3 <= len { + // Advance timing BEFORE processing this caption + timing_callback(); + callback(CC_TYPE_FIELD1, buffer[i + 1], buffer[i + 2]); + i += 3; + continue; + } + + // Look for fe marker (field 2 data follows) + if buffer[i] == FIELD2_MARKER && i + 3 <= len { + callback(CC_TYPE_FIELD2, buffer[i + 1], buffer[i + 2]); + i += 3; + continue; + } + + // Unknown byte, skip it + i += 1; + } + + i +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_dvdraw_header() { + assert!(is_dvdraw_header(&DVD_HEADER)); + assert!(is_dvdraw_header(&[ + 0x00, 0x00, 0x01, 0xB2, 0x43, 0x43, 0x01, 0xF8, 0x8A + ])); + assert!(!is_dvdraw_header(&[ + 0x00, 0x00, 0x01, 0xB2, 0x43, 0x43, 0x01 + ])); // Too short + assert!(!is_dvdraw_header(&[ + 0x00, 0x00, 0x01, 0xB2, 0x43, 0x43, 0x01, 0xF9 + ])); // Wrong byte + assert!(!is_dvdraw_header(&[0x47, 0x00, 0x00, 0x00])); // TS sync byte + } + + #[test] + fn test_parse_empty_buffer() { + let result = parse_dvdraw(&[]); + assert!(result.caption_blocks.is_empty()); + assert_eq!(result.bytes_consumed, 0); + } + + #[test] + fn test_parse_header_only() { + let result = parse_dvdraw(&DVD_HEADER); + assert!(result.caption_blocks.is_empty()); + assert_eq!(result.bytes_consumed, 8); + } + + #[test] + fn test_parse_single_caption_pair_loop1() { + // DVD_HEADER + loop marker 1 + field 1 + field 2 + let mut data = DVD_HEADER.to_vec(); + data.push(LOOP_MARKER_1); // 0x8A + data.extend_from_slice(&[FIELD1_MARKER, 0x94, 0x2C]); // Field 1: RCL command + data.extend_from_slice(&[FIELD2_MARKER, 0x80, 0x80]); // Field 2: padding + + let result = parse_dvdraw(&data); + assert_eq!(result.caption_blocks.len(), 2); + assert_eq!(result.caption_blocks[0], [CC_TYPE_FIELD1, 0x94, 0x2C]); + assert_eq!(result.caption_blocks[1], [CC_TYPE_FIELD2, 0x80, 0x80]); + } + + #[test] + fn test_parse_single_caption_pair_loop2() { + let mut data = DVD_HEADER.to_vec(); + data.push(LOOP_MARKER_2); // 0x8F + data.extend_from_slice(&[FIELD1_MARKER, 0x80, 0x80]); + data.extend_from_slice(&[FIELD2_MARKER, 0x80, 0x80]); + + let result = parse_dvdraw(&data); + assert_eq!(result.caption_blocks.len(), 2); + } + + #[test] + fn test_parse_two_byte_loop_marker() { + // Loop marker 3: 0x16 0xFE + let mut data = DVD_HEADER.to_vec(); + data.extend_from_slice(&[LOOP_MARKER_3_FIRST, LOOP_MARKER_SECOND]); // 0x16 0xFE + data.extend_from_slice(&[FIELD1_MARKER, 0x45, 0x4C]); // "EL" + data.extend_from_slice(&[FIELD2_MARKER, 0x80, 0x80]); + + let result = parse_dvdraw(&data); + assert_eq!(result.caption_blocks.len(), 2); + assert_eq!(result.caption_blocks[0], [CC_TYPE_FIELD1, 0x45, 0x4C]); + } + + #[test] + fn test_parse_mcpoodle_format_loop3() { + // McPoodle's format: loop marker 0x16 followed directly by field 2 marker + // This tests the case where 0xFE is NOT part of loop marker but is field 2 data + let mut data = DVD_HEADER.to_vec(); + data.push(LOOP_MARKER_3_FIRST); // 0x16 (single byte marker) + data.extend_from_slice(&[FIELD2_MARKER, 0x80, 0x80]); // Field 2 first (McPoodle convention) + data.extend_from_slice(&[FIELD1_MARKER, 0x80, 0x80]); // Field 1 second + + let result = parse_dvdraw(&data); + assert_eq!(result.caption_blocks.len(), 2); + // McPoodle format has field 2 first after loop 3 marker + assert_eq!(result.caption_blocks[0], [CC_TYPE_FIELD2, 0x80, 0x80]); + assert_eq!(result.caption_blocks[1], [CC_TYPE_FIELD1, 0x80, 0x80]); + } + + #[test] + fn test_parse_multiple_headers() { + // Two headers with caption data + let mut data = DVD_HEADER.to_vec(); + data.push(LOOP_MARKER_1); + data.extend_from_slice(&[FIELD1_MARKER, 0x80, 0x80]); + data.extend_from_slice(&[FIELD2_MARKER, 0x80, 0x80]); + // Second header + data.extend_from_slice(&DVD_HEADER); + data.push(LOOP_MARKER_2); + data.extend_from_slice(&[FIELD1_MARKER, 0x94, 0x20]); // RCL + data.extend_from_slice(&[FIELD2_MARKER, 0x80, 0x80]); + + let result = parse_dvdraw(&data); + assert_eq!(result.caption_blocks.len(), 4); + assert_eq!(result.caption_blocks[2], [CC_TYPE_FIELD1, 0x94, 0x20]); + } + + #[test] + fn test_parse_with_callbacks() { + let mut data = DVD_HEADER.to_vec(); + data.push(LOOP_MARKER_1); + data.extend_from_slice(&[FIELD1_MARKER, 0x80, 0x80]); + data.extend_from_slice(&[FIELD2_MARKER, 0x80, 0x80]); + data.extend_from_slice(&[FIELD1_MARKER, 0x94, 0x2C]); + data.extend_from_slice(&[FIELD2_MARKER, 0x80, 0x80]); + + let mut caption_count = 0; + let mut timing_count = 0; + + parse_dvdraw_with_callbacks( + &data, + |_cc_type, _d1, _d2| { + caption_count += 1; + }, + || { + timing_count += 1; + }, + ); + + assert_eq!(caption_count, 4); // 2 field 1 + 2 field 2 + assert_eq!(timing_count, 2); // Only called for field 1 blocks + } + + #[test] + fn test_parse_real_caption_data() { + // Simulate real caption data: "KISSES DELUXE CHOCOLATES" + // This tests parsing actual CC commands + let mut data = DVD_HEADER.to_vec(); + data.push(LOOP_MARKER_1); + // EDM - Erase Displayed Memory + data.extend_from_slice(&[FIELD1_MARKER, 0x94, 0x2C]); + data.extend_from_slice(&[FIELD2_MARKER, 0x80, 0x80]); + // RCL - Resume Caption Loading + data.extend_from_slice(&[FIELD1_MARKER, 0x94, 0x20]); + data.extend_from_slice(&[FIELD2_MARKER, 0x80, 0x80]); + // Text: "KI" + data.extend_from_slice(&[FIELD1_MARKER, 0x4B, 0x49]); + data.extend_from_slice(&[FIELD2_MARKER, 0x80, 0x80]); + + let result = parse_dvdraw(&data); + assert_eq!(result.caption_blocks.len(), 6); + + // Verify EDM command + assert_eq!(result.caption_blocks[0], [CC_TYPE_FIELD1, 0x94, 0x2C]); + // Verify RCL command + assert_eq!(result.caption_blocks[2], [CC_TYPE_FIELD1, 0x94, 0x20]); + // Verify "KI" text + assert_eq!(result.caption_blocks[4], [CC_TYPE_FIELD1, 0x4B, 0x49]); + } + + #[test] + fn test_parse_padding_only() { + // File with only padding data (0x80 0x80) + let mut data = DVD_HEADER.to_vec(); + data.push(LOOP_MARKER_1); + for _ in 0..5 { + data.extend_from_slice(&[FIELD1_MARKER, 0x80, 0x80]); + data.extend_from_slice(&[FIELD2_MARKER, 0x80, 0x80]); + } + + let result = parse_dvdraw(&data); + assert_eq!(result.caption_blocks.len(), 10); + // All should be padding + for block in &result.caption_blocks { + assert_eq!(block[1], 0x80); + assert_eq!(block[2], 0x80); + } + } + + #[test] + fn test_constants() { + assert_eq!(DVD_HEADER.len(), 8); + assert_eq!(FRAME_DURATION_TICKS, 3003); + assert_eq!(CC_TYPE_FIELD1, 0x04); + assert_eq!(CC_TYPE_FIELD2, 0x05); + } + + #[test] + fn test_loop_marker_detection() { + assert!(is_single_byte_loop_marker(LOOP_MARKER_1)); + assert!(is_single_byte_loop_marker(LOOP_MARKER_2)); + assert!(!is_single_byte_loop_marker(LOOP_MARKER_3_FIRST)); + assert!(!is_single_byte_loop_marker(FIELD1_MARKER)); + + assert!(is_two_byte_loop_marker( + LOOP_MARKER_3_FIRST, + LOOP_MARKER_SECOND + )); + assert!(is_two_byte_loop_marker( + LOOP_MARKER_4_FIRST, + LOOP_MARKER_SECOND + )); + assert!(!is_two_byte_loop_marker(LOOP_MARKER_1, LOOP_MARKER_SECOND)); + assert!(!is_two_byte_loop_marker(LOOP_MARKER_3_FIRST, 0x00)); + } + + #[test] + fn test_incomplete_caption_at_end() { + // Buffer ends with incomplete caption data + let mut data = DVD_HEADER.to_vec(); + data.push(LOOP_MARKER_1); + data.extend_from_slice(&[FIELD1_MARKER, 0x80, 0x80]); // Complete + data.extend_from_slice(&[FIELD1_MARKER, 0x80]); // Incomplete - only 2 bytes + + let result = parse_dvdraw(&data); + assert_eq!(result.caption_blocks.len(), 1); // Only the complete one + } + + #[test] + fn test_garbage_data_handling() { + // Mix of valid and invalid data + let mut data = vec![0x00, 0x01, 0x02, 0x03]; // Garbage + data.extend_from_slice(&DVD_HEADER); + data.push(LOOP_MARKER_1); + data.extend_from_slice(&[FIELD1_MARKER, 0x80, 0x80]); + data.extend_from_slice(&[0x00, 0x01, 0x02]); // More garbage + data.extend_from_slice(&[FIELD2_MARKER, 0x80, 0x80]); + + let result = parse_dvdraw(&data); + // Should skip garbage and find the valid caption blocks + assert_eq!(result.caption_blocks.len(), 2); + } +} diff --git a/src/rust/src/demuxer/mod.rs b/src/rust/src/demuxer/mod.rs index 4d8dd6fe5..4af6af76d 100644 --- a/src/rust/src/demuxer/mod.rs +++ b/src/rust/src/demuxer/mod.rs @@ -38,4 +38,5 @@ pub mod common_types; pub mod demux; pub mod demuxer_data; +pub mod dvdraw; pub mod stream_functions; diff --git a/src/rust/src/demuxer/stream_functions.rs b/src/rust/src/demuxer/stream_functions.rs index c3c8e0789..012874d5c 100644 --- a/src/rust/src/demuxer/stream_functions.rs +++ b/src/rust/src/demuxer/stream_functions.rs @@ -159,6 +159,21 @@ unsafe fn detect_stream_type_common(ctx: &mut CcxDemuxer, ccx_options: &mut Opti ctx.stream_mode = StreamMode::Wtv; } + // Check for McPoodle DVD raw format: 00 00 01 B2 43 43 ("CC") 01 F8 + if ctx.stream_mode == StreamMode::ElementaryOrNotFound + && ctx.startbytes_avail >= 8 + && ctx.startbytes[0] == 0x00 + && ctx.startbytes[1] == 0x00 + && ctx.startbytes[2] == 0x01 + && ctx.startbytes[3] == 0xb2 + && ctx.startbytes[4] == 0x43 // 'C' + && ctx.startbytes[5] == 0x43 // 'C' + && ctx.startbytes[6] == 0x01 + && ctx.startbytes[7] == 0xf8 + { + ctx.stream_mode = StreamMode::McpoodlesRaw; + } + // Hex dump check #[cfg(feature = "wtv_debug")] { diff --git a/src/rust/src/libccxr_exports/demuxer.rs b/src/rust/src/libccxr_exports/demuxer.rs index 5cbb66034..53f9c8f31 100755 --- a/src/rust/src/libccxr_exports/demuxer.rs +++ b/src/rust/src/libccxr_exports/demuxer.rs @@ -9,7 +9,7 @@ use lib_ccxr::common::{Codec, Options, StreamMode, StreamType}; use lib_ccxr::time::Timestamp; use std::alloc::{alloc_zeroed, Layout}; use std::ffi::CStr; -use std::os::raw::{c_char, c_int, c_longlong, c_uchar, c_uint, c_void}; +use std::os::raw::{c_char, c_int, c_long, c_longlong, c_uchar, c_uint, c_void}; // External C function declarations extern "C" { @@ -467,6 +467,88 @@ pub unsafe extern "C" fn ccxr_demuxer_print_cfg(ctx: *mut ccx_demuxer) { demux_ctx.print_cfg() } +// ============================================================================ +// DVD Raw Format Processing (McPoodle format) +// ============================================================================ + +use crate::bindings::{cc_subtitle, ccx_common_timing_ctx, lib_cc_decode}; +use crate::demuxer::dvdraw::{is_dvdraw_header, parse_dvdraw_with_callbacks, FRAME_DURATION_TICKS}; + +// External C function declarations for caption processing +extern "C" { + fn do_cb(ctx: *mut lib_cc_decode, cc_block: *mut c_uchar, sub: *mut cc_subtitle) -> c_int; + fn ccxr_add_current_pts(ctx: *mut ccx_common_timing_ctx, pts: c_long); + fn ccxr_set_fts(ctx: *mut ccx_common_timing_ctx) -> c_int; +} + +/// Check if a buffer contains McPoodle DVD raw format header. +/// +/// # Safety +/// +/// `buffer` must be a valid pointer to at least `len` bytes. +#[no_mangle] +pub unsafe extern "C" fn ccxr_is_dvdraw_header(buffer: *const c_uchar, len: c_uint) -> c_int { + if buffer.is_null() || len < 8 { + return 0; + } + let slice = std::slice::from_raw_parts(buffer, len as usize); + if is_dvdraw_header(slice) { + 1 + } else { + 0 + } +} + +/// Process McPoodle's DVD raw format and extract caption blocks. +/// +/// This function parses the DVD raw binary format, extracts caption data, +/// advances timing appropriately, and calls do_cb() for each caption block. +/// +/// # Safety +/// +/// - `ctx` must be a valid pointer to a lib_cc_decode structure +/// - `sub` must be a valid pointer to a cc_subtitle structure +/// - `buffer` must be a valid pointer to at least `len` bytes +/// +/// # Returns +/// +/// The number of bytes consumed from the buffer. +#[no_mangle] +pub unsafe extern "C" fn ccxr_process_dvdraw( + ctx: *mut lib_cc_decode, + sub: *mut cc_subtitle, + buffer: *const c_uchar, + len: c_uint, +) -> c_uint { + if ctx.is_null() || sub.is_null() || buffer.is_null() || len == 0 { + return 0; + } + + let slice = std::slice::from_raw_parts(buffer, len as usize); + + // Get the timing context from lib_cc_decode + let timing_ctx = (*ctx).timing; + if timing_ctx.is_null() { + return 0; + } + + let bytes_consumed = parse_dvdraw_with_callbacks( + slice, + |cc_type, data1, data2| { + // Build caption block and call do_cb + let mut cc_block: [c_uchar; 3] = [cc_type, data1, data2]; + do_cb(ctx, cc_block.as_mut_ptr(), sub); + }, + || { + // Advance timing before each field 1 caption + ccxr_add_current_pts(timing_ctx, FRAME_DURATION_TICKS as c_long); + ccxr_set_fts(timing_ctx); + }, + ); + + bytes_consumed as c_uint +} + #[cfg(test)] #[allow(clippy::field_reassign_with_default)] mod tests {