diff --git a/src/lib_ccx/ccx_decoders_xds.c b/src/lib_ccx/ccx_decoders_xds.c index 14a6b23d0..8234f7210 100644 --- a/src/lib_ccx/ccx_decoders_xds.c +++ b/src/lib_ccx/ccx_decoders_xds.c @@ -4,6 +4,26 @@ #include "ccx_common_common.h" #include "utility.h" +#ifndef DISABLE_RUST +extern void ccxr_process_xds_bytes( + struct ccx_decoders_xds_context *ctx, + unsigned char hi, + unsigned char lo); + +extern void ccxr_do_end_of_xds( + struct cc_subtitle *sub, + struct ccx_decoders_xds_context *ctx, + unsigned char expected_checksum); + +extern struct ccx_decoders_xds_context *ccxr_ccx_decoders_xds_init_library( + struct ccx_common_timing_ctx *timing, + int xds_write_to_file); + +extern void ccxr_xds_cea608_test( + struct ccx_decoders_xds_context *ctx, + struct cc_subtitle *sub); +#endif + LLONG ts_start_of_xds = -1; // Time at which we switched to XDS mode, =-1 hasn't happened yet static const char *XDSclasses[] = @@ -80,6 +100,9 @@ static const char *XDSProgramTypes[] = struct ccx_decoders_xds_context *ccx_decoders_xds_init_library(struct ccx_common_timing_ctx *timing, int xds_write_to_file) { +#ifndef DISABLE_RUST + return ccxr_ccx_decoders_xds_init_library(timing, xds_write_to_file); // Use the Rust implementation +#else int i; struct ccx_decoders_xds_context *ctx = NULL; @@ -121,6 +144,7 @@ struct ccx_decoders_xds_context *ccx_decoders_xds_init_library(struct ccx_common ctx->xds_write_to_file = xds_write_to_file; return ctx; +#endif } int write_xds_string(struct cc_subtitle *sub, struct ccx_decoders_xds_context *ctx, char *p, size_t len) @@ -138,6 +162,7 @@ int write_xds_string(struct cc_subtitle *sub, struct ccx_decoders_xds_context *c { sub->data = data; sub->datatype = CC_DATATYPE_GENERIC; + data = (struct eia608_screen *)sub->data + sub->nb_data; data->format = SFORMAT_XDS; data->start_time = ts_start_of_xds; @@ -145,6 +170,7 @@ int write_xds_string(struct cc_subtitle *sub, struct ccx_decoders_xds_context *c data->xds_str = p; data->xds_len = len; data->cur_xds_packet_class = ctx->cur_xds_packet_class; + sub->nb_data++; sub->type = CC_608; sub->got_output = 1; @@ -203,6 +229,9 @@ void xds_debug_test(struct ccx_decoders_xds_context *ctx, struct cc_subtitle *su void xds_cea608_test(struct ccx_decoders_xds_context *ctx, struct cc_subtitle *sub) { +#ifndef DISABLE_RUST + return ccxr_xds_cea608_test(ctx, sub); +#else /* This test is the sample data that comes in CEA-608. It sets the program name to be "Star Trek". The checksum is 0x1d and the validation must succeed. */ process_xds_bytes(ctx, 0x01, 0x03); @@ -214,6 +243,7 @@ void xds_cea608_test(struct ccx_decoders_xds_context *ctx, struct cc_subtitle *s process_xds_bytes(ctx, 0x02, 0x03); process_xds_bytes(ctx, 0x6b, 0x00); do_end_of_xds(sub, ctx, 0x1d); +#endif } int how_many_used(struct ccx_decoders_xds_context *ctx) @@ -236,6 +266,9 @@ void clear_xds_buffer(struct ccx_decoders_xds_context *ctx, int num) void process_xds_bytes(struct ccx_decoders_xds_context *ctx, const unsigned char hi, int lo) { +#ifndef DISABLE_RUST + return ccxr_process_xds_bytes(ctx, hi, lo); // Use the Rust implementation +#else int is_new; if (!ctx) return; @@ -305,7 +338,9 @@ void process_xds_bytes(struct ccx_decoders_xds_context *ctx, const unsigned char ctx->xds_buffers[ctx->cur_xds_buffer_idx].bytes[ctx->xds_buffers[ctx->cur_xds_buffer_idx].used_bytes++] = lo; ctx->xds_buffers[ctx->cur_xds_buffer_idx].bytes[ctx->xds_buffers[ctx->cur_xds_buffer_idx].used_bytes] = 0; } +#endif } + /** * ctx XDS context can be NULL, if user don't want to write xds in transcript */ @@ -857,6 +892,10 @@ int xds_do_misc(struct ccx_decoders_xds_context *ctx) void do_end_of_xds(struct cc_subtitle *sub, struct ccx_decoders_xds_context *ctx, unsigned char expected_checksum) { +#ifndef DISABLE_RUST + return ccxr_do_end_of_xds(sub, ctx, expected_checksum); +#else + int cs = 0; int i; @@ -935,4 +974,5 @@ void do_end_of_xds(struct cc_subtitle *sub, struct ccx_decoders_xds_context *ctx dump(CCX_DMT_DECODER_XDS, ctx->cur_xds_payload, ctx->cur_xds_payload_length, 0, 0); } clear_xds_buffer(ctx, ctx->cur_xds_buffer_idx); +#endif } diff --git a/src/lib_ccx/ccx_decoders_xds.h b/src/lib_ccx/ccx_decoders_xds.h index 8f894ed75..46161d09e 100644 --- a/src/lib_ccx/ccx_decoders_xds.h +++ b/src/lib_ccx/ccx_decoders_xds.h @@ -12,7 +12,7 @@ void do_end_of_xds (struct cc_subtitle *sub, struct ccx_decoders_xds_context *ct struct ccx_decoders_xds_context *ccx_decoders_xds_init_library(struct ccx_common_timing_ctx *timing, int xds_write_to_file); -void xds_cea608_test(); +void xds_cea608_test(struct ccx_decoders_xds_context *ctx, struct cc_subtitle *sub); struct xds_buffer { diff --git a/src/rust/lib_ccxr/src/common/options.rs b/src/rust/lib_ccxr/src/common/options.rs index 489605c44..ee0a108bd 100644 --- a/src/rust/lib_ccxr/src/common/options.rs +++ b/src/rust/lib_ccxr/src/common/options.rs @@ -38,6 +38,7 @@ impl Default for EncodersTranscriptFormat { } } +#[repr(C)] #[derive(Debug, Default, Copy, Clone, PartialEq)] pub enum FrameType { #[default] diff --git a/src/rust/lib_ccxr/src/decoder_xds/exit_codes.rs b/src/rust/lib_ccxr/src/decoder_xds/exit_codes.rs new file mode 100644 index 000000000..db8b1f89d --- /dev/null +++ b/src/rust/lib_ccxr/src/decoder_xds/exit_codes.rs @@ -0,0 +1,43 @@ +// codes for do_end_of_xds +pub const XDS_CLASS_FUTURE: i64 = 1; +pub const XDS_CLASS_CHANNEL: i64 = 2; +pub const XDS_CLASS_MISC: i64 = 3; +pub const XDS_CLASS_PRIVATE: i64 = 6; +pub const XDS_CLASS_OUT_OF_BAND: i64 = 0x40; + +// codes for xds_do_misc +pub const XDS_TYPE_TIME_OF_DAY: i64 = 1; +pub const XDS_TYPE_LOCAL_TIME_ZONE: i64 = 4; + +// codes for xds_do_channel +pub const XDS_TYPE_NETWORK_NAME: i64 = 1; +pub const XDS_TYPE_CALL_LETTERS_AND_CHANNEL: i64 = 2; +pub const XDS_TYPE_TSID: i64 = 4; + +// codes for xds_do_current_and_future +pub const XDS_CLASS_CURRENT: i64 = 0; + +pub const XDS_TYPE_PIN_START_TIME: i64 = 1; +pub const XDS_TYPE_LENGTH_AND_CURRENT_TIME: i64 = 2; +pub const XDS_TYPE_PROGRAM_NAME: i64 = 3; +pub const XDS_TYPE_PROGRAM_TYPE: i64 = 4; +pub const XDS_TYPE_CONTENT_ADVISORY: i64 = 5; +pub const XDS_TYPE_AUDIO_SERVICES: i64 = 6; +pub const XDS_TYPE_CGMS: i64 = 8; +pub const XDS_TYPE_ASPECT_RATIO_INFO: i64 = 9; + +pub const XDS_TYPE_PROGRAM_DESC_1: i64 = 0x10; +pub const XDS_TYPE_PROGRAM_DESC_8: i64 = 0x17; + +// codes for write_xds_string +pub const TS_START_OF_XDS: i64 = -1; // Time at which we switched to XDS mode, =-1 hasn't happened yet + +// codes for Eia608Screen::default +pub const CCX_DECODER_608_SCREEN_ROWS: usize = 15; +pub const CCX_DECODER_608_SCREEN_WIDTH: usize = 32; + +// codes for CcxDecodersXdsContext::default +pub const NUM_XDS_BUFFERS: usize = 9; + +// codes for XdsBuffer::default +pub const NUM_BYTES_PER_PACKET: usize = 35; diff --git a/src/rust/lib_ccxr/src/decoder_xds/functions_xds.rs b/src/rust/lib_ccxr/src/decoder_xds/functions_xds.rs new file mode 100644 index 000000000..1da2dba67 --- /dev/null +++ b/src/rust/lib_ccxr/src/decoder_xds/functions_xds.rs @@ -0,0 +1,1083 @@ +use crate::time::c_functions::get_fts; +use crate::time::timing::CaptionField; +use crate::util::log::{debug, info, DebugMessageFlag}; + +use crate::decoder_xds::exit_codes::*; +use crate::decoder_xds::structs_xds::*; + +//---------------------------------------------------------------- + +#[derive(Debug)] +pub enum XdsError { + WriteError, + OtherError(String), +} +pub fn write_xds_string( + sub: &mut CcSubtitle, + ctx: &mut CcxDecodersXdsContext, + p: String, + len: i64, +) -> Result<(), XdsError> { + let new_screen = Eia608Screen { + format: CcxEia608Format::SformatXds, + start_time: TS_START_OF_XDS, + end_time: (get_fts(&mut ctx.timing, CaptionField::Field2)).millis(), + xds_str: p, + xds_len: len, + cur_xds_packet_class: ctx.cur_xds_packet_class, + ..Default::default() // Use default values for the remaining fields + }; + + sub.data.push(new_screen); + sub.datatype = SubDataType::Generic; + sub.nb_data = (sub.data.len() + 1) as i64; + sub.subtype = SubType::Cc608; + sub.got_output = true; + + Ok(()) +} +//---------------------------------------------------------------- + +// macro could be used, bad coz nothing is returned, could be an impl +pub fn xdsprint(sub: &mut CcSubtitle, ctx: &mut CcxDecodersXdsContext, args: std::fmt::Arguments) { + if !ctx.xds_write_to_file { + return; + } + + let formatted = format!("{}", args); + let formatted_len = formatted.len() as i64; + write_xds_string(sub, ctx, formatted, formatted_len).unwrap(); // unhandled err variant +} + +//---------------------------------------------------------------- + +pub fn process_xds_bytes(ctx: &mut CcxDecodersXdsContext, hi: i64, lo: i64) { + if (0x01..=0x0f).contains(&hi) { + let xds_class = (hi - 1) / 2; + let is_new = hi % 2 != 0; + + let mut first_free_buf: Option = None; + let mut matching_buf: Option = None; + + for (i, buf) in ctx.xds_buffers.iter_mut().enumerate() { + if buf.in_use && buf.xds_class == xds_class && buf.xds_type == lo { + matching_buf = Some(i); + break; + } + if first_free_buf.is_none() && !buf.in_use { + first_free_buf = Some(i); + } + } + + let buf_idx = match matching_buf.or(first_free_buf) { + Some(idx) => idx, + None => { + println!("All XDS buffers full, ignoring."); + ctx.cur_xds_buffer_idx = None; + return; + } + }; + + ctx.cur_xds_buffer_idx = Some(buf_idx as i64); + + let buf = &mut ctx.xds_buffers[buf_idx]; + + if is_new || !buf.in_use { + buf.xds_class = xds_class; + buf.xds_type = lo; + buf.used_bytes = 0; + buf.in_use = true; + buf.bytes = vec![0; NUM_BYTES_PER_PACKET]; + } + + if !is_new { + return; + } + } else if (0x01..=0x1f).contains(&hi) || (0x01..=0x1f).contains(&lo) { + println!("Illegal XDS data"); + return; + } + + if let Some(buf_idx) = ctx.cur_xds_buffer_idx { + let buf_idx = buf_idx as usize; + let buf = &mut ctx.xds_buffers[buf_idx]; + if buf.used_bytes <= 32 && buf.used_bytes < ((buf.bytes.len() as i64) - 1) { + buf.bytes[buf.used_bytes as usize] = hi; + buf.used_bytes += 1; + buf.bytes[buf.used_bytes as usize] = lo; + buf.used_bytes += 1; + buf.bytes[buf.used_bytes as usize] = 0; + } + } +} + +//---------------------------------------------------------------- + +pub struct XdsCopyState { + last_c1: Option, + last_c2: Option, + copy_permitted: String, + aps: String, + rcd: String, +} +impl XdsCopyState { + fn new() -> Self { + Self { + last_c1: None, + last_c2: None, + copy_permitted: String::new(), + aps: String::new(), + rcd: String::new(), + } + } +} + +impl Default for XdsCopyState { + fn default() -> Self { + Self::new() + } +} + +pub fn xds_do_copy_generation_management_system( + sub: &mut CcSubtitle, + ctx: &mut CcxDecodersXdsContext, + c1: u8, + c2: u8, + state: &mut XdsCopyState, +) { + let c1_6 = (c1 & 0x40) >> 6; + let c2_6 = (c2 & 0x40) >> 6; + + if c1_6 == 0 || c2_6 == 0 { + return; + } + + let changed = state.last_c1 != Some(c1) || state.last_c2 != Some(c2); + + if changed { + state.last_c1 = Some(c1); + state.last_c2 = Some(c2); + + let copytext = [ + "Copy permitted (no restrictions)", + "No more copies (one generation copy has been made)", + "One generation of copies can be made", + "No copying is permitted", + ]; + + let apstext = [ + "No APS", + "PSP On; Split Burst Off", + "PSP On; 2 line Split Burst On", + "PSP On; 4 line Split Burst On", + ]; + + let cgms_index = ((c1 & 0x10) >> 3) | ((c1 & 0x08) >> 3); + let aps_index = ((c1 & 0x04) >> 1) | ((c1 & 0x02) >> 1); + let rcd0 = c2 & 0x01; + + state.copy_permitted = format!("CGMS: {}", copytext[cgms_index as usize]); + state.aps = format!("APS: {}", apstext[aps_index as usize]); + state.rcd = format!("Redistribution Control Descriptor: {}", rcd0); + } + + xdsprint(sub, ctx, format_args!("{}", state.copy_permitted)); + xdsprint(sub, ctx, format_args!("{}", state.aps)); + xdsprint(sub, ctx, format_args!("{}", state.rcd)); + + if changed { + info!("{}", state.copy_permitted); + info!("{}", state.aps); + info!("{}", state.rcd); + } + + debug!(msg_type = DebugMessageFlag::DECODER_XDS; "{}", state.copy_permitted); + debug!(msg_type = DebugMessageFlag::DECODER_XDS; "{}", state.aps); + debug!(msg_type = DebugMessageFlag::DECODER_XDS; "{}", state.rcd); +} + +//---------------------------------------------------------------- + +struct Bits { + // c1_6: bool, + da2: bool, + a1: bool, + a0: bool, + r2: bool, + r1: bool, + r0: bool, + // c2_6: bool, + fv: bool, + s: bool, + la3: bool, + g2: bool, + g1: bool, + g0: bool, +} + +fn decode_bits(byte: u8) -> Bits { + Bits { + // c1_6: byte & 0x40 != 0, + da2: byte & 0x20 != 0, + a1: byte & 0x10 != 0, + a0: byte & 0x08 != 0, + r2: byte & 0x04 != 0, + r1: byte & 0x02 != 0, + r0: byte & 0x01 != 0, + // c2_6: byte & 0x40 != 0, + fv: byte & 0x20 != 0, + s: byte & 0x10 != 0, + la3: byte & 0x08 != 0, + g2: byte & 0x04 != 0, + g1: byte & 0x02 != 0, + g0: byte & 0x01 != 0, + } +} + +fn bits_to_number(b2: bool, b1: bool, b0: bool) -> usize { + (b2 as usize) * 4 + (b1 as usize) * 2 + (b0 as usize) +} + +pub fn xds_do_content_advisory( + sub: &mut CcSubtitle, + ctx: Option<&mut CcxDecodersXdsContext>, + c1: u8, + c2: u8, + state: &mut XdsCopyState, +) { + if ctx.is_none() { + return; + } + let ctx = ctx.unwrap(); + + if (c1 & 0x40) == 0 || (c2 & 0x40) == 0 { + return; + } + + let changed = state.last_c1 != Some(c1) || state.last_c2 != Some(c2); + + if changed { + state.last_c1 = Some(c1); + state.last_c2 = Some(c2); + } + + let c1_bits = decode_bits(c1); + let c2_bits = decode_bits(c2); + + let mut supported = false; + let mut age_info = String::new(); + let mut content_info = String::new(); + let mut rating_info = String::new(); + + if !c1_bits.a1 && c1_bits.a0 { + // US TV parental guidelines + let age_labels = [ + "None", + "TV-Y (All Children)", + "TV-Y7 (Older Children)", + "TV-G (General Audience)", + "TV-PG (Parental Guidance Suggested)", + "TV-14 (Parents Strongly Cautioned)", + "TV-MA (Mature Audience Only)", + "None", + ]; + let age_idx = bits_to_number(c2_bits.g2, c2_bits.g1, c2_bits.g0); + age_info = format!( + "ContentAdvisory: US TV Parental Guidelines. Age Rating: {}", + age_labels.get(age_idx).unwrap_or(&"Unknown") + ); + + if !c2_bits.g2 && c2_bits.g1 && !c2_bits.g0 { + if c2_bits.fv { + content_info.push_str("[Fantasy Violence] "); + } + } else if c2_bits.fv { + content_info.push_str("[Violence] "); + } + + if c2_bits.s { + content_info.push_str("[Sexual Situations] "); + } + + if c2_bits.la3 { + content_info.push_str("[Adult Language] "); + } + + if c1_bits.da2 { + content_info.push_str("[Sexually Suggestive Dialog] "); + } + + supported = true; + } + + if !c1_bits.a0 { + // MPA rating + let rating_labels = ["N/A", "G", "PG", "PG-13", "R", "NC-17", "X", "Not Rated"]; + let rating_idx = bits_to_number(c1_bits.r2, c1_bits.r1, c1_bits.r0); + rating_info = format!( + "ContentAdvisory: MPA Rating: {}", + rating_labels.get(rating_idx).unwrap_or(&"Unknown") + ); + + supported = true; + } + + if c1_bits.a0 && c1_bits.a1 && !c1_bits.da2 && !c2_bits.la3 { + // Canadian English rating + let rating_labels = [ + "Exempt", + "Children", + "Children eight years and older", + "General programming suitable for all audiences", + "Parental Guidance", + "Viewers 14 years and older", + "Adult Programming", + "[undefined]", + ]; + let rating_idx = bits_to_number(c2_bits.g2, c2_bits.g1, c2_bits.g0); + rating_info = format!( + "ContentAdvisory: Canadian English Rating: {}", + rating_labels.get(rating_idx).unwrap_or(&"Unknown") + ); + + supported = true; + } + + if c1_bits.a0 && c1_bits.a1 && c1_bits.da2 && !c2_bits.la3 { + // Canadian French rating + let rating_labels = [ + "Exemptées", + "Général", + "Général - Déconseillé aux jeunes enfants", + "Cette émission peut ne pas convenir aux enfants de moins de 13 ans", + "Cette émission ne convient pas aux moins de 16 ans", + "Cette émission est réservée aux adultes", + "[invalid]", + "[invalid]", + ]; + let rating_idx = bits_to_number(c2_bits.g2, c2_bits.g1, c2_bits.g0); + rating_info = format!( + "ContentAdvisory: Canadian French Rating: {}", + rating_labels.get(rating_idx).unwrap_or(&"Unknown") + ); + + supported = true; + } + + if !c1_bits.a1 && c1_bits.a0 { + // US TV + + xdsprint(sub, ctx, format_args!("{}", age_info)); + xdsprint(sub, ctx, format_args!("{}", content_info)); + if changed { + info!("{}", age_info); + info!("{}", content_info); + } + debug!(msg_type = DebugMessageFlag::DECODER_XDS; "{}", age_info); + debug!(msg_type = DebugMessageFlag::DECODER_XDS; "{}", content_info); + } + + if !c1_bits.a0 || (c1_bits.a1 && !c1_bits.da2 && !c2_bits.la3) { + xdsprint(sub, ctx, format_args!("{}", rating_info)); + if changed { + info!("{}", rating_info); + } + debug!(msg_type = DebugMessageFlag::DECODER_XDS; "{}", rating_info); + } + + if changed && !supported { + info!("XDS: Unsupported ContentAdvisory encoding, please submit sample."); + } +} + +//---------------------------------------------------------------- + +use std::fmt::Write; + +pub fn xds_do_current_and_future( + sub: &mut CcSubtitle, + ctx: &mut CcxDecodersXdsContext, +) -> Result { + let mut was_proc = false; + let mut str_buf = String::with_capacity(1024); + + match ctx.cur_xds_packet_type { + XDS_TYPE_PIN_START_TIME => { + was_proc = true; + if ctx.cur_xds_payload_length < 7 { + return Ok(was_proc); + } + + let min = ctx.cur_xds_payload[2] & 0x3f; + let hour = ctx.cur_xds_payload[3] & 0x1f; + let date = ctx.cur_xds_payload[4] & 0x1f; + let month = ctx.cur_xds_payload[5] & 0x0f; + + if ctx.current_xds_min != min.into() + || ctx.current_xds_hour != hour.into() + || ctx.current_xds_date != date.into() + || ctx.current_xds_month != month.into() + { + ctx.xds_start_time_shown = 0; + ctx.current_xds_min = min.into(); + ctx.current_xds_hour = hour.into(); + ctx.current_xds_date = date.into(); + ctx.current_xds_month = month.into(); + } + + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "PIN (Start Time): {} {:02}-{:02} {:02}:{:02}", + if ctx.cur_xds_packet_class == XDS_CLASS_CURRENT { + "Current" + } else { + "Future" + }, + date, + month, + hour, + min + ); + + xdsprint( + sub, + ctx, + format_args!( + "PIN (Start Time): {} {:02}-{:02} {:02}:{:02}", + if ctx.cur_xds_packet_class == XDS_CLASS_CURRENT { + "Current" + } else { + "Future" + }, + date, + month, + hour, + min + ), + ); + + if ctx.xds_start_time_shown == 0 && ctx.cur_xds_packet_class == XDS_CLASS_CURRENT { + info!("XDS: Program changed."); + info!( + "XDS program start time (DD/MM HH:MM) {:02}-{:02} {:02}:{:02}", + date, month, hour, min + ); + // GUI update would go here + ctx.xds_start_time_shown = 1; + } + } + XDS_TYPE_LENGTH_AND_CURRENT_TIME => { + was_proc = true; + if ctx.cur_xds_payload_length < 5 { + return Ok(was_proc); + } + + let min = ctx.cur_xds_payload[2] & 0x3f; + let hour = ctx.cur_xds_payload[3] & 0x1f; + + if ctx.xds_program_length_shown == 0 { + info!("XDS: Program length (HH:MM): {:02}:{:02} ", hour, min); + } else { + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "XDS: Program length (HH:MM): {:02}:{:02} ", + hour, + min + ); + } + + xdsprint( + sub, + ctx, + format_args!("Program length (HH:MM): {:02}:{:02} ", hour, min), + ); + + if ctx.cur_xds_payload_length > 6 { + let el_min = ctx.cur_xds_payload[4] & 0x3f; + let el_hour = ctx.cur_xds_payload[5] & 0x1f; + + if ctx.xds_program_length_shown == 0 { + info!("Elapsed (HH:MM): {:02}:{:02}", el_hour, el_min); + } else { + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "Elapsed (HH:MM): {:02}:{:02}", + el_hour, + el_min + ); + } + xdsprint( + sub, + ctx, + format_args!("Elapsed (HH:MM): {:02}:{:02}", el_hour, el_min), + ); + } + + if ctx.cur_xds_payload_length > 8 { + let el_sec = ctx.cur_xds_payload[6] & 0x3f; + if ctx.xds_program_length_shown == 0 { + debug!(msg_type = DebugMessageFlag::DECODER_XDS; ":{:02}", el_sec); + } + xdsprint(sub, ctx, format_args!("Elapsed (SS) :{:02}", el_sec)); + } + + if ctx.xds_program_length_shown == 0 { + info!(""); + } else { + debug!(msg_type = DebugMessageFlag::DECODER_XDS; ""); + } + ctx.xds_program_length_shown = 1; + } + XDS_TYPE_PROGRAM_NAME => { + was_proc = true; + let mut xds_program_name = String::new(); + for i in 2..ctx.cur_xds_payload_length - 1 { + xds_program_name.push(ctx.cur_xds_payload[i as usize] as char); + } + + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "XDS Program name: {}", + xds_program_name + ); + xdsprint(sub, ctx, format_args!("Program name: {}", xds_program_name)); + + if ctx.cur_xds_packet_class == XDS_CLASS_CURRENT + && ctx.current_xds_program_name != xds_program_name + { + info!("XDS Notice: Program is now {}", xds_program_name); + ctx.current_xds_program_name = xds_program_name; + // GUI update would go here + } + } + XDS_TYPE_PROGRAM_TYPE => { + was_proc = true; + if ctx.cur_xds_payload_length < 5 { + return Ok(was_proc); + } + + if ctx.current_program_type_reported != 0 { + for i in 0..ctx.cur_xds_payload_length { + if ctx.cur_xds_payload[i as usize] + != ctx.current_xds_program_type.as_bytes()[i as usize] + { + ctx.current_program_type_reported = 0; + break; + } + } + } + + ctx.current_xds_program_type = String::from_utf8_lossy( + &ctx.cur_xds_payload[..ctx.cur_xds_payload_length as usize], + ) + .into_owned(); + + if ctx.current_program_type_reported == 0 { + info!("XDS Program Type: "); + } + + str_buf.clear(); + for i in 2..ctx.cur_xds_payload_length - 1 { + if ctx.cur_xds_payload[i as usize] == 0 { + continue; + } + + if ctx.current_program_type_reported == 0 { + info!("[{:02X}-", ctx.cur_xds_payload[i as usize]); + } + + if ctx.cur_xds_payload[i as usize] >= 0x20 && ctx.cur_xds_payload[i as usize] < 0x7F + { + let type_str = + XDS_PROGRAM_TYPES[(ctx.cur_xds_payload[i as usize] - 0x20) as usize]; + write!(str_buf, "[{}] ", type_str).unwrap(); + } + + if ctx.current_program_type_reported == 0 { + if ctx.cur_xds_payload[i as usize] >= 0x20 + && ctx.cur_xds_payload[i as usize] < 0x7F + { + info!( + "{}", + XDS_PROGRAM_TYPES[(ctx.cur_xds_payload[i as usize] - 0x20) as usize] + ); + } else { + info!("ILLEGAL VALUE"); + } + info!("] "); + } + } + + xdsprint(sub, ctx, format_args!("Program type {}", str_buf)); + if ctx.current_program_type_reported == 0 { + info!(""); + } + ctx.current_program_type_reported = 1; + } + XDS_TYPE_CONTENT_ADVISORY => { + was_proc = true; + if ctx.cur_xds_payload_length < 5 { + return Ok(was_proc); + } + let xds_do_content_advisory_c1: u8 = ctx.cur_xds_payload[2]; + let xds_do_content_advisory_c2: u8 = ctx.cur_xds_payload[3]; + let mut state = XdsCopyState::new(); + xds_do_content_advisory( + sub, + Some(ctx), + xds_do_content_advisory_c1, + xds_do_content_advisory_c2, + &mut state, + ); + } + XDS_TYPE_AUDIO_SERVICES => { + was_proc = true; + } + XDS_TYPE_CGMS => { + was_proc = true; + let mut state = XdsCopyState::new(); + xds_do_copy_generation_management_system( + sub, + ctx, + ctx.cur_xds_payload[2], + ctx.cur_xds_payload[3], + &mut state, + ); + } + XDS_TYPE_ASPECT_RATIO_INFO => { + was_proc = true; + if ctx.cur_xds_payload_length < 5 { + return Ok(was_proc); + } + if (ctx.cur_xds_payload[2] & 0x20) == 0 || (ctx.cur_xds_payload[3] & 0x20) == 0 { + return Ok(was_proc); + } + + let ar_start = (ctx.cur_xds_payload[2] & 0x1F) as u32 + 22; + let ar_end = 262 - (ctx.cur_xds_payload[3] & 0x1F) as u32; + let active_picture_height = ar_end - ar_start; + let aspect_ratio = 320.0 / active_picture_height as f32; + + if ar_start != ctx.current_ar_start as u32 { + ctx.current_ar_start = ar_start as i64; + ctx.current_ar_end = ar_end as i64; + info!( + "XDS Notice: Aspect ratio info, start line={}, end line={}", + ar_start, ar_end + ); + info!( + "XDS Notice: Aspect ratio info, active picture height={}, ratio={}", + active_picture_height, aspect_ratio + ); + } else { + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "XDS Notice: Aspect ratio info, start line={}, end line={}", + ar_start, + ar_end + ); + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "XDS Notice: Aspect ratio info, active picture height={}, ratio={}", + active_picture_height, + aspect_ratio + ); + } + } + XDS_TYPE_PROGRAM_DESC_1..=XDS_TYPE_PROGRAM_DESC_8 => { + was_proc = true; + let mut xds_desc = String::new(); + for i in 2..ctx.cur_xds_payload_length - 1 { + xds_desc.push(ctx.cur_xds_payload[i as usize] as char); + } + + if !xds_desc.is_empty() { + let line_num = (ctx.cur_xds_packet_type - XDS_TYPE_PROGRAM_DESC_1) as usize; + let changed = ctx.xds_program_description[line_num] != xds_desc; + + if changed { + info!("XDS description line {}: {}", line_num, xds_desc); + ctx.xds_program_description[line_num] = xds_desc.clone(); + } else { + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "XDS description line {}: {}", + line_num, + xds_desc + ); + } + xdsprint( + sub, + ctx, + format_args!("XDS description line {}: {}", line_num, xds_desc), + ); + // at end, to-do : GUI Update + } + } + _ => {} + } + + Ok(was_proc) +} + +pub const XDS_PROGRAM_TYPES: [&str; 96] = [ + "Education", + "Entertainment", + "Movie", + "News", + "Religious", + "Sports", + "Other", + "Action", + "Advertisement", + "Animated", + "Anthology", + "Automobile", + "Awards", + "Baseball", + "Basketball", + "Bulletin", + "Business", + "Classical", + "College", + "Combat", + "Comedy", + "Commentary", + "Concert", + "Consumer", + "Contemporary", + "Crime", + "Dance", + "Documentary", + "Drama", + "Elementary", + "Erotica", + "Exercise", + "Fantasy", + "Farm", + "Fashion", + "Fiction", + "Food", + "Football", + "Foreign", + "Fund-Raiser", + "Game/Quiz", + "Garden", + "Golf", + "Government", + "Health", + "High_School", + "History", + "Hobby", + "Hockey", + "Home", + "Horror", + "Information", + "Instruction", + "International", + "Interview", + "Language", + "Legal", + "Live", + "Local", + "Math", + "Medical", + "Meeting", + "Military", + "Mini-Series", + "Music", + "Mystery", + "National", + "Nature", + "Police", + "Politics", + "Premiere", + "Pre-Recorded", + "Product", + "Professional", + "Public", + "Racing", + "Reading", + "Repair", + "Repeat", + "Review", + "Romance", + "Science", + "Series", + "Service", + "Shopping", + "Soap_Opera", + "Special", + "Suspense", + "Talk", + "Technical", + "Tennis", + "Travel", + "Variety", + "Video", + "Weather", + "Western", +]; + +//---------------------------------------------------------------- + +pub fn xds_do_channel( + sub: &mut CcSubtitle, + ctx: &mut CcxDecodersXdsContext, +) -> Result { + let mut was_proc = false; + + match ctx.cur_xds_packet_type { + XDS_TYPE_NETWORK_NAME => { + was_proc = true; + let mut xds_network_name = String::new(); + for i in 2..ctx.cur_xds_payload_length - 1 { + xds_network_name.push(ctx.cur_xds_payload[i as usize] as char); + } + + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "XDS Network name: {}", + xds_network_name + ); + xdsprint(sub, ctx, format_args!("Network: {}", xds_network_name)); + + if ctx.current_xds_network_name != xds_network_name { + info!("XDS Notice: Network is now {}", xds_network_name); + ctx.current_xds_network_name = xds_network_name; + } + } + XDS_TYPE_CALL_LETTERS_AND_CHANNEL => { + was_proc = true; + if ctx.cur_xds_payload_length != 7 && ctx.cur_xds_payload_length != 9 { + return Ok(was_proc); + } + + let mut xds_call_letters = String::new(); + for i in 2..ctx.cur_xds_payload_length - 1 { + xds_call_letters.push(ctx.cur_xds_payload[i as usize] as char); + } + + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "XDS Network call letters: {}", + xds_call_letters + ); + xdsprint(sub, ctx, format_args!("Call Letters: {}", xds_call_letters)); + + if ctx.current_xds_call_letters != xds_call_letters { + info!("XDS Notice: Network call letters now {}", xds_call_letters); + ctx.current_xds_call_letters = xds_call_letters; + // GUI update would go here + } + } + XDS_TYPE_TSID => { + was_proc = true; + if ctx.cur_xds_payload_length < 7 { + return Ok(was_proc); + } + + let b1 = (ctx.cur_xds_payload[2] & 0x0F) as u32; + let b2 = (ctx.cur_xds_payload[3] & 0x0F) as u32; + let b3 = (ctx.cur_xds_payload[4] & 0x0F) as u32; + let b4 = (ctx.cur_xds_payload[5] & 0x0F) as u32; + let tsid = (b4 << 12) | (b3 << 8) | (b2 << 4) | b1; + + if tsid != 0 { + xdsprint(sub, ctx, format_args!("TSID: {}", tsid)); + } + } + _ => {} + } + + Ok(was_proc) +} + +//---------------------------------------------------------------- + +pub fn xds_do_private_data( + sub: &mut CcSubtitle, + ctx: &mut CcxDecodersXdsContext, +) -> Result { + if ctx.cur_xds_payload_length < 3 { + return Ok(false); + } + + let mut hex_dump = String::with_capacity((ctx.cur_xds_payload_length as usize) * 3); + + for i in 2..ctx.cur_xds_payload_length - 1 { + write!(hex_dump, "{:02X} ", ctx.cur_xds_payload[i as usize]) + .map_err(|_| "Failed to format hex string")?; + } + + xdsprint(sub, ctx, format_args!("{}", hex_dump)); + Ok(true) +} + +//---------------------------------------------------------------- + +pub fn xds_do_misc(ctx: &mut CcxDecodersXdsContext) -> Result { + let mut was_proc = false; + + match ctx.cur_xds_packet_type { + XDS_TYPE_TIME_OF_DAY => { + was_proc = true; + if ctx.cur_xds_payload_length < 9 { + return Ok(was_proc); + } + + let min = ctx.cur_xds_payload[2] & 0x3f; + let hour = ctx.cur_xds_payload[3] & 0x1f; + let date = ctx.cur_xds_payload[4] & 0x1f; + let month = ctx.cur_xds_payload[5] & 0x0f; + let reset_seconds = (ctx.cur_xds_payload[5] & 0x20) != 0; + let day_of_week = ctx.cur_xds_payload[6] & 0x07; + let year = ((ctx.cur_xds_payload[7] as u16) & 0x3f) + 1990; + + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "Time of day: (YYYY/MM/DD) {:04}/{:02}/{:02} (HH:MM) {:02}:{:02} DoW: {} Reset seconds: {}", + year, month, date, hour, min, day_of_week, reset_seconds + ); + } + XDS_TYPE_LOCAL_TIME_ZONE => { + was_proc = true; + if ctx.cur_xds_payload_length < 5 { + return Ok(was_proc); + } + + let dst = (ctx.cur_xds_payload[2] & 0x20) != 0; + let hour = ctx.cur_xds_payload[2] & 0x1f; + + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "Local Time Zone: {:02} DST: {}", + hour, dst + ); + } + _ => {} + } + + Ok(was_proc) +} + +//---------------------------------------------------------------- + +pub fn do_end_of_xds(sub: &mut CcSubtitle, ctx: &mut CcxDecodersXdsContext, expected_checksum: u8) { + if ctx.cur_xds_buffer_idx.is_none() { + return; + } + let buffer_idx = ctx.cur_xds_buffer_idx.unwrap(); + + if buffer_idx < 0 + || buffer_idx as usize >= ctx.xds_buffers.len() + || !ctx.xds_buffers[buffer_idx as usize].in_use + { + return; + } + + let buffer = &ctx.xds_buffers[buffer_idx as usize]; + ctx.cur_xds_packet_class = buffer.xds_class; + ctx.cur_xds_payload = buffer.bytes.iter().map(|&x| x as u8).collect(); + ctx.cur_xds_payload_length = buffer.used_bytes; + ctx.cur_xds_packet_type = buffer.bytes[1]; + + ctx.cur_xds_payload.push(0x0F); + ctx.cur_xds_payload_length += 1; + + let mut cs = 0; + for i in 0..ctx.cur_xds_payload_length as usize { + let byte = ctx.cur_xds_payload[i]; + cs = (cs + byte) & 0x7f; + let c = byte & 0x7F; + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "{:02X} - {} cs: {:02X}", + c, + if c >= 0x20 { c as char } else { '?' }, + cs + ); + } + cs = (128 - cs) & 0x7F; + + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "End of XDS. Class={} ({}), size={} Checksum OK: {} Used buffers: {}", + ctx.cur_xds_packet_class, + match ctx.cur_xds_packet_class { + 0 => "Current", + 1 => "Future", + 2 => "Channel", + 3 => "Misc", + 4 => "Private", + 5 => "Out-of-band", + _ => "Unknown", + }, + ctx.cur_xds_payload_length, + cs == expected_checksum, + ctx.how_many_used() + ); + + if cs != expected_checksum || ctx.cur_xds_payload_length < 3 { + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "Expected checksum: {:02X} Calculated: {:02X}", + expected_checksum, + cs + ); + ctx.clear_xds_buffer(buffer_idx as usize) + .unwrap_or_default(); + return; + } + + if (ctx.cur_xds_packet_type & 0x40) != 0 { + ctx.cur_xds_packet_class = XDS_CLASS_OUT_OF_BAND; + } + + let was_proc = match ctx.cur_xds_packet_class { + XDS_CLASS_FUTURE + if !matches!(DebugMessageFlag::DECODER_XDS, DebugMessageFlag::DECODER_XDS) => + { + true + } + XDS_CLASS_FUTURE | XDS_CLASS_CURRENT => { + xds_do_current_and_future(sub, ctx).unwrap_or(false) + } + XDS_CLASS_CHANNEL => xds_do_channel(sub, ctx).unwrap_or(false), + XDS_CLASS_MISC => xds_do_misc(ctx).unwrap_or(false), + XDS_CLASS_PRIVATE => xds_do_private_data(sub, ctx).unwrap_or(false), + XDS_CLASS_OUT_OF_BAND => { + debug!(msg_type = DebugMessageFlag::DECODER_XDS; "Out-of-band data, ignored."); + true + } + _ => false, + }; + + if !was_proc { + info!("Note: We found a currently unsupported XDS packet."); + //to-do : how to dump? + } + + ctx.clear_xds_buffer(buffer_idx as usize) + .unwrap_or_default(); +} + +//---------------------------------------------------------------- + +pub fn xds_debug_test(ctx: &mut CcxDecodersXdsContext, sub: &mut CcSubtitle) { + process_xds_bytes(ctx, 0x05, 0x02); + process_xds_bytes(ctx, 0x20, 0x20); + do_end_of_xds(sub, ctx, 0x2a); +} + +pub fn xds_cea608_test(ctx: &mut CcxDecodersXdsContext, sub: &mut CcSubtitle) { + process_xds_bytes(ctx, 0x01, 0x03); + process_xds_bytes(ctx, 0x53, 0x74); + process_xds_bytes(ctx, 0x61, 0x72); + process_xds_bytes(ctx, 0x20, 0x54); + process_xds_bytes(ctx, 0x72, 0x65); + process_xds_bytes(ctx, 0x02, 0x03); + process_xds_bytes(ctx, 0x02, 0x03); + process_xds_bytes(ctx, 0x6b, 0x00); + do_end_of_xds(sub, ctx, 0x1d); +} diff --git a/src/rust/lib_ccxr/src/decoder_xds/mod.rs b/src/rust/lib_ccxr/src/decoder_xds/mod.rs new file mode 100644 index 000000000..eae0cb96c --- /dev/null +++ b/src/rust/lib_ccxr/src/decoder_xds/mod.rs @@ -0,0 +1,3 @@ +pub mod exit_codes; +pub mod functions_xds; +pub mod structs_xds; diff --git a/src/rust/lib_ccxr/src/decoder_xds/structs_xds.rs b/src/rust/lib_ccxr/src/decoder_xds/structs_xds.rs new file mode 100644 index 000000000..162c52390 --- /dev/null +++ b/src/rust/lib_ccxr/src/decoder_xds/structs_xds.rs @@ -0,0 +1,300 @@ +use crate::time::timing::TimingContext; + +use crate::decoder_xds::exit_codes::*; + +#[derive(Clone)] +pub struct XdsBuffer { + pub in_use: bool, + pub xds_class: i64, + pub xds_type: i64, + pub bytes: Vec, // of size NUM_BYTES_PER_PACKET + pub used_bytes: i64, +} + +impl XdsBuffer { + pub fn clear(&mut self) { + self.in_use = false; + self.xds_class = -1; + self.xds_type = -1; + self.bytes = vec![0; NUM_BYTES_PER_PACKET]; + self.used_bytes = 0; + } +} + +impl Default for XdsBuffer { + fn default() -> Self { + XdsBuffer { + in_use: false, + xds_class: -1, + xds_type: -1, + bytes: vec![0; NUM_BYTES_PER_PACKET], + used_bytes: 0, + } + } +} + +#[repr(C)] +pub struct CcxDecodersXdsContext { + pub current_xds_min: i64, + pub current_xds_hour: i64, + pub current_xds_date: i64, + pub current_xds_month: i64, + pub current_program_type_reported: i64, + pub xds_start_time_shown: i64, + pub xds_program_length_shown: i64, + pub xds_program_description: Vec, // 8 string of 33 bytes + + pub current_xds_network_name: String, // 33 bytes + pub current_xds_program_name: String, // 33 bytes + pub current_xds_call_letters: String, // 7 bytes + pub current_xds_program_type: String, // 33 bytes + + pub xds_buffers: Vec, // of size NUM_BYTES_PER_PACKET + pub cur_xds_buffer_idx: Option, + pub cur_xds_packet_class: i64, + pub cur_xds_payload: Vec, + pub cur_xds_payload_length: i64, + pub cur_xds_packet_type: i64, + pub timing: TimingContext, // use TimingContext as ccx_common_timing_ctx + + pub current_ar_start: i64, + pub current_ar_end: i64, + + pub xds_write_to_file: bool, // originally i64 +} + +impl CcxDecodersXdsContext { + pub fn how_many_used(&self) -> i64 { + let mut count = 0; + for buffer in self.xds_buffers.iter() { + if buffer.in_use { + count += 1; + } + } + count + } + + pub fn clear_xds_buffer(&mut self, index: usize) -> Result<(), &str> { + if index < self.xds_buffers.len() { + self.xds_buffers[index].clear(); + Ok(()) + } else { + Err("Index out of bounds") + } + } +} + +impl CcxDecodersXdsContext { + pub fn xds_init_library(timing: TimingContext, xds_write_to_file: bool) -> Self { + Self { + timing, + xds_write_to_file, + ..Default::default() + } + } +} + +impl Default for CcxDecodersXdsContext { + fn default() -> Self { + CcxDecodersXdsContext { + current_xds_min: -1, + current_xds_hour: -1, + current_xds_date: -1, + current_xds_month: -1, + current_program_type_reported: 0, + xds_start_time_shown: 0, + xds_program_length_shown: 0, + + xds_program_description: vec![String::new(); 8], + current_xds_network_name: String::new(), + current_xds_program_name: String::new(), + current_xds_call_letters: String::new(), + current_xds_program_type: String::new(), + xds_buffers: vec![XdsBuffer::default(); NUM_XDS_BUFFERS], + cur_xds_buffer_idx: None, + cur_xds_packet_class: 0, + cur_xds_payload: Vec::new(), + cur_xds_payload_length: 0, + cur_xds_packet_type: 0, + timing: TimingContext::default(), + current_ar_start: 0, + current_ar_end: 0, + xds_write_to_file: true, + } + } +} + +//---------------------------------------------------------------- + +use std::ptr::NonNull; + +// #[derive(Debug)] +// pub enum SubtitleData { +// None, +// Eia608(Vec), +// } + +#[derive(Debug)] +pub struct CcSubtitle { + pub data: Vec, // originally SubtitleData + pub datatype: SubDataType, + pub nb_data: i64, + pub subtype: SubType, + pub enc_type: CcxEncodingType, + pub start_time: i64, + pub end_time: i64, + pub flags: i64, + pub lang_index: i64, + pub got_output: bool, + pub mode: Vec, // of size 5 + pub info: Vec, // of size 4 + pub time_out: i64, + pub next: Option>, + pub prev: Option>, +} + +impl Default for CcSubtitle { + fn default() -> Self { + Self { + data: Vec::new(), + datatype: SubDataType::Default, + nb_data: 0, + subtype: SubType::Default, + enc_type: CcxEncodingType::Default, + start_time: 0, + end_time: 0, + flags: 0, + lang_index: 0, + got_output: false, + mode: vec![0; 5], + info: vec![0; 4], + time_out: 0, + next: None, + prev: None, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] +pub enum SubDataType { + #[default] + Generic = 0, + Dvb = 1, + Default, // fallback variant +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] +pub enum SubType { + #[default] + Bitmap, + Cc608, + Text, + Raw, + Default, // fallback variant +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] +pub enum CcxEncodingType { + #[default] + Unicode = 0, + Latin1 = 1, + Utf8 = 2, + Ascii = 3, + Default, // fallback variant +} + +//---------------------------------------------------------------- + +#[derive(Debug)] +pub struct Eia608Screen { + pub format: CcxEia608Format, + // pub characters: [[u8; CCX_DECODER_608_SCREEN_WIDTH + 1]; CCX_DECODER_608_SCREEN_ROWS], + pub characters: Vec>, + // pub colors: [[CcxDecoder608ColorCode; CCX_DECODER_608_SCREEN_WIDTH + 1]; CCX_DECODER_608_SCREEN_ROWS], + pub colors: Vec>, + // pub fonts: [[FontBits; CCX_DECODER_608_SCREEN_WIDTH + 1]; CCX_DECODER_608_SCREEN_ROWS], + pub fonts: Vec>, + // pub row_used: [i64; CCX_DECODER_608_SCREEN_ROWS], + pub row_used: Vec, + + pub empty: i64, + pub start_time: i64, + pub end_time: i64, + pub mode: CcModes, + pub channel: i64, + pub my_field: i64, + pub xds_str: String, + pub xds_len: i64, + pub cur_xds_packet_class: i64, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CcxEia608Format { + SformatCcScreen, + SformatCcLine, + SformatXds, + Default, // fallback variant +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CcxDecoder608ColorCode { + ColWhite = 0, + ColGreen = 1, + ColBlue = 2, + ColCyan = 3, + ColRed = 4, + ColYellow = 5, + ColMagenta = 6, + ColUserDefined = 7, + ColBlack = 8, + ColTransparent = 9, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum FontBits { + FontRegular = 0, + FontItalics = 1, + FontUnderlined = 2, + FontUnderlinedItalics = 3, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CcModes { + ModePopOn = 0, + ModeRollUp2 = 1, + ModeRollUp3 = 2, + ModeRollUp4 = 3, + ModeText = 4, + ModePaintOn = 5, + ModeFakeRollUp1 = 100, // Fake mode, also used as default mode +} + +impl Default for Eia608Screen { + fn default() -> Eia608Screen { + Eia608Screen { + format: CcxEia608Format::Default, + characters: vec![ + vec![0; CCX_DECODER_608_SCREEN_WIDTH + 1]; + CCX_DECODER_608_SCREEN_ROWS + ], + colors: vec![ + vec![CcxDecoder608ColorCode::ColWhite; CCX_DECODER_608_SCREEN_WIDTH + 1]; + CCX_DECODER_608_SCREEN_ROWS + ], + fonts: vec![ + vec![FontBits::FontRegular; CCX_DECODER_608_SCREEN_WIDTH + 1]; + CCX_DECODER_608_SCREEN_ROWS + ], + row_used: vec![0; CCX_DECODER_608_SCREEN_ROWS], + empty: 0, + start_time: TS_START_OF_XDS, + end_time: 0, + mode: CcModes::ModeFakeRollUp1, + channel: 0, + my_field: 0, + xds_str: String::new(), + xds_len: 0, + cur_xds_packet_class: 0, + } + } +} diff --git a/src/rust/lib_ccxr/src/lib.rs b/src/rust/lib_ccxr/src/lib.rs index 9f32678db..fef19a6aa 100644 --- a/src/rust/lib_ccxr/src/lib.rs +++ b/src/rust/lib_ccxr/src/lib.rs @@ -1,5 +1,6 @@ pub mod activity; pub mod common; +pub mod decoder_xds; pub mod hardsubx; pub mod subtitle; pub mod teletext; diff --git a/src/rust/lib_ccxr/src/time/timing.rs b/src/rust/lib_ccxr/src/time/timing.rs index 564b61634..8250c457c 100644 --- a/src/rust/lib_ccxr/src/time/timing.rs +++ b/src/rust/lib_ccxr/src/time/timing.rs @@ -14,6 +14,7 @@ pub static GLOBAL_TIMING_INFO: RwLock = RwLock::new(GlobalTimi pub const DEFAULT_FRAME_RATE: f64 = 30000.0 / 1001.0; /// Represents the status of [`TimingContext::current_pts`] and [`TimingContext::min_pts`] +#[repr(C)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum PtsSet { No = 0, @@ -33,7 +34,8 @@ pub enum CaptionField { /// /// [`GlobalTimingInfo`] serves a similar purpose. The only difference is that its lifetime is /// global. -#[derive(Debug)] +#[repr(C)] +#[derive(Debug, PartialEq, Clone)] pub struct TimingContext { pub pts_set: PtsSet, /// if true then don't adjust again. diff --git a/src/rust/lib_ccxr/src/time/units.rs b/src/rust/lib_ccxr/src/time/units.rs index 0bc546c6a..5385cbe11 100644 --- a/src/rust/lib_ccxr/src/time/units.rs +++ b/src/rust/lib_ccxr/src/time/units.rs @@ -19,6 +19,7 @@ extern "C" { /// Represents a timestamp in milliseconds. /// /// The number can be negetive. +#[repr(C)] #[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Add, Sub, Neg)] pub struct Timestamp { millis: i64, @@ -478,6 +479,7 @@ impl Timestamp { /// Represent the number of clock ticks as defined in Mpeg standard. /// /// This number can never be negetive. +#[repr(C)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Add, Sub)] pub struct MpegClockTick(i64); @@ -508,6 +510,7 @@ impl MpegClockTick { /// Represents the number of frames. /// /// This number can never be negetive. +#[repr(C)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Add, Sub)] pub struct FrameCount(u64); diff --git a/src/rust/src/libccxr_exports/mod.rs b/src/rust/src/libccxr_exports/mod.rs index c2f64a258..8d496a39a 100644 --- a/src/rust/src/libccxr_exports/mod.rs +++ b/src/rust/src/libccxr_exports/mod.rs @@ -1,6 +1,7 @@ //! Provides C-FFI functions that are direct equivalent of functions available in C. pub mod time; +pub mod xds_exports; use crate::ccx_options; use lib_ccxr::util::log::*; use lib_ccxr::util::{bits::*, levenshtein::*}; diff --git a/src/rust/src/libccxr_exports/xds_exports.rs b/src/rust/src/libccxr_exports/xds_exports.rs new file mode 100644 index 000000000..c85587a2e --- /dev/null +++ b/src/rust/src/libccxr_exports/xds_exports.rs @@ -0,0 +1,81 @@ +use lib_ccxr::decoder_xds::functions_xds::{do_end_of_xds, process_xds_bytes, xds_cea608_test}; +use lib_ccxr::decoder_xds::structs_xds::{CcSubtitle, CcxDecodersXdsContext}; +use lib_ccxr::time::TimingContext; + +#[no_mangle] +/// # Safety +/// - `sub` must be a non-null pointer to a valid, mutable `CcSubtitle` instance. +/// - `ctx` must be a non-null pointer to a valid, mutable `CcxDecodersXdsContext` instance. +/// - The caller must ensure that the memory referenced by `sub` and `ctx` remains valid for the duration of the function call. +/// - Passing null or invalid pointers will result in undefined behavior. +pub unsafe extern "C" fn ccxr_do_end_of_xds( + sub: *mut CcSubtitle, + ctx: *mut CcxDecodersXdsContext, + expected_checksum: u8, +) { + if sub.is_null() || ctx.is_null() { + return; + } + + let sub = unsafe { &mut *sub }; + let ctx = unsafe { &mut *ctx }; + + do_end_of_xds(sub, ctx, expected_checksum); +} + +#[no_mangle] +/// # Safety +/// - `ctx` must be a non-null pointer to a valid, mutable `CcxDecodersXdsContext` instance. +/// - The caller must ensure that the memory referenced by `ctx` remains valid for the duration of the function call. +/// - Passing a null or invalid pointer will result in undefined behavior. +pub unsafe extern "C" fn ccxr_process_xds_bytes(ctx: *mut CcxDecodersXdsContext, hi: u8, lo: u8) { + if ctx.is_null() { + return; + } + + let ctx = unsafe { &mut *ctx }; + + process_xds_bytes(ctx, hi as i64, lo as i64); +} + +#[no_mangle] +/// # Safety +/// - `timing` must be a non-null pointer to a valid, mutable `TimingContext` instance. +/// - The caller must ensure that the memory referenced by `timing` remains valid for the duration of the function call. +/// - The returned pointer must be managed properly by the caller. It points to a heap-allocated `CcxDecodersXdsContext` instance, and the caller is responsible for freeing it using `Box::from_raw` when it is no longer needed. +/// - Failure to free the returned pointer will result in a memory leak. +/// - Passing a null or invalid pointer for `timing` will result in undefined behavior. +pub unsafe extern "C" fn ccxr_ccx_decoders_xds_init_library( + timing: *mut TimingContext, + xds_write_to_file: bool, +) -> *mut CcxDecodersXdsContext { + if timing.is_null() { + return std::ptr::null_mut(); + } + + let timing = unsafe { &mut *timing }; + + let ctx = CcxDecodersXdsContext::xds_init_library(timing.clone(), xds_write_to_file); + + Box::into_raw(Box::new(ctx)) +} + +#[no_mangle] +/// # Safety +/// - `ctx` must be a non-null pointer to a valid, mutable `CcxDecodersXdsContext` instance. +/// - `sub` must be a non-null pointer to a valid, mutable `CcSubtitle` instance. +/// - The caller must ensure that the memory referenced by `ctx` and `sub` remains valid for the duration of the function call. +/// - Passing null or invalid pointers will result in undefined behavior. +pub unsafe extern "C" fn ccxr_xds_cea608_test( + ctx: *mut CcxDecodersXdsContext, + sub: *mut CcSubtitle, +) { + if ctx.is_null() || sub.is_null() { + return; + } + + let ctx = unsafe { &mut *ctx }; + let sub = unsafe { &mut *sub }; + + xds_cea608_test(ctx, sub); +} diff --git a/src/rust/wrapper.h b/src/rust/wrapper.h index a7297478f..18b62075d 100644 --- a/src/rust/wrapper.h +++ b/src/rust/wrapper.h @@ -10,4 +10,4 @@ #include "../lib_ccx/lib_ccx.h" #include "../lib_ccx/hardsubx.h" #include "../lib_ccx/utility.h" -#include "../lib_ccx/ccx_encoders_helpers.h" +#include "../lib_ccx/ccx_encoders_helpers.h" \ No newline at end of file