diff --git a/docs/CHANGES.TXT b/docs/CHANGES.TXT index 214584c72..0c65a7fe5 100644 --- a/docs/CHANGES.TXT +++ b/docs/CHANGES.TXT @@ -1,5 +1,6 @@ 0.95 (to be released) ----------------- +- New: Add SCC support for CEA-708 decoder (#1595) - Fix: respect `-stdout` even if multiple CC tracks are present in a Matroska input file (#1453) - Fix: crash in Rust decoder on ATSC1.0 TS Files (#1407) - Removed the --with-gui flag for linux/configure and mac/configure (use the Flutter GUI instead) diff --git a/src/lib_ccx/ccx_common_timing.c b/src/lib_ccx/ccx_common_timing.c index d1fca4282..1fd123cb9 100644 --- a/src/lib_ccx/ccx_common_timing.c +++ b/src/lib_ccx/ccx_common_timing.c @@ -295,6 +295,34 @@ LLONG get_fts_max(struct ccx_common_timing_ctx *ctx) return ctx->fts_max + ctx->fts_global; } +/** + * SCC Time formatting + */ +size_t print_scc_time(struct ccx_boundary_time time, char *buf) +{ + char *fmt = "%02u:%02u:%02u;%02u"; + double frame; + + frame = ((double)(time.time_in_ms - 1000 * (time.ss + 60 * (time.mm + 60 * time.hh))) * 29.97 / 1000); + + return (size_t)sprintf(buf + time.set, fmt, time.hh, time.mm, time.ss, (unsigned)frame); +} + +struct ccx_boundary_time get_time(LLONG time) +{ + if (time < 0) // Avoid loss of data warning with abs() + time = -time; + + struct ccx_boundary_time result; + result.hh = (unsigned)(time / 1000 / 60 / 60); + result.mm = (unsigned)(time / 1000 / 60 - 60 * result.hh); + result.ss = (unsigned)(time / 1000 - 60 * (result.mm + 60 * result.hh)); + result.time_in_ms = time; + result.set = (time < 0 ? 1 : 0); + + return result; +} + /** * Fill buffer with a time string using specified format * @param fmt has to contain 4 format specifiers for h, m, s and ms respectively diff --git a/src/lib_ccx/ccx_common_timing.h b/src/lib_ccx/ccx_common_timing.h index f54f4302c..1f157173c 100644 --- a/src/lib_ccx/ccx_common_timing.h +++ b/src/lib_ccx/ccx_common_timing.h @@ -77,6 +77,8 @@ struct ccx_common_timing_ctx *init_timing_ctx(struct ccx_common_timing_settings_ void set_current_pts(struct ccx_common_timing_ctx *ctx, LLONG pts); void add_current_pts(struct ccx_common_timing_ctx *ctx, LLONG pts); +struct ccx_boundary_time get_time(LLONG mstime); +size_t print_scc_time(struct ccx_boundary_time time, char *buf); int set_fts(struct ccx_common_timing_ctx *ctx); LLONG get_fts(struct ccx_common_timing_ctx *ctx, int current_field); LLONG get_fts_max(struct ccx_common_timing_ctx *ctx); diff --git a/src/lib_ccx/ccx_decoders_708.h b/src/lib_ccx/ccx_decoders_708.h index 8cbd01f8b..0fe0dde7d 100644 --- a/src/lib_ccx/ccx_decoders_708.h +++ b/src/lib_ccx/ccx_decoders_708.h @@ -302,6 +302,7 @@ typedef struct dtvcc_tv_screen LLONG time_ms_hide; unsigned int cc_count; int service_number; + int old_cc_time_end; } dtvcc_tv_screen; /** diff --git a/src/lib_ccx/ccx_decoders_708_output.c b/src/lib_ccx/ccx_decoders_708_output.c index c1bf3faa2..5697ad242 100644 --- a/src/lib_ccx/ccx_decoders_708_output.c +++ b/src/lib_ccx/ccx_decoders_708_output.c @@ -367,6 +367,155 @@ void dtvcc_write_sami(dtvcc_writer_ctx *writer, dtvcc_service_decoder *decoder, write_wrapped(encoder->dtvcc_writers[tv->service_number - 1].fd, buf, strlen(buf)); } +unsigned char adjust_odd_parity(const unsigned char value) +{ + unsigned int i, ones = 0; + for (i = 0; i < 8; i++) + { + if ((value & (1 << i)) != 0) + { + ones += 1; + } + } + if (ones % 2 == 0) + { + // make the number of ones always odd + return value | 0b10000000; + } + return value; +} + +void dtvcc_write_scc_header(dtvcc_tv_screen *tv, struct encoder_ctx *encoder) +{ + char *buf = (char *)encoder->buffer; + // 18 characters long + 2 new lines + memset(buf, 0, 20); + sprintf(buf, "Scenarist_SCC V1.0\n\n"); + + write_wrapped(encoder->dtvcc_writers[tv->service_number - 1].fd, buf, strlen(buf)); +} + +int count_captions_lines_scc(dtvcc_tv_screen *tv) +{ + int count = 0; + for (int i = 0; i < CCX_DTVCC_SCREENGRID_ROWS; i++) + { + if (!dtvcc_is_row_empty(tv, i)) + { + count++; + } + } + + return count; +} + +/** This function is designed to assign appropriate SSC labels for positioning subtitles based on their length. + * In some scenarios where the video stream provides lengthy subtitles that cannot fit within a single line. + * Single-line subtitle can be placed in 15th row(most bottom row) + * 2 line length subtitles can be placed in 14th and 15th row + * 3 line length subtitles can be placed in 13th, 14th and 15th row + */ +void add_needed_scc_labels(char *buf, int total_subtitle_count, int current_subtitle_count) +{ + switch (total_subtitle_count) + { + case 1: + // row 15, column 00 + sprintf(buf + strlen(buf), " 94e0 94e0"); + break; + case 2: + // 9440: row 14, column 00 | 94e0: row 15, column 00 + sprintf(buf + strlen(buf), current_subtitle_count == 1 ? " 9440 9440" : " 94e0 94e0"); + break; + default: + // 13e0: row 13, column 04 | 9440: row 14, column 00 | 94e0: row 15, column 00 + sprintf(buf + strlen(buf), current_subtitle_count == 1 ? " 13e0 13e0" : (current_subtitle_count == 2 ? " 9440 9440" : " 94e0 94e0")); + } +} + +void dtvcc_write_scc(dtvcc_writer_ctx *writer, dtvcc_service_decoder *decoder, struct encoder_ctx *encoder) +{ + dtvcc_tv_screen *tv = decoder->tv; + + if (dtvcc_is_screen_empty(tv, encoder)) + return; + + if (tv->time_ms_show + encoder->subs_delay < 0) + return; + + if (tv->cc_count == 2) + dtvcc_write_scc_header(tv, encoder); + + char *buf = (char *)encoder->buffer; + struct ccx_boundary_time time_show = get_time(tv->time_ms_show + encoder->subs_delay); + // when hiding subtract a frame (1 frame = 34 ms) + struct ccx_boundary_time time_end = get_time(tv->time_ms_hide + encoder->subs_delay - 34); + + if (tv->old_cc_time_end > time_show.time_in_ms) + { + // Correct the frame delay + time_show.time_in_ms -= 1000 / 29.97; + print_scc_time(time_show, buf); + sprintf(buf + strlen(buf), "\t942c 942c"); + time_show.time_in_ms += 1000 / 29.97; + // Clear the buffer and start pop on caption + sprintf(buf + strlen(buf), "94ae 94ae 9420 9420"); + } + else if (tv->old_cc_time_end < time_show.time_in_ms) + { + // Clear the screen for new caption + struct ccx_boundary_time time_to_display = get_time(tv->old_cc_time_end); + print_scc_time(time_to_display, buf); + sprintf(buf + strlen(buf), "\t942c 942c \n\n"); + // Correct the frame delay + time_show.time_in_ms -= 1000 / 29.97; + // Clear the buffer and start pop on caption in new time + print_scc_time(time_show, buf); + sprintf(buf + strlen(buf), "\t94ae 94ae 9420 9420"); + time_show.time_in_ms += 1000 / 29.97; + } + else + { + time_show.time_in_ms -= 1000 / 29.97; + print_scc_time(time_show, buf); + sprintf(buf + strlen(buf), "\t942c 942c 94ae 94ae 9420 9420"); + time_show.time_in_ms += 1000 / 29.97; + } + + int total_subtitle_count = count_captions_lines_scc(tv); + int current_subtitle_count = 0; + + for (int i = 0; i < CCX_DTVCC_SCREENGRID_ROWS; i++) + { + if (!dtvcc_is_row_empty(tv, i)) + { + current_subtitle_count++; + add_needed_scc_labels(buf, total_subtitle_count, current_subtitle_count); + + int first, last, bytes_written = 0; + dtvcc_get_write_interval(tv, i, &first, &last); + for (int j = first; j <= last; j++) + { + if (bytes_written % 2 == 0) + sprintf(buf + strlen(buf), " "); + sprintf(buf + strlen(buf), "%x", adjust_odd_parity(tv->chars[i][j].sym)); + bytes_written += 1; + } + // if byte pair are not even then make it even by adding 0x80 as padding + if (bytes_written % 2 == 1) + sprintf(buf + strlen(buf), "80 "); + else + sprintf(buf + strlen(buf), " "); + } + } + + // Display caption (942f 942f) + sprintf(buf + strlen(buf), "942f 942f \n\n"); + write_wrapped(encoder->dtvcc_writers[tv->service_number - 1].fd, buf, strlen(buf)); + + tv->old_cc_time_end = time_end.time_in_ms; +} + void dtvcc_write(dtvcc_writer_ctx *writer, dtvcc_service_decoder *decoder, struct encoder_ctx *encoder) { switch (encoder->write_format) @@ -382,6 +531,9 @@ void dtvcc_write(dtvcc_writer_ctx *writer, dtvcc_service_decoder *decoder, struc case CCX_OF_SAMI: dtvcc_write_sami(writer, decoder, encoder); break; + case CCX_OF_SCC: + dtvcc_write_scc(writer, decoder, encoder); + break; case CCX_OF_MCC: printf("REALLY BAD... [%s:%d]\n", __FILE__, __LINE__); break; diff --git a/src/lib_ccx/ccx_decoders_708_output.h b/src/lib_ccx/ccx_decoders_708_output.h index 3b9f6759f..63544df79 100644 --- a/src/lib_ccx/ccx_decoders_708_output.h +++ b/src/lib_ccx/ccx_decoders_708_output.h @@ -8,11 +8,11 @@ void dtvcc_write_done(dtvcc_tv_screen *tv, struct encoder_ctx *encoder); void dtvcc_writer_init(dtvcc_writer_ctx *writer, - char *base_filename, - int program_number, - int service_number, - enum ccx_output_format write_format, - struct encoder_cfg *cfg); + char *base_filename, + int program_number, + int service_number, + enum ccx_output_format write_format, + struct encoder_cfg *cfg); void dtvcc_writer_cleanup(dtvcc_writer_ctx *writer); void dtvcc_writer_output(dtvcc_writer_ctx *writer, dtvcc_service_decoder *decoder, struct encoder_ctx *encoder); @@ -30,6 +30,9 @@ void dtvcc_write_transcript(dtvcc_writer_ctx *writer, dtvcc_service_decoder *dec void dtvcc_write_sami_header(dtvcc_tv_screen *tv, struct encoder_ctx *encoder); void dtvcc_write_sami_footer(dtvcc_tv_screen *tv, struct encoder_ctx *encoder); void dtvcc_write_sami(dtvcc_writer_ctx *writer, dtvcc_service_decoder *decoder, struct encoder_ctx *encoder); +void dtvcc_write_scc_header(dtvcc_tv_screen *tv, struct encoder_ctx *encoder); +void add_needed_scc_labels(char *buf, int total_subtitle_count, int current_subtitle_count); +void dtvcc_write_scc(dtvcc_writer_ctx *writer, dtvcc_service_decoder *decoder, struct encoder_ctx *encoder); void dtvcc_write(dtvcc_writer_ctx *writer, dtvcc_service_decoder *decoder, struct encoder_ctx *encoder); -#endif /*_CCX_DECODERS_708_OUTPUT_H_*/ \ No newline at end of file +#endif /*_CCX_DECODERS_708_OUTPUT_H_*/ diff --git a/src/rust/src/decoder/output.rs b/src/rust/src/decoder/output.rs index fe13f6ff5..fd0c96dc3 100644 --- a/src/rust/src/decoder/output.rs +++ b/src/rust/src/decoder/output.rs @@ -20,6 +20,7 @@ pub struct Writer<'a> { pub no_font_color: bool, pub transcript_settings: &'a ccx_encoders_transcript_format, pub no_bom: i32, + pub old_cc_time_end: i32, } impl<'a> Writer<'a> { @@ -42,6 +43,7 @@ impl<'a> Writer<'a> { no_font_color: is_true(no_font_color), transcript_settings, no_bom, + old_cc_time_end: 0, } } /// Write subtitles to the file diff --git a/src/rust/src/decoder/timing.rs b/src/rust/src/decoder/timing.rs index e65aad9e2..5702359ef 100644 --- a/src/rust/src/decoder/timing.rs +++ b/src/rust/src/decoder/timing.rs @@ -47,3 +47,31 @@ pub fn get_time_str(time: LLONG) -> String { let ms = time - 1000 * (ss + 60 * (mm + 60 * hh)); format!("{:02}:{:02}:{:02},{:03}", hh, mm, ss, ms) } + +impl ccx_boundary_time { + /// Returns ccx_boundary_time from given time + pub fn get_time(time: LLONG) -> Self { + let hh = time / 1000 / 60 / 60; + let mm = time / 1000 / 60 - 60 * hh; + let ss = time / 1000 - 60 * (mm + 60 * hh); + + Self { + hh: hh as i32, + mm: mm as i32, + ss: ss as i32, + time_in_ms: time, + set: Default::default(), + } + } +} + +/// Returns a hh:mm:ss;frame string of time for SCC format +pub fn get_scc_time_str(time: ccx_boundary_time) -> String { + // Feel sorry for formatting:( + let frame: u8 = (((time.time_in_ms + - 1000 * ((time.ss as i64) + 60 * ((time.mm as i64) + 60 * (time.hh as i64)))) + as f64) + * 29.97 + / 1000.0) as u8; + format!("{:02}:{:02}:{:02};{:02}", time.hh, time.mm, time.ss, frame) +} diff --git a/src/rust/src/decoder/tv_screen.rs b/src/rust/src/decoder/tv_screen.rs index 71f3f2b2f..43128b9bf 100644 --- a/src/rust/src/decoder/tv_screen.rs +++ b/src/rust/src/decoder/tv_screen.rs @@ -3,6 +3,7 @@ //! TV screen contains the captions to be displayed. //! Captions are added to TV screen from a window when any of DSW, HDW, TGW, DLW or CR commands are received +use std::cmp::Ordering; #[cfg(unix)] use std::os::unix::prelude::IntoRawFd; #[cfg(windows)] @@ -10,7 +11,7 @@ use std::os::windows::io::IntoRawHandle; use std::{ffi::CStr, fs::File}; use super::output::{color_to_hex, write_char, Writer}; -use super::timing::get_time_str; +use super::timing::{get_scc_time_str, get_time_str}; use super::{CCX_DTVCC_SCREENGRID_COLUMNS, CCX_DTVCC_SCREENGRID_ROWS}; use crate::{ bindings::*, @@ -128,6 +129,7 @@ impl dtvcc_tv_screen { ccx_output_format::CCX_OF_SRT => self.write_srt(writer), ccx_output_format::CCX_OF_SAMI => self.write_sami(writer), ccx_output_format::CCX_OF_TRANSCRIPT => self.write_transcript(writer), + ccx_output_format::CCX_OF_SCC => self.write_scc(writer), _ => { self.write_debug(); Err("Unsupported write format".to_owned()) @@ -358,6 +360,149 @@ impl dtvcc_tv_screen { Ok(()) } + fn count_captions_lines_scc(&self) -> usize { + (0..CCX_DTVCC_SCREENGRID_ROWS) + .filter(|&row_index| !self.is_row_empty(row_index as usize)) + .count() + } + + /// Write captions in SCC format + pub fn write_scc(&self, writer: &mut Writer) -> Result<(), String> { + fn adjust_odd_parity(value: u8) -> u8 { + let mut ones = 0; + for i in 0..=7 { + if value & (1 << i) != 0 { + ones += 1; + } + } + if ones % 2 == 0 { + 0b10000000 | value + } else { + value + } + } + // This function is designed to assign appropriate SSC labels for positioning subtitles based on their length. + // In some scenarios where the video stream provides lengthy subtitles that cannot fit within a single line. + // Single-line subtitle can be placed in 15th row(most bottom row) + // 2 line length subtitles can be placed in 14th and 15th row + // 3 line length subtitles can be placed in 13th, 14th and 15th row + fn add_needed_scc_labels( + buf: &mut String, + total_subtitle_count: usize, + current_subtitle_count: usize, + ) { + match total_subtitle_count { + // row 15, column 00 + 1 => buf.push_str(" 94e0 94e0"), + 2 => { + if current_subtitle_count == 1 { + // row 14, column 00 + buf.push_str(" 9440 9440"); + } else { + // row 15, column 00 + buf.push_str(" 94e0 94e0") + } + } + _ => { + if current_subtitle_count == 1 { + // row 13, column 04 + buf.push_str(" 13e0 13e0"); + } else if current_subtitle_count == 2 { + // row 14, column 00 + buf.push_str(" 9440 9440"); + } else { + // row 15, column 00 + buf.push_str(" 94e0 94e0") + } + } + } + } + if self.is_screen_empty(writer) { + return Ok(()); + } + + if self.time_ms_show + writer.subs_delay < 0 { + return Ok(()); + } + + if self.cc_count == 2 { + writer.write_to_file(b"Scenarist_SCC V1.0\n\n")?; + } + + if writer.old_cc_time_end == 0 { + writer.old_cc_time_end = self.time_ms_show as i32; + } + + let mut buf = String::new(); + let mut time_show = ccx_boundary_time::get_time(self.time_ms_show); + let time_end = ccx_boundary_time::get_time(self.time_ms_hide); + + // Caption overlapping situation + match writer.old_cc_time_end.cmp(&(time_show.time_in_ms as i32)) { + Ordering::Greater => { + // Correct the frame delay + time_show.time_in_ms -= 1000 / 29.97 as i64; + buf.push_str(&(get_scc_time_str(time_show) + "\t942c 942c ").to_owned()); + time_show.time_in_ms += 1000 / 29.97 as i64; + // Clear the buffer and start pop on caption + buf.push_str("94ae 94ae 9420 9420"); + } + Ordering::Less => { + // Clear the screen for new caption + let time_to_display = ccx_boundary_time::get_time(writer.old_cc_time_end as i64); + buf.push_str(&(get_scc_time_str(time_to_display) + "\t942c 942c \n\n").to_owned()); + // Correct the frame delay + time_show.time_in_ms -= 1000 / 29.97 as i64; + // Clear the buffer and start pop on caption in new time + buf.push_str(&(get_scc_time_str(time_show) + "\t94ae 94ae 9420 9420").to_owned()); + time_show.time_in_ms += 1000 / 29.97 as i64; + } + Ordering::Equal => { + time_show.time_in_ms -= 1000 / 29.97 as i64; + buf.push_str( + &(get_scc_time_str(time_show) + "\t942c 942c 94ae 94ae 9420 9420").to_owned(), + ); + time_show.time_in_ms += 1000 / 29.97 as i64; + } + } + + let total_subtitle_count = self.count_captions_lines_scc(); + let mut current_subtitle_count = 0; + + for row_index in 0..CCX_DTVCC_SCREENGRID_ROWS as usize { + if !self.is_row_empty(row_index) { + current_subtitle_count += 1; + add_needed_scc_labels(&mut buf, total_subtitle_count, current_subtitle_count); + + let (first, last) = self.get_write_interval(row_index); + debug!("First: {}, Last: {}", first, last); + + let mut bytes_written = 0; + for i in 0..last + 1 { + if bytes_written % 2 == 0 { + buf.push(' '); + } + let adjusted_val = adjust_odd_parity(self.chars[row_index][i].sym as u8); + buf = format!("{}{:x}", buf, adjusted_val); + bytes_written += 1; + } + // add 0x80 padding and form byte pair if the last byte pair is not form + if bytes_written % 2 == 1 { + buf.push_str("80 "); + } else { + buf.push(' '); + } + } + } + + // Display caption (942f 942f) + buf.push_str("942f 942f \n\n"); + writer.write_to_file(buf.as_bytes())?; + + writer.old_cc_time_end = time_end.time_in_ms as i32; + Ok(()) + } + /// Write debug messages /// /// Write all characters,show and hide time as a debug log