[FEATURE] Allow output \0 terminated frames (for WebSocket streaming support) (#2105)

* [feat] Allow output \0 terminated frames

* Fix rust `FromCType`

* use encoded_end_frame for text-based captions

* add changelog entry

* fix CEA-708 Rust decoder

* fix Rust formating

* remove unused `crlf` field - satisfy clippy function argument limit

* silence clippy function argument limit in `Writer`

* Fix writing frame end with multiline captions

* fix formatting errors
This commit is contained in:
pszemus
2026-03-19 02:16:43 +01:00
committed by GitHub
parent 9f250b144d
commit 03ad9e8e02
15 changed files with 88 additions and 16 deletions

View File

@@ -1,5 +1,6 @@
0.96.7 (unreleased)
-------------------
- New: Allow output \0 terminated frames via --null-terminated
- New: Added ASS/SSA \pos-based positioning for CEA-608 captions when layout is simple (12 rows) (#1726)
- Fix: Remove strdup() memory leaks in WebVTT styling encoder, fix invalid CSS rgba(0,256,0) green value, fix missing free(unescaped) on write-error path (#2154)
- Fix: Prevent crash in Rust timing module when logging out-of-range PTS/FTS timestamps from malformed streams.

View File

@@ -134,6 +134,7 @@ void init_options(struct ccx_s_options *options)
options->enc_cfg.trim_subs = 0; // " Remove spaces at sides? "
options->enc_cfg.in_format = 1;
options->enc_cfg.line_terminator_lf = 0; // 0 = CRLF
options->enc_cfg.frame_terminator_0 = 0; // 0 = frames terminated by line_terminator_lf
options->enc_cfg.start_credits_text = NULL;
options->enc_cfg.end_credits_text = NULL;
options->enc_cfg.encoding = CCX_ENC_UTF_8;

View File

@@ -67,6 +67,7 @@ struct encoder_cfg
int no_type_setting;
int cc_to_stdout; // If this is set to 1, the stdout will be flushed when data was written to the screen during a process_608 call.
int line_terminator_lf; // 0 = CRLF, 1=LF
int frame_terminator_0; // 0 = frames terminated by line_terminator_lf, 1 = frames terminated by \0
LLONG subs_delay; // ms to delay (or advance) subs
int program_number;
unsigned char in_format;

View File

@@ -843,6 +843,17 @@ struct encoder_ctx *init_encoder(struct encoder_cfg *opt)
ctx->encoded_br_length = encode_line(ctx, ctx->encoded_br, (unsigned char *)"<br>");
if (opt->frame_terminator_0)
{
ctx->encoded_end_frame[0] = '\0';
ctx->encoded_end_frame_length = 1;
}
else
{
memcpy(ctx->encoded_end_frame, ctx->encoded_crlf, ctx->encoded_crlf_length + 1);
ctx->encoded_end_frame_length = ctx->encoded_crlf_length;
}
for (i = 0; i < ctx->nb_out; i++)
write_subtitle_file_header(ctx, ctx->out + i);

View File

@@ -146,6 +146,8 @@ struct encoder_ctx
unsigned int encoded_crlf_length;
unsigned char encoded_br[16];
unsigned int encoded_br_length;
unsigned char encoded_end_frame[16];
unsigned int encoded_end_frame_length;
// MCC File
int header_printed_flag;

View File

@@ -92,7 +92,7 @@ int write_cc_bitmap_as_transcript(struct cc_subtitle *sub, struct encoder_ctx *c
}
}
write_wrapped(context->out->fh, context->encoded_crlf, context->encoded_crlf_length);
write_wrapped(context->out->fh, context->encoded_end_frame, context->encoded_end_frame_length);
}
}
#endif
@@ -134,6 +134,7 @@ int write_cc_subtitle_as_transcript(struct cc_subtitle *sub, struct encoder_ctx
str = sub->data;
int wrote_something = 0;
str = strtok_r(str, "\r\n", &save_str);
do
{
@@ -143,6 +144,15 @@ int write_cc_subtitle_as_transcript(struct cc_subtitle *sub, struct encoder_ctx
continue;
}
if (wrote_something)
{
ret = write(context->out->fh, context->encoded_crlf, context->encoded_crlf_length);
if (ret < context->encoded_crlf_length)
{
mprint("Warning:Loss of data\n");
}
}
if (context->transcript_settings->showStartTime)
{
char buf[80];
@@ -200,14 +210,16 @@ int write_cc_subtitle_as_transcript(struct cc_subtitle *sub, struct encoder_ctx
mprint("Warning:Loss of data\n");
}
ret = write(context->out->fh, context->encoded_crlf, context->encoded_crlf_length);
if (ret < context->encoded_crlf_length)
{
mprint("Warning:Loss of data\n");
}
wrote_something = 1;
} while ((str = strtok_r(NULL, "\r\n", &save_str)));
ret = write(context->out->fh, context->encoded_end_frame, context->encoded_end_frame_length);
if (ret < context->encoded_end_frame_length)
{
mprint("Warning:Loss of data\n");
}
freep(&sub->data);
lsub = sub;
sub = sub->next;
@@ -321,18 +333,13 @@ void write_cc_line_as_transcript2(struct eia608_screen *data, struct encoder_ctx
{
mprint("Warning:Loss of data\n");
}
ret = write(context->out->fh, context->encoded_crlf, context->encoded_crlf_length);
if (ret < context->encoded_crlf_length)
{
mprint("Warning:Loss of data\n");
}
}
// fprintf (wb->fh,encoded_crlf);
}
int write_cc_buffer_as_transcript2(struct eia608_screen *data, struct encoder_ctx *context)
{
int ret;
int wrote_something = 0;
dbg_print(CCX_DMT_DECODER_608, "\n- - - TRANSCRIPT caption - - -\n");
@@ -340,10 +347,29 @@ int write_cc_buffer_as_transcript2(struct eia608_screen *data, struct encoder_ct
{
if (data->row_used[i])
{
if (wrote_something)
{
ret = write(context->out->fh, context->encoded_crlf, context->encoded_crlf_length);
if (ret < context->encoded_crlf_length)
{
mprint("Warning:Loss of data\n");
}
}
write_cc_line_as_transcript2(data, context, i);
wrote_something = 1;
}
wrote_something = 1;
}
if (wrote_something)
{
ret = write(context->out->fh, context->encoded_end_frame, context->encoded_end_frame_length);
if (ret < context->encoded_end_frame_length)
{
mprint("Warning:Loss of data\n");
}
}
dbg_print(CCX_DMT_DECODER_608, "- - - - - - - - - - - -\r\n");
return wrote_something;
}

View File

@@ -347,6 +347,9 @@ void print_usage(void)
mprint(" to the output file.\n");
mprint(" --lf: Use LF (UNIX) instead of CRLF (DOS, Windows) as line\n");
mprint(" terminator.\n");
mprint(" --null-terminated: Use \\0 instead of CRLF or LF for frame termination (see '--lf').\n");
mprint(" e.g use '--txt --stdout --null-terminated' when piping to\n");
mprint(" 'websocat -0' (https://github.com/vi/websocat)\n");
mprint(" --df: For MCC Files, force dropframe frame count.\n");
mprint(" --autodash: Based on position on screen, attempt to determine\n");
mprint(" the different speakers and a dash (-) when each\n");

View File

@@ -248,6 +248,7 @@ impl Default for EncoderConfig {
no_type_setting: false,
cc_to_stdout: false,
line_terminator_lf: false,
frame_terminator_0: false,
subs_delay: Timestamp::default(),
program_number: 0,
in_format: 1,
@@ -324,6 +325,8 @@ pub struct EncoderConfig {
pub cc_to_stdout: bool,
/// false = CRLF, true = LF
pub line_terminator_lf: bool,
/// false = frames terminated by line_terminator_lf, true = frames terminated by \0
pub frame_terminator_0: bool,
/// ms to delay (or advance) subs
pub subs_delay: Timestamp,
pub program_number: u32,

View File

@@ -565,6 +565,9 @@ pub struct Args {
/// terminator.
#[arg(long, verbatim_doc_comment, help_heading=OUTPUT_AFFECTING_OUTPUT_FILES)]
pub lf: bool,
/// Use \0 instead of CRLF or LF for frame termination (see '--lf')
#[arg(long, verbatim_doc_comment, help_heading=OUTPUT_AFFECTING_OUTPUT_FILES)]
pub null_terminated: bool,
/// For MCC Files, force dropframe frame count.
#[arg(long, verbatim_doc_comment, help_heading=OUTPUT_AFFECTING_OUTPUT_FILES)]
pub df: bool,

View File

@@ -960,6 +960,7 @@ impl CType<encoder_cfg> for EncoderConfig {
no_type_setting: self.no_type_setting as _,
cc_to_stdout: self.cc_to_stdout as _,
line_terminator_lf: self.line_terminator_lf as _,
frame_terminator_0: self.frame_terminator_0 as _,
subs_delay: self.subs_delay.millis(),
program_number: self.program_number as _,
in_format: self.in_format,

View File

@@ -442,6 +442,7 @@ impl FromCType<encoder_cfg> for EncoderConfig {
no_type_setting: cfg.no_type_setting != 0,
cc_to_stdout: cfg.cc_to_stdout != 0,
line_terminator_lf: cfg.line_terminator_lf != 0,
frame_terminator_0: cfg.frame_terminator_0 != 0,
subs_delay: Timestamp::from_millis(cfg.subs_delay),
program_number: cfg.program_number as u32,
in_format: cfg.in_format,

View File

@@ -14,7 +14,7 @@ use log::{debug, warn};
pub struct Writer<'a> {
pub cea_708_counter: &'a mut u32,
pub subs_delay: LLONG,
pub crlf: String,
pub end_frame: Vec<u8>,
pub write_format: ccx_output_format,
pub writer_ctx: &'a mut dtvcc_writer_ctx,
pub no_font_color: bool,
@@ -25,6 +25,7 @@ pub struct Writer<'a> {
impl<'a> Writer<'a> {
/// Create a new writer context
#[allow(clippy::too_many_arguments)]
pub fn new(
cea_708_counter: &'a mut u32,
subs_delay: LLONG,
@@ -33,11 +34,12 @@ impl<'a> Writer<'a> {
no_font_color: i32,
transcript_settings: &'a ccx_encoders_transcript_format,
no_bom: i32,
encoded_end_frame: &[u8],
) -> Self {
Self {
cea_708_counter,
subs_delay,
crlf: "\r\n".to_owned(),
end_frame: encoded_end_frame.to_vec(),
write_format,
writer_ctx,
no_font_color: is_true(no_font_color),

View File

@@ -873,6 +873,7 @@ impl dtvcc_service_decoder {
} else {
&ccx_encoders_transcript_format::default()
};
let end_frame = &encoder.encoded_end_frame[..encoder.encoded_end_frame_length as usize];
let mut writer = Writer::new(
&mut encoder.cea_708_counter,
encoder.subs_delay,
@@ -881,6 +882,7 @@ impl dtvcc_service_decoder {
encoder.no_font_color,
transcript_settings,
encoder.no_bom,
end_frame,
);
tv.writer_output(&mut writer).unwrap();
tv.clear();
@@ -1202,6 +1204,7 @@ impl dtvcc_service_decoder {
let sn = tv.service_number;
let writer_ctx = &mut encoder.dtvcc_writers[(sn - 1) as usize];
let end_frame = &encoder.encoded_end_frame[..encoder.encoded_end_frame_length as usize];
let mut writer = Writer::new(
&mut encoder.cea_708_counter,
encoder.subs_delay,
@@ -1210,6 +1213,7 @@ impl dtvcc_service_decoder {
encoder.no_font_color,
transcript_settings,
encoder.no_bom,
end_frame,
);
writer.write_done();
}

View File

@@ -330,8 +330,13 @@ impl dtvcc_tv_screen {
let time_show = get_time_str(self.time_ms_show);
let time_hide = get_time_str(self.time_ms_hide);
let mut wrote_something = false;
for row_index in 0..CCX_DTVCC_SCREENGRID_ROWS as usize {
if !self.is_row_empty(row_index) {
if wrote_something {
writer.write_to_file(b"\r\n")?;
}
let mut buf = String::new();
if is_true(writer.transcript_settings.showStartTime) {
@@ -350,9 +355,11 @@ impl dtvcc_tv_screen {
}
writer.write_to_file(buf.as_bytes())?;
self.write_row(writer, row_index, false)?;
writer.write_to_file(b"\r\n")?;
wrote_something = true;
}
}
let end_frame = writer.end_frame.clone();
writer.write_to_file(&end_frame)?;
Ok(())
}
@@ -800,6 +807,7 @@ mod test {
0,
&transcript_settings,
0,
b"\r\n",
);
// This should succeed without error (fd is valid, not -1)
@@ -835,6 +843,7 @@ mod test {
0,
&transcript_settings,
0,
b"\r\n",
);
// This should return an error, not panic

View File

@@ -1257,6 +1257,10 @@ impl OptionsExt for Options {
self.enc_cfg.line_terminator_lf = true;
}
if args.null_terminated {
self.enc_cfg.frame_terminator_0 = true;
}
if args.df {
self.enc_cfg.force_dropframe = true;
}