Skip to content

Conversation

@ujjwalr27
Copy link
Contributor

In raising this pull request, I confirm the following (please check boxes):

  • I have read and understood the contributors guide.
  • I have checked that another pull request for this purpose does not exist.
  • I have considered, and confirmed that this submission will be valuable to others.
  • I accept that this submission may not be used, and the pull request closed at the will of the maintainer.
  • I give this submission freely, and claim no ownership to its content.
  • I have mentioned this change in the changelog.

My familiarity with the project is as follows (check one):

  • I have never used CCExtractor.
  • I have used CCExtractor just a couple of times.
  • I absolutely love CCExtractor, but have not contributed previously.
  • I am an active contributor to CCExtractor.

Summary

This PR implements bandwidth-aware timing for SCC (Scenarist Closed Caption) output to ensure YouTube and broadcast compliance, addressing issue #1221.

Problem

SCC files generated by CCExtractor were being rejected by YouTube due to timing violations. The EIA-608 standard specifies a transmission bandwidth of 2 bytes per frame, but CCExtractor was not accounting for the time required to transmit captions before their display time, causing overlapping transmissions and compliance issues.

Solution

Added a new --scc-accurate-timing command-line option that implements:

  1. Byte counting: Calculates the exact byte size of each caption including control codes, preambles, mid-row codes, text, and padding
  2. Pre-roll calculation: Determines when caption transmission must start based on byte size and frame rate to ensure it completes before display time
  3. Collision detection & resolution: Detects overlapping caption transmissions and shifts captions forward to resolve conflicts
  4. Frame-perfect EOC timing: Ensures End-of-Caption codes are sent one frame before display time

Changes Made

C Code

  • ccx_common_option.h/c: Added scc_accurate_timing option (default: off)
  • ccx_encoders_common.h/c: Added timing state tracking (scc_last_transmission_end, scc_last_display_end)
  • [ccx_encoders_scc.c]: Implemented core timing logic:
    • [calculate_caption_bytes()]: Byte counting
    • [calculate_preroll_time()]: Pre-roll timing calculation
    • [resolve_collision()]: Collision detection and resolution
    • Modified [write_cc_buffer_as_scenarist()]: Integrated accurate timing when flag is enabled

Rust Code

  • [args.rs]: Added --scc-accurate-timing CLI argument
  • [parser.rs]: Added argument parsing logic
  • [options.rs]: Added field to Options struct
  • [common.rs]: Updated C/Rust FFI bindings

Usage

ccextractor input.ts -out=scc --scc-accurate-timing -o output.scc

@cfsmp3
Copy link
Contributor

cfsmp3 commented Jan 2, 2026

How did you test this?

@ujjwalr27
Copy link
Contributor Author

I tested using the Broadcast Source sample from issue #1120:

Results (First Caption):

Output Timestamp
Reference (scc_tools) 00:00:04:22 (load) → 00:00:06:10 (display)
Master (no flag) 00:00:06:12
--scc-accurate-timing 00:00:05:29

@cfsmp3
Copy link
Contributor

cfsmp3 commented Jan 3, 2026

I tested using the Broadcast Source sample from issue #1120:

Results (First Caption):

Output Timestamp
Reference (scc_tools) 00:00:04:22 (load) → 00:00:06:10 (display)
Master (no flag) 00:00:06:12
--scc-accurate-timing 00:00:05:29

That only tests that there's been a change (on the first caption). How did you test that the output is now valid and that is in sync with audio?

@ujjwalr27
Copy link
Contributor Author

I tested the output by uploading the generated SCC file to YouTube video of the broadcast sample:(https://www.youtube.com/watch?v=MLcqTwihpOQ) , which accepted it without any validation errors - previously it was rejected with timestamp ordering issues. You can verify the audio sync by watching the video with English subtitles enabled; the captions appear synchronized with the spoken dialogue. I also compared the output against the reference scc_tools implementation and confirmed the timing matches within 1-2 frames .

@cfsmp3
Copy link
Contributor

cfsmp3 commented Jan 3, 2026

Code Review: --scc-accurate-timing

Thanks for the detailed implementation and the YouTube validation. I've done a thorough code review.

Where the 1-2 frame difference comes from

The "timing matches within 1-2 frames" you mentioned comes from several intentional design choices in your implementation:

  1. Deliberate 1-frame buffer in calculate_preroll_time() (line ~130 in your diff):
LLONG preroll_start = display_time - transmission_time_ms - one_frame_ms;

You explicitly subtract one extra frame to ensure EOC is sent before display time.

  1. Another 1-frame buffer in resolve_collision() (line ~150):
LLONG new_preroll = context->scc_last_transmission_end + one_frame_ms;

When resolving bandwidth collisions, you add another frame as a safety buffer.

  1. Integer truncation throughout:
    • (LLONG)(frames_needed * ms_per_frame) truncates fractional milliseconds
    • (total_bytes + 1) / 2 rounds up bytes to frames
    • (int)(milli * fps / 1000.0f) truncates in frame calculation

So the 1-2 frame difference is by design - you're building in safety margins. This is actually good for broadcast compliance.

Questions/Concerns

  1. Missing EDM (clear) in accurate timing mode: You skip writing the clear command when use_separate_display_time is true:
if (!use_separate_display_time)
{
    // Legacy mode: always write clear
    clear_screen(context, actual_end_time, data->channel, disassemble);
}

You mention "scc_tools doesn't write EDM between consecutive captions" - but what happens if there's a gap between captions? Will the previous caption stay on screen until the next one appears? This could be an issue for sparse captions.

  1. Byte counting accuracy: Your calculate_caption_bytes() estimates the byte cost, but it may not perfectly match what write_cc_buffer_as_scenarist() actually writes because:

    • The actual function has optimization paths (styled PACs at column 0)
    • Position code logic differs based on current cursor position

    Is there a risk of under-estimating and having insufficient pre-roll?

  2. The CI shows DVB 6/7 - is that a known infrastructure issue or related to this PR?

What I can verify

  • ✅ The math for EIA-608 bandwidth (2 bytes/frame) is correct
  • ✅ The frame rate handling matches existing code
  • ✅ The option is off by default (backwards compatible)
  • ✅ The Rust/C FFI bindings look correct
  • ✅ The frame capping in add_timestamp() is a good fix

What I cannot verify without the sample

  • The actual YouTube acceptance (I'll take your word for it since you uploaded it)
  • Whether the timing matches audio sync precisely

Could you clarify the EDM/clear behavior question above? If that's intentional and works correctly, this looks ready to merge.

@ccextractor-bot
Copy link
Collaborator

CCExtractor CI platform finished running the test files on linux. Below is a summary of the test results, when compared to test for commit dfaebd5...:
Report Name Tests Passed
Broken 13/13
CEA-708 14/14
DVB 6/7
DVD 3/3
DVR-MS 2/2
General 27/27
Hardsubx 1/1
Hauppage 3/3
MP4 3/3
NoCC 10/10
Options 86/86
Teletext 21/21
WTV 13/13
XDS 34/34

Your PR breaks these cases:

  • ccextractor --autoprogram --out=srt --latin1 --quant 0 85271be4d2...

Congratulations: Merging this PR would fix the following tests:

  • ccextractor --autoprogram --out=ttxt --latin1 --ucla dab1c1bd65..., Last passed: Never
  • ccextractor --out=srt --latin1 --autoprogram 29e5ffd34b..., Last passed: Never
  • ccextractor --startcreditstext "CCextractor Start crdit Testing" c4dd893cb9..., Last passed: Never
  • ccextractor --startcreditsnotbefore 1 --startcreditstext "CCextractor Start crdit Testing" c4dd893cb9..., Last passed: Never
  • ccextractor --startcreditsnotafter 2 --startcreditstext "CCextractor Start crdit Testing" c4dd893cb9..., Last passed: Never
  • ccextractor --startcreditsforatleast 1 --startcreditstext "CCextractor Start crdit Testing" c4dd893cb9..., Last passed: Never
  • ccextractor --startcreditsforatmost 2 --startcreditstext "CCextractor Start crdit Testing" c4dd893cb9..., Last passed: Never

It seems that not all tests were passed completely. This is an indication that the output of some files is not as expected (but might be according to you).

Check the result page for more info.

@ccextractor-bot
Copy link
Collaborator

CCExtractor CI platform finished running the test files on windows. Below is a summary of the test results, when compared to test for commit dfaebd5...:
Report Name Tests Passed
Broken 13/13
CEA-708 14/14
DVB 7/7
DVD 3/3
DVR-MS 2/2
General 27/27
Hardsubx 1/1
Hauppage 3/3
MP4 3/3
NoCC 10/10
Options 86/86
Teletext 21/21
WTV 13/13
XDS 34/34

Congratulations: Merging this PR would fix the following tests:

  • ccextractor --autoprogram --out=srt --latin1 --quant 0 85271be4d2..., Last passed: Never
  • ccextractor --autoprogram --out=ttxt --latin1 --ucla dab1c1bd65..., Last passed: Never
  • ccextractor --out=srt --latin1 --autoprogram 29e5ffd34b..., Last passed: Never
  • ccextractor --out=spupng c83f765c66..., Last passed: Never
  • ccextractor --startcreditstext "CCextractor Start crdit Testing" c4dd893cb9..., Last passed: Never
  • ccextractor --startcreditsnotbefore 1 --startcreditstext "CCextractor Start crdit Testing" c4dd893cb9..., Last passed: Never
  • ccextractor --startcreditsnotafter 2 --startcreditstext "CCextractor Start crdit Testing" c4dd893cb9..., Last passed: Never
  • ccextractor --startcreditsforatleast 1 --startcreditstext "CCextractor Start crdit Testing" c4dd893cb9..., Last passed: Never
  • ccextractor --startcreditsforatmost 2 --startcreditstext "CCextractor Start crdit Testing" c4dd893cb9..., Last passed: Never

All tests passed completely.

Check the result page for more info.

@cfsmp3 cfsmp3 merged commit 8c33412 into CCExtractor:master Jan 3, 2026
23 of 24 checks passed
@ujjwalr27
Copy link
Contributor Author

byte estimation is intentionally conservative . i think For most video content , captions are close together, so the next EOC naturally handles the transition , If this becomes an issue, I could add a threshold check: only skip EDM if the gap is small (e.g., < 2 seconds), otherwise write EDM.

Proposed fix if needed:

// Only skip clear if gap to next caption is small
if (!use_separate_display_time || (actual_end_time - actual_start_time > 2000))
{
    clear_screen(context, actual_end_time, data->channel, disassemble);
}

The test failure is unrelated to my changes. My PR only modifies the SCC encoder when --scc-accurate-timing is used. The test uses -out=srt with DVB subtitles, which doesn't touch any of the code I modified.

@ujjwalr27 ujjwalr27 deleted the scc-accurate-timing branch January 7, 2026 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants