mirror of
https://github.com/CCExtractor/ccextractor.git
synced 2026-07-08 18:06:30 +00:00
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:
@@ -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};
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
476
src/rust/src/demuxer/dvdraw.rs
Normal file
476
src/rust/src/demuxer/dvdraw.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -38,4 +38,5 @@
|
||||
pub mod common_types;
|
||||
pub mod demux;
|
||||
pub mod demuxer_data;
|
||||
pub mod dvdraw;
|
||||
pub mod stream_functions;
|
||||
|
||||
@@ -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")]
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user