fix: McPoodle DVD raw format read/write (Issue #1524)

Reading:
- Migrate DVD raw parser from C to Rust (src/rust/src/demuxer/dvdraw.rs)
- Add FFI exports: ccxr_process_dvdraw(), ccxr_is_dvdraw_header()
- Handle both McPoodle's single-byte and legacy 2-byte loop markers
- Add 15 unit tests covering all edge cases

Writing:
- Fix LC3/LC4 constants from 2-byte to 1-byte to match McPoodle's format
- Output files now have identical size to McPoodle's original

Fixes #1524

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Carlos Fernandez
2025-12-18 11:31:47 +01:00
parent 138ccd01c2
commit ac49bb5978
11 changed files with 694 additions and 45 deletions

View File

@@ -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};

View File

@@ -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];

View File

@@ -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;

View File

@@ -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);

View File

@@ -122,48 +122,84 @@ void writeDVDraw(const unsigned char *data1, int length1,
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)
if (datacount == 0)
{
writeraw(lc3, sizeof(lc3), 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);
}
if (loopcount > 3)
{
writeraw(lc4, sizeof(lc4), NULL, sub);
if (data2 && length2)
writeraw(data2, length2, 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)))
{
writeraw(lc6, sizeof(lc6), NULL, sub);
if (data2 && length2)
writeraw(data2, length2, NULL, sub);
waiting_for_field2 = 1;
return;
}
else
/* If we have data2 only (field 2), write fe + data2 */
if ((!data1 || !length1) && data2 && length2)
{
if (loopcount == 1)
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)
{
writeraw(lc6, sizeof(lc6), NULL, sub);
if (data2 && length2)
writeraw(data2, length2, NULL, sub);
loopcount++;
datacount = 0;
}
return;
}
/* If we have both data1 and data2 (legacy behavior, just in case) */
if (data1 && length1 && data2 && length2)
{
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)
{
loopcount++;
datacount = 0;
}
loopcount++;
datacount = 0;
}
}

View File

@@ -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)
{

View File

@@ -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];

View File

@@ -0,0 +1,476 @@
//! 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
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<F, T>(
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);
}
}

View File

@@ -38,4 +38,5 @@
pub mod common_types;
pub mod demux;
pub mod demuxer_data;
pub mod dvdraw;
pub mod stream_functions;

View File

@@ -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")]
{

View File

@@ -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 {