fix(cc608): flip each caption on its cue's first frame, with a -sc opt-out - #326
Closed
tobbee wants to merge 5 commits into
Closed
fix(cc608): flip each caption on its cue's first frame, with a -sc opt-out#326tobbee wants to merge 5 commits into
tobbee wants to merge 5 commits into
Conversation
This was referenced Jul 26, 2026
Closed
A pop-on caption needs a build written into non-displayed memory plus an EOC that flips it on screen, and both drain at one 608 pair per frame. BuildUnitCues starts the build at its own cue's first frame, so the flip landed 15-20 frames into the cue - 0.6-0.75s of a one-second cue - and the caption became visible well after the time it displays. Place each cue's EOC on its cue's first frame and transmit the build over the preceding frames instead, so the flip coincides with the cue boundary and a caption is shown over exactly the interval its text names. This is driven directly off go-608's schedule.Scheduler, whose FIFO is gated by eligibility time, so pushing build-then-EOC keeps the byte stream ordered and lands the flip on the intended frame. Captions now span unit boundaries: a fragment carries the build for the first cue of whatever follows it, which is why the next unit's segment number is passed down. A client starting or seeking mid-stream gets a leading EOC without its build and shows no caption for that first cue period, then is correct. Each cue is encoded with a fresh encoder so its build always fully describes its screen, which is what lets independently generated on-demand segments line up. The 608 data rate is unchanged at one pair per frame.
…ELOG Move the explanation of where a cue's build and flip sit, and what that costs at a discontinuity, into the README next to the timecc608 description, and cut the CHANGELOG entry down to a summary pointing there. Correct one claim while doing it: a client hitting a segment's leading EOC without its build was described as showing no caption for that cue period, which only holds for a fresh 608 decoder. One that keeps state across a seek flips whatever was last preloaded and can show a stale caption instead. Neither can be prevented server-side, since an ENM ahead of the EOC would erase the build about to be flipped.
The stale-caption case after a seek is fixed by the player resetting its 608 decoder state at a discontinuity, as it would any other decoder. The server has no lever here, so livesim2 keeps flipping each segment's first cue rather than skipping it.
tobbee
force-pushed
the
fix/cc608-cue-boundary-flip
branch
from
July 28, 2026 07:17
d0032da to
5d43eac
Compare
The caption clock must come from the segment's own media time, not from segmentNr * segmentDuration. startnr_ renumbers the segments and a non-zero availabilityStartTime shifts the wall clock without touching the numbers, so either option decouples the two. Add TestGenLiveSegmentCC608NrTimeOffset with a startnr_5 case (segment 45 is the 80 s segment, so the clock reads 00:01:2x) and an AST=3600 case (same media and numbers, clocks an hour later), both decoding real B-frame segments end to end. Computing the clock from meta.newNr * meta.newDur instead makes only the startnr_5 case fail, at 00:01:31.000.
Flipping each caption on its cue's first frame is frame-accurate but makes a caption span the segment boundary, which costs a client that starts, seeks or joins mid-stream its first cue period. Make that trade selectable per stream: timecc608_CC1-eng-sc keeps a cue's build and flip inside the cue's own frames, so every segment is independently decodable, at the price of the caption appearing ~0.5 s into the second its clock names (measured: flips at frames 15 and 45 for cue boundaries 0 and 30 at 30 fps). The default is unchanged. The grammar is now <channel>-<lang>[-sc]. A trailing sc is only the flag when a language precedes it, so CC1-sc stays the Sardinian tag, CC1-sc-sc is that language self-contained, and CC1-eng-xx remains the language eng-xx; only a tag whose final subtag is literally sc cannot be expressed. cc608UnitFrames takes a cc608FlipMode and keeps the fresh-encoder-per-cue rebuild in both modes rather than delegating to go-608's generate.BuildUnitCues + WithFlipAtCueStart: BuildUnitCues shares one encoder across a unit's cues, so cues after the first are diffs, and a receiver joining mid-unit would flip a diff and see one row of a two-row caption. /urlgen documents the option and its trade-off; the field already accepted it since it validates through CreateCC608Config.
Contributor
Author
|
Superseded by #329, which generalises this. Rather than only moving the pop-on flip onto its cue's first frame, The default is paint-on, so segments are independently decodable out of the box; the flip-at-cue-start placement from this PR lives on as |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #325.
What & why
A pop-on CTA-608 caption is two transmissions: a build (RCL + ENM + rows) written into non-displayed memory,
and an EOC that flips it on screen. Both drain at one 608 pair per frame, so where the build sits decides when
the caption appears.
generate.BuildUnitCuesstarts the build at its own cue's first frame and lets the EOC follow, so — as its owncomment notes — the flip lands at
start+pairs. For the two-row clock cue (~16-19 pairs) that is 0.6-0.75 sinto the very second the caption names, and the caption then straddles the boundary into the next cue. The
timestamp and segment number visibly lagged the picture.
This places each cue's EOC on its cue's first frame and transmits the build over the frames immediately
before it, so the flip coincides with the cue boundary and a caption is displayed over exactly the interval its
text names.
That accuracy costs self-contained segments, which is a genuine trade rather than a strict improvement, so the
previous behaviour stays reachable per stream:
timecc608_CC1-eng-sckeeps every caption inside the segmentthat carries it. The default is the fix;
-scis the opt-out. SeeOpting out below.
How
cc608UnitFramesdrives go-608'sschedule.Schedulerdirectly instead of callingBuildUnitCues. Thescheduler's FIFO is gated by eligibility time, so pushing build-then-EOC keeps the 608 byte stream ordered and
lands the flip on the intended frame. Everything used is already exported by go-608 v0.6.0 — no dependency bump.
Two consequences, both deliberate:
next fragment, or the next segment for a final fragment — which is why the next unit's segment number is now
passed down through
injectCC608. A client that starts, seeks, or joins mid-stream receives a leading EOCwithout the build that belongs to it, and what it shows for that cue period depends on its decoder: a fresh 608
decoder has nothing loaded and shows no caption, while one that keeps 608 state across the discontinuity
flips whatever was last preloaded and can show one cue of a stale caption. Either way it corrects at the next
cue boundary, typically within a second. Within this mode the server cannot avoid it — any pair sent ahead of
the EOC to clear the state would erase the build about to be flipped — which is why the behaviour is selectable
rather than simply replaced: a client that needs segment-independent captions asks for
-sc. This is a realchange for seeking clients, and it is the argument for the open question in CTA-608 captions are displayed ~0.6-0.76 s after the time they name #325 about skipping a segment's
first cue instead of flipping it.
cta608.Encoder(in both modes), so a build always fully describes itsscreen. This is what lets independently generated, on-demand segments line up: the build in segment N's tail and
the flip at segment N+1's first frame come from separate requests and must agree without shared encoder state.
It is also what lets a receiver joining mid-unit get a whole caption rather than one row of one.
A build that cannot fit between the previous flip and its own is now an explicit error rather than a silently
dropped build (which would leave an EOC with nothing loaded, i.e. a caption that never appears).
The 608 data rate is unchanged at one pair per frame, so
cc_countstaysround(600/fps)and the stream remainsin line with the A/53 / SCTE-128 rate model. The alternative — bursting a whole cue into the cue's first frame —
was rejected for that reason, and because consumers that trust parser-reported cue times would mistime it: the
SVTA
@svta/cml-608parser (the dash.js/hls.js decoder) advances its internal clock ~33.4 ms per byte pairwithin one
addDatacall, so a ~19-pair burst reads as ~0.6 s late.The README's
timecc608description now covers where a cue's build and flip sit and what that costs at adiscontinuity; the CHANGELOG entry is a short summary pointing there.
Opting out:
timecc608_CC1-eng-scBecause the mid-stream-join cost above is a real trade rather than a strict improvement, the previous behaviour is
kept available per stream. The
timecc608grammar becomes<channel>-<lang>[-sc]:timecc608_CC1-eng(default) — flip on the cue's first frame. Frame-accurate; captions span the segment boundary.timecc608_CC1-eng-sc— self-contained: a cue's build and its flip ride the cue's own frames, so everysegment is independently decodable and a client starting, seeking or joining anywhere sees a whole caption at
once. The cost is that each caption appears ~0.5 s into the second its clock names (measured: flips at frames 15
and 45 for cue boundaries 0 and 30 at 30 fps).
A trailing
sccounts as the flag only when a language precedes it, so a hyphenated language tag still parses:CC1-scis the Sardinian tag,CC1-sc-scis that language self-contained,CC1-en-US-scworks, andCC1-eng-xxstays the language
eng-xx. Only a tag whose final subtag is literallysccannot be expressed.cc608UnitFramestakes acc608FlipModeand keeps the fresh-encoder-per-cue rebuild in both modes, ratherthan delegating the two behaviours to go-608 v0.7.0's
generate.BuildUnitCueswith and withoutWithFlipAtCueStart.BuildUnitCuesshares onecta608.Encoderacross a unit's cues, so cues after the first aretransmitted as diffs; a receiver joining mid-unit would then flip a diff and see one row of a two-row caption — and
no test would catch it, since a decoder that saw cue 0 applies the diff correctly. Adopting the upstream helper
remains possible, just not without giving up that property.
/urlgendocuments the option and its trade-off. The field itself needed no change: it is free text validatedthrough
CreateCC608Config.Testing
go test ./...andgolangci-lintare green. The caption tests now decode two consecutive units as onestream, which is the only way to observe the cross-boundary build:
TestInjectCC608AVC/HEVC— flips land on frames 30, 60, 90 of the two-unit stream, and the frame-60 flip(unit B's first cue, built in unit A's tail) carries unit B's segment number.
TestGenLiveSegmentCC608— two realtestpic_2s/V300segments throughgenLiveSegmentplus an encoderound-trip:
00:01:21.000/SEG 40,00:01:22.000/SEG 41,00:01:23.000/SEG 41.TestGenLiveSegmentCC608HEVC— same forbbb_hevc_ac3_8sat 24 fps (frames 24/48/72).TestGenLiveSegmentCC608_2997fps— two 2.002 s segments at 30000/1001:00:01:21.081,00:01:22.082,00:01:23.083, so the boundary cue is frame-accurate where a fractional frame duration would otherwise drift.TestGenLiveSegmentCC608NrTimeOffset— the caption clock follows the segment's media time, notsegmentNr * segmentDuration: withstartnr_5segment 45 is the 80 s segment and reads00:01:21.000/SEG 45, and withavailabilityStartTimean hour past the epoch the same media and numbers read01:01:21.000/SEG 40. Deriving the clock frommeta.newNr * meta.newDurinstead fails only this test, at00:01:31.000.TestInjectCC608FirstCueUnbuilt— pins the mid-stream-join behaviour described above.TestCC608UnitFramesBuildDoesNotFit— the new misfit error.TestInjectCC608SelfContained/TestGenLiveSegmentCC608SelfContainedSegment— in-scmode a single unitshows both of its cues (the direct contrast with
TestInjectCC608FirstCueUnbuilt, where the same unit shows onlyits second), each flip landing inside the cue it names. The second test goes through
genLiveSegment, so theCC608Config.SelfContained->applyCC608plumbing is covered.TestCC608UnitFramesSelfContainedDoesNotFit— the self-contained misfit error.TestCreateCC608Config/TestProcessURLCfg/TestCreateURLCC608— the[-sc]grammar, including theSardinian-tag cases, the URL -> config wiring, and the wizard emitting
timecc608_CC1-eng-sc.Related
The same scheduling change is wanted in Eyevinn/moqlivemock (Eyevinn/moqlivemock#118), which shares this
scheduler, and the logic could later move into go-608 itself (Eyevinn/go-608#55) so both consumers share one
implementation. It was surfaced by player-side caption work where a low-latency player cannot recover a caption
transmitted after the moment it should be displayed.