From 399b9397cb664046806c833a52d68884b6bcd3d3 Mon Sep 17 00:00:00 2001 From: Dhanush Varma Date: Thu, 18 Dec 2025 01:06:01 +0530 Subject: [PATCH 1/4] fix: scope SCC timing to encoder context (fixes test failures) - Add last_scc_tx_end_ms field to encoder_ctx - Initialize it to -1 in init_encoder - Use encoder->last_scc_tx_end_ms instead of static variable - This ensures SCC timing only affects SCC output format - Other formats (SRT, TXT, TTXT) are unaffected Fixes #1221 --- src/lib_ccx/ccx_decoders_708_output.c | 31 +++++++++++++++++++++++++++ src/lib_ccx/ccx_encoders_common.c | 1 + src/lib_ccx/ccx_encoders_common.h | 2 ++ 3 files changed, 34 insertions(+) diff --git a/src/lib_ccx/ccx_decoders_708_output.c b/src/lib_ccx/ccx_decoders_708_output.c index bda39e056..f3ddffdba 100644 --- a/src/lib_ccx/ccx_decoders_708_output.c +++ b/src/lib_ccx/ccx_decoders_708_output.c @@ -538,6 +538,36 @@ void dtvcc_write_scc(dtvcc_writer_ctx *writer, dtvcc_service_decoder *decoder, s // 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); + /* ---- SCC accurate timing: byte budgeting ---- */ + int bytes_required = 0; + + /* Control codes: RCL + ENM + EOC (each = 2 bytes) */ + bytes_required += 6; + + for (int i = 0; i < CCX_DTVCC_SCREENGRID_ROWS; i++) + { + if (!dtvcc_is_row_empty(tv, i)) + { + int first, last; + dtvcc_get_write_interval(tv, i, &first, &last); + bytes_required += (last - first + 1); + } + } + + /* Pad to even number of bytes */ + if (bytes_required % 2 != 0) + bytes_required++; + + /* ---- SCC accurate timing: scheduling ---- */ + int frames_required = (bytes_required + 1) / 2; + int tx_duration_ms = (int)(frames_required * (1000.0 / 29.97)); + + time_show.time_in_ms -= tx_duration_ms; + + /* ---- SCC accurate timing: collision handling ---- */ + if (encoder->last_scc_tx_end_ms >= 0 && time_show.time_in_ms < encoder->last_scc_tx_end_ms) + time_show.time_in_ms = encoder->last_scc_tx_end_ms; + #define SCC_SNPRINTF(fmt, ...) \ do \ { \ @@ -614,6 +644,7 @@ void dtvcc_write_scc(dtvcc_writer_ctx *writer, dtvcc_service_decoder *decoder, s #undef SCC_SNPRINTF write_wrapped(encoder->dtvcc_writers[tv->service_number - 1].fd, buf, strlen(buf)); + encoder->last_scc_tx_end_ms = time_show.time_in_ms + tx_duration_ms; tv->old_cc_time_end = time_end.time_in_ms; } diff --git a/src/lib_ccx/ccx_encoders_common.c b/src/lib_ccx/ccx_encoders_common.c index ac2af699a..916670ded 100644 --- a/src/lib_ccx/ccx_encoders_common.c +++ b/src/lib_ccx/ccx_encoders_common.c @@ -829,6 +829,7 @@ struct encoder_ctx *init_encoder(struct encoder_cfg *opt) ctx->dtvcc_extract = opt->dtvcc_extract; + ctx->last_scc_tx_end_ms = -1; ctx->segment_pending = 0; ctx->segment_last_key_frame = 0; ctx->nospupngocr = opt->nospupngocr; diff --git a/src/lib_ccx/ccx_encoders_common.h b/src/lib_ccx/ccx_encoders_common.h index 4540de681..e3d70e73a 100644 --- a/src/lib_ccx/ccx_encoders_common.h +++ b/src/lib_ccx/ccx_encoders_common.h @@ -157,6 +157,8 @@ struct encoder_ctx int sbs_enabled; // for dvb subs + + int last_scc_tx_end_ms; struct encoder_ctx *prev; int write_previous; // for dvb in .mkv From 6f013b7efe0dcc7584b323fad4808e78d9dd3449 Mon Sep 17 00:00:00 2001 From: Dhanush Varma Date: Thu, 18 Dec 2025 01:11:27 +0530 Subject: [PATCH 2/4] fix: use original display time for transmission end tracking The transmission end should be the original display time (when caption becomes visible), not the pre-rolled transmission start time. --- src/lib_ccx/ccx_decoders_708_output.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib_ccx/ccx_decoders_708_output.c b/src/lib_ccx/ccx_decoders_708_output.c index f3ddffdba..05c91d484 100644 --- a/src/lib_ccx/ccx_decoders_708_output.c +++ b/src/lib_ccx/ccx_decoders_708_output.c @@ -644,7 +644,7 @@ void dtvcc_write_scc(dtvcc_writer_ctx *writer, dtvcc_service_decoder *decoder, s #undef SCC_SNPRINTF write_wrapped(encoder->dtvcc_writers[tv->service_number - 1].fd, buf, strlen(buf)); - encoder->last_scc_tx_end_ms = time_show.time_in_ms + tx_duration_ms; + encoder->last_scc_tx_end_ms = tv->time_ms_show + encoder->subs_delay; tv->old_cc_time_end = time_end.time_in_ms; } From 154a4f1696c687d08853d6f4e565ccd4666b9f12 Mon Sep 17 00:00:00 2001 From: Dhanush Varma Date: Mon, 22 Dec 2025 23:40:15 +0530 Subject: [PATCH 3/4] fix: apply SCC accurate timing to 608 encoder path - Add byte budgeting calculation for 608 captions - Pre-roll transmission start time by transmission duration - Prevent collision with previous caption transmissions - Track transmission end time for collision detection This applies the same timing logic from the 708 path to fix issue #1221 for EIA-608 SCC output. --- src/lib_ccx/ccx_encoders_scc.c | 41 +++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/lib_ccx/ccx_encoders_scc.c b/src/lib_ccx/ccx_encoders_scc.c index fdeca183b..38c4f60fd 100644 --- a/src/lib_ccx/ccx_encoders_scc.c +++ b/src/lib_ccx/ccx_encoders_scc.c @@ -550,8 +550,44 @@ int write_cc_buffer_as_scenarist(const struct eia608_screen *data, struct encode unsigned char current_row = UINT8_MAX; unsigned char current_column = UINT8_MAX; + /* ---- SCC accurate timing: byte budgeting ---- */ + int bytes_required = 0; + + /* Control codes: RCL + ENM + EOC (each = 2 bytes) */ + bytes_required += 6; + + /* Count text bytes and control codes needed */ + for (uint8_t row = 0; row < 15; ++row) + { + if (!data->row_used[row]) + continue; + + int first, last; + find_limit_characters(data->characters[row], &first, &last, CCX_DECODER_608_SCREEN_WIDTH); + + /* Preamble code (2 bytes) + potential tab offset (2 bytes) */ + bytes_required += 4; + + /* Text characters */ + bytes_required += (last - first + 1); + } + + /* Pad to even number of bytes */ + if (bytes_required % 2 != 0) + bytes_required++; + + /* ---- SCC accurate timing: scheduling ---- */ + int frames_required = (bytes_required + 1) / 2; + int tx_duration_ms = (int)(frames_required * (1000.0 / 29.97)); + + LLONG adjusted_start_time = data->start_time - tx_duration_ms; + + /* ---- SCC accurate timing: collision handling ---- */ + if (context->last_scc_tx_end_ms >= 0 && adjusted_start_time < context->last_scc_tx_end_ms) + adjusted_start_time = context->last_scc_tx_end_ms; + // 1. Load the caption - add_timestamp(context, data->start_time, disassemble); + add_timestamp(context, adjusted_start_time, disassemble); write_control_code(context->out->fh, data->channel, RCL, disassemble, &bytes_written); for (uint8_t row = 0; row < 15; ++row) { @@ -624,6 +660,9 @@ int write_cc_buffer_as_scenarist(const struct eia608_screen *data, struct encode // 3. Clear the caption clear_screen(context, data->end_time, data->channel, disassemble); + + /* Track transmission end time */ + context->last_scc_tx_end_ms = adjusted_start_time + tx_duration_ms; return 1; } From aad0d78274cb8c244244f999d78730ff65ced34e Mon Sep 17 00:00:00 2001 From: Dhanush Varma Date: Mon, 22 Dec 2025 23:46:25 +0530 Subject: [PATCH 4/4] style: apply clang-format to modified files --- src/lib_ccx/ccx_encoders_scc.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib_ccx/ccx_encoders_scc.c b/src/lib_ccx/ccx_encoders_scc.c index 38c4f60fd..edaa876ea 100644 --- a/src/lib_ccx/ccx_encoders_scc.c +++ b/src/lib_ccx/ccx_encoders_scc.c @@ -660,7 +660,6 @@ int write_cc_buffer_as_scenarist(const struct eia608_screen *data, struct encode // 3. Clear the caption clear_screen(context, data->end_time, data->channel, disassemble); - /* Track transmission end time */ context->last_scc_tx_end_ms = adjusted_start_time + tx_duration_ms; return 1;