Some checks failed
WAI conformance / does this change touch it (pull_request) Successful in 5s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 5s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 6s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 8s
WAI energy receipts / does this change touch it (pull_request) Successful in 5s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 5s
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Successful in 6s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 5s
wai-web wasm32 / does this change touch it (pull_request) Successful in 5s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 6s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 27s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m0s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 12s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 25s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m10s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 28s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 2m17s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 1m34s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 5m38s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 9m11s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 3m15s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Failing after 18m23s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 18m40s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m48s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 1m46s
wai-pack / wai-pack · linux (pull_request) Successful in 1m41s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 4m14s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 3m18s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 3m39s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 14m26s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 2m43s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 8m48s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 3m57s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 42m36s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 17m14s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 26s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 1m6s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 8m1s
wai-quantum studio and desktop / desktop app · macos (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
Co-Authored-By: Claude Opus 5.5 <[email protected]>
948 lines
57 KiB
Markdown
948 lines
57 KiB
Markdown
# WAI Extension: Timed Text and Live Captions
|
||
|
||
> Status: Draft. Registers the record `wai.text.timed`: timed text, and live captions as
|
||
> events. A **cue** is a span of media time and its text, in a small, closed set of
|
||
> styling (italic runs, line breaks, and one of six anchors) that WebVTT and the IMSC1 Text
|
||
> Profile both express. A cue therefore maps onto each of them exactly, and onto the two
|
||
> MoQ caption timelines, CTA-608 and HLS. Where a target cannot hold a cue, the mapping
|
||
> refuses it and names the reason; it never drops meaning silently. A **live caption** is
|
||
> a `caption` event of [live-events](live-events.md) (§4.10 there), unchanged. A machine
|
||
> caption is revised in place until it is final: each revision is a new `caption` event,
|
||
> followed by a `meta` event whose payload is this extension's **annotation**, `WTX1`. The
|
||
> annotation names the revision it supersedes, says whether the text is final and how much
|
||
> of it is stable, and carries the anchor and the italic runs. The event's `origin` (human,
|
||
> machine or machine-reviewed), with the digest of the model that wrote it, is the
|
||
> **disclosure label**, and every mapping carries it or hands it to a sidecar. No new
|
||
> transport: field semantics and rules over them only. The reference implementation is
|
||
> the `timed_text` module of `wai-rs` (feature `timed_text`); the corpus is
|
||
> [`live-conformance/captions/`](../live-conformance/captions/), and its second
|
||
> implementation is `live-conformance/timed_text_verify.py`.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||
>
|
||
> Texts read for this revision, each pinned to the version named:
|
||
> WebVTT, W3C Candidate Recommendation Draft of 2026-05-20 (`CRD-webvtt1-20260520`);
|
||
> IMSC Text Profile 1.3, W3C Recommendation of 2026-05-21 (`REC-ttml-imsc1.3-20260521`);
|
||
> TTML Profiles for Internet Media Subtitles and Captions 1.0.1, W3C Recommendation of
|
||
> 2018-04-24 (`REC-ttml-imsc1.0.1-20180424`), for its profile signalling;
|
||
> IMSC Hypothetical Render Model, W3C Recommendation of 2024-04-25 (`REC-imsc-hrm-20240425`);
|
||
> `draft-wilaw-moq-webvtt-msf-00`, `draft-law-moq-imsc1-msf-00` and `draft-ietf-moq-msf-01`;
|
||
> `draft-pantos-hls-rfc8216bis-22`; the C2PA Technical Specification, version 2.4; the IPTC
|
||
> Digital Source Type vocabulary as released on 2024-10-23; EN 301 549 V4.1.1 (2026-09);
|
||
> and 47 CFR 79.101 as in force on 2021-06-01 and 47 CFR 79.102, which reproduce the 608
|
||
> and 708 decoder rules. A section number of one of those texts in this extension is that
|
||
> version's. CTA-608-E and CTA-708-E themselves could not be read for this revision: their
|
||
> publisher distributes them through an account-gated store. §8.1 and §9 say what that
|
||
> leaves unchecked.
|
||
|
||
## 1. Scope
|
||
|
||
In scope: the cue model and the text subset WebVTT and the IMSC1 Text Profile both hold
|
||
exactly (§3); live captions as `caption` events, the annotation, how revisions are ordered
|
||
and checked, what a sink shows, the cues a stream resolves to, and what a subscriber joining
|
||
mid-stream receives (§4); WebVTT documents and segments, and a reader of what this
|
||
extension writes (§5); IMSC1 Text Profile documents and a check of the hypothetical render
|
||
model (§6); the MoQ caption timelines and their catalog entries (§7); CTA-608 on field 1,
|
||
channel CC1, pop-on and roll-up (§8); a CTA-708 mapping, specified only (§9); the
|
||
disclosure label in every target (§10); and what a WAI player owes its viewers (§11).
|
||
|
||
Out of scope: a transport; speech recognition and translation, which produce the text; the
|
||
fragmented MP4 packaging of an IMSC1 rendition (HLS `draft-pantos-hls-rfc8216bis-22` §3.1.5
|
||
and ISO/IEC 14496-30 carry the document §6 writes); the paint-on style of 608, colours,
|
||
underline and flash; audio description, which [access-services](access-services.md) §5
|
||
defines, with sign language and figures of a caption track's latency, stability and accuracy
|
||
computed from its revisions (§4 there); and a sink's own rendering of text, beyond §4.4 and
|
||
§11.
|
||
|
||
Relationship to the core spec: `wai.text.timed` is a **record** ([SPEC](../SPEC.md) §5).
|
||
Its unit is the signed caption revision, and the documents this extension writes are
|
||
mappings of it, as [live-events](live-events.md) §8 maps an event onto a playlist tag. It
|
||
names no payload format and is not dispatchable. A timed-text component that the
|
||
[graphics-vector](graphics-vector.md) §5.7 package rule asks for is a WebVTT or IMSC1
|
||
document written by §5 or §6.
|
||
|
||
Relationship to [live-events](live-events.md): this extension changes nothing in it. Its
|
||
`caption` payload has no field for a revision, so the annotation rides in the `meta` kind,
|
||
which live-events leaves to applications (§4.2 there), and is bound to its caption by the
|
||
event chain (§4.3 here). A later revision of live-events MAY fold the annotation's fields
|
||
into `caption`; this extension would then read both forms.
|
||
|
||
## 2. Terms
|
||
|
||
- **Cue**: one entry of the cue model (§3.1).
|
||
- **Revision**: one `caption` event, with what its annotation says (§4.3).
|
||
- **Chain of a cue**: its revisions, the first superseding none and each later one
|
||
superseding the one before.
|
||
- **Partial**: a revision that is not final. **Final**: a revision that is.
|
||
- **Stable prefix**: the leading code points of a partial's text that no later revision of
|
||
its cue changes.
|
||
- **Document order**: cues by `start`, and in the order given among cues of equal `start`.
|
||
- **Media time**: milliseconds on the timeline of the media the cues accompany. The media
|
||
time of the room's `t0` is `pt0`, as [live-events](live-events.md) §8.4 has it.
|
||
- **Slot**: one position on the 608 field-1 pair clock (§8.2).
|
||
|
||
## 3. The cue model
|
||
|
||
### 3.1 Fields
|
||
|
||
| field | type | meaning |
|
||
|---|---|---|
|
||
| `start`, `end` | u64 | media time in milliseconds; `start` < `end` ≤ 359 999 999 (99:59:59.999) |
|
||
| `lang` | string | a BCP 47 language tag, read as [live-events](live-events.md) §4.10 reads one |
|
||
| `origin` | u8 | 1 human, 2 machine, 3 machine-reviewed: the `caption` event's `origin` |
|
||
| `model` | 32 bytes | present exactly when `origin` is 2 or 3: the `caption` event's model digest |
|
||
| `anchor` | one of six | §3.3 |
|
||
| `text` | UTF-8 | the text; its lines are separated by U+000A |
|
||
| `italic` | list of ranges | each range a first code point and a count of code points, over `text` |
|
||
|
||
`origin` 3, machine-reviewed, is text a model produced and a person reviewed, or reviewed
|
||
and corrected, before it was published. This is the reading C2PA 2.4 §18.28.3 gives the
|
||
human oversight level `human_validated` (§10.3).
|
||
|
||
### 3.2 Text: the common subset
|
||
|
||
A cue's text and italic ranges are in the subset when these rules hold. Each rule exists
|
||
because one of the targets would otherwise change what the viewer reads.
|
||
|
||
1. The text has at least one code point. Every line has at least one code point: no
|
||
U+000A at the start or the end, and no two in a row. (An empty line ends a WebVTT cue,
|
||
WebVTT §4.1.)
|
||
2. Every code point other than U+000A is in U+0020–U+007E, U+00A0–U+D7FF, U+E000–U+FFFD or
|
||
U+10000–U+10FFFF, and is not U+2028, U+2029 or U+FEFF. (These are the characters
|
||
XML 1.0 holds that are neither controls nor line or paragraph separators; a tab is
|
||
refused, since IMSC1's white-space handling turns it into a space.)
|
||
3. No line begins or ends with U+0020, and no line holds two U+0020 in a row. (The default
|
||
white-space handling of TTML collapses runs of spaces and removes spaces at a line's
|
||
edges, so an IMSC1 rendering would differ from the WebVTT one.)
|
||
4. The italic ranges are canonical: each has at least one code point and lies within the
|
||
text; they ascend, and each ends before the next begins; no range holds a U+000A; no
|
||
range begins or ends with U+0020; and between two ranges there is at least one code
|
||
point other than U+0020. (So one rendering has one spelling: two italic words with a
|
||
space between them are one range.)
|
||
|
||
### 3.3 Anchors
|
||
|
||
The caption area is the inner 80% of the picture, 10% to 90% across and down, which is the
|
||
safe caption area of 47 CFR 79.101(n)(12). An anchor puts a cue at the bottom or the top of
|
||
it, with its lines centred, at its left edge or at its right edge.
|
||
|
||
| code | anchor | WebVTT cue settings (§5.1) | IMSC1 region and `tts:textAlign` (§6.1) | 608 rows of a cue of *n* lines; column of a line of *L* cells (§8.5) |
|
||
|---|---|---|---|---|
|
||
| 0 | `bottom-center` | `line:90%,end position:50%,center size:80% align:center` | `bottom`, `center` | 16 − *n* to 15; ⌊(32 − *L*) / 2⌋ |
|
||
| 1 | `bottom-left` | `line:90%,end position:50%,center size:80% align:left` | `bottom`, `left` | 16 − *n* to 15; 0 |
|
||
| 2 | `bottom-right` | `line:90%,end position:50%,center size:80% align:right` | `bottom`, `right` | 16 − *n* to 15; 32 − *L* |
|
||
| 3 | `top-center` | `line:10%,start position:50%,center size:80% align:center` | `top`, `center` | 1 to *n*; ⌊(32 − *L*) / 2⌋ |
|
||
| 4 | `top-left` | `line:10%,start position:50%,center size:80% align:left` | `top`, `left` | 1 to *n*; 0 |
|
||
| 5 | `top-right` | `line:10%,start position:50%,center size:80% align:right` | `top`, `right` | 1 to *n*; 32 − *L* |
|
||
|
||
In WebVTT the cue box spans 10% to 90% of the width, its end edge at 90% of the height for
|
||
the bottom and its start edge at 10% for the top; in IMSC1 the regions are the same bands
|
||
of the root container (§6.1). The two place a cue in the same band with the same alignment.
|
||
They are not the same pixels, and this extension does not claim that they are.
|
||
|
||
### 3.4 Checking cues
|
||
|
||
A cue is checked in this order, and the first failure names the refusal:
|
||
|
||
1. `start` < `end` ≤ 359 999 999: otherwise `time_out_of_range`.
|
||
2. `lang` reads, and `origin` is 1, 2 or 3: otherwise `cue_malformed`.
|
||
3. A `model` when `origin` is 1: `cue_malformed`. No `model` when `origin` is 2 or 3:
|
||
`model_missing`.
|
||
4. §3.2 rules 1 to 3: otherwise `text_not_in_subset`.
|
||
5. §3.2 rule 4: otherwise `cue_malformed`.
|
||
|
||
A **document** is written for one language, `lang`, a parameter of the mapping. A mapping
|
||
whose `lang` does not read is `parameter_malformed`. It checks each cue in the order given,
|
||
then that every cue's `lang` is the document's: otherwise `language_mismatch`. A document is
|
||
one language because an HLS rendition and an MSF track each state one (`LANGUAGE`,
|
||
`draft-pantos-hls-rfc8216bis-22` §4.4.6.1; `lang`, `draft-ietf-moq-msf-01` §5.2.32).
|
||
|
||
### 3.5 What each target drops
|
||
|
||
| target | what it does not carry, and where it goes instead |
|
||
|---|---|
|
||
| WebVTT document or segment | a language (WebVTT §4.1 has none for a file): the rendition's `LANGUAGE` and the sidecar (§10.5) carry it. Partials: only final cues are written (§4.5). |
|
||
| IMSC1 document | partials, as for WebVTT. |
|
||
| MoQ WebVTT timeline | as WebVTT; the language is the catalog entry's `lang` (§7.4). |
|
||
| MoQ IMSC1 timeline, cue mode | as IMSC1. The label rides in members the draft does not define (§7.2), which a consumer may ignore; the catalog field and the sidecar carry it too. |
|
||
| CTA-608 | the label (608 has no field for it): the sidecar and the HLS characteristic carry it (§10.6). The language: the `LANGUAGE` of the `CLOSED-CAPTIONS` rendition. Times: rounded up to slots (§8.2). Position: a whole column and row. Italics: only where they begin at a row's start or after a space and end at a row's end or before one; otherwise refused (§8.5). Characters outside its sets: refused unless §8.4 substitutes one. A cue's end in roll-up: honoured only when a gap follows the cue (§8.7). |
|
||
| HLS playlist | the label of each cue: the rendition-level characteristic says that some cue is machine-generated, the in-segment form and the sidecar say which (§10.2). |
|
||
|
||
## 4. Live captions
|
||
|
||
### 4.1 A caption event is a revision
|
||
|
||
A live caption is a `caption` event ([live-events](live-events.md) §4.10): `duration_ms`,
|
||
`origin`, `model`, `lang` and `text`. Its `t` is the room-clock instant from which this
|
||
revision is shown, and `duration_ms` is how long it is shown from `t`, or 0 when not known
|
||
(§4.4). A cue revised in place is a chain of `caption` events, each followed by its
|
||
annotation. A `caption` event with no annotation is a final revision that supersedes
|
||
nothing, at anchor `bottom-center`, with no italic range. A human captioner whose captions
|
||
need neither an anchor nor italics sends `caption` events alone.
|
||
|
||
Live-events verifies every event first (§6 there): signature, chain, rates and payload. A
|
||
`caption` payload cannot express a machine caption without a model digest: live-events
|
||
reads the 32 bytes after origin 2 or 3 as the digest, and refuses a payload too short for
|
||
them, `malformed_payload`.
|
||
|
||
### 4.2 The annotation
|
||
|
||
An **annotation** is the payload of a `meta` event whose first four bytes are `WTX1`.
|
||
Integers are little-endian.
|
||
|
||
| offset | size | field | meaning |
|
||
|---|---|---|---|
|
||
| 0 | 4 | `magic` | `WTX1` (ASCII) |
|
||
| 4 | 1 | `version` | 1 in this revision |
|
||
| 5 | 32 | `supersedes` | the digest ([live-events](live-events.md) §3.2) of the `caption` event this revision supersedes; 32 zero bytes when it supersedes none |
|
||
| 37 | 1 | `state` | 0 partial, 1 final |
|
||
| 38 | 2 | `stable_cp` | u16: the number of leading code points of this revision's text that no later revision of its cue changes |
|
||
| 40 | 1 | `anchor` | the anchor's code (§3.3), 0 to 5 |
|
||
| 41 | 1 | `n` | the number of italic ranges |
|
||
| 42 | 4 × `n` | `italic` | each range: `start` u16, then `len` u16, in code points of the caption's text |
|
||
|
||
An annotation is 42 + 4 × `n` bytes. It is refused `annotation_malformed` when: it is
|
||
shorter than 42 bytes; its version is not 1; `state` is not 0 or 1; `anchor` is above 5; its
|
||
length is not 42 + 4 × `n`; `stable_cp` exceeds the code points of its caption's text; its
|
||
`state` is final and `stable_cp` is not that count; or its ranges are not canonical over its
|
||
caption's text (§3.2 rule 4).
|
||
|
||
### 4.3 Revisions, their order and their checks
|
||
|
||
A verifier of captions is offered the records of one room in stream order. It offers each
|
||
to a live-events verifier ([live-events](live-events.md) §6), which refuses it with that
|
||
extension's reason, ignores it as a duplicate, or accepts it. For each event accepted, in
|
||
stream order, of principal *P* at `seq` *s*:
|
||
|
||
1. **The caption awaiting its annotation.** The verifier keeps, for each principal, the
|
||
last `caption` event it accepted whose annotation is not yet settled. If there is one,
|
||
at `seq` *c*, and this event is an annotation at *c* + 1, it is that caption's
|
||
annotation (step 4). Otherwise the caption has no annotation: it is final, its
|
||
`stable_cp` is its text's code points, its anchor `bottom-center`, with no italic range;
|
||
and if *s* is above *c* + 1, its annotation is **unknown**, since the verifier does not
|
||
hold the event that would carry it.
|
||
2. **A caption.** Its text MUST meet §3.2 rules 1 to 3, otherwise `text_not_in_subset`. It
|
||
becomes the caption awaiting its annotation.
|
||
3. **An annotation not at** *c* + 1 **of a caption awaiting it**: when the verifier holds
|
||
*P*'s event at *s* − 1, it is `annotation_misplaced`. When it does not, the annotation is
|
||
an orphan of a delivery gap, and is read no further.
|
||
4. **An annotation of the caption at** *s* − 1, in this order:
|
||
1. it reads (§4.2, its layout): otherwise `annotation_malformed`;
|
||
2. its `t` equals its caption's: otherwise `annotation_misplaced`;
|
||
3. its `stable_cp`, its state and its ranges hold against its caption's text (§4.2):
|
||
otherwise `annotation_malformed`;
|
||
4. when `supersedes` is not zero:
|
||
- it names a `caption` event of *P* the verifier holds: otherwise, when the verifier
|
||
holds every `seq` of *P* from 0 to *s*, `supersede_unknown`; when it does not, the
|
||
reference is **unresolved**, which is not a refusal;
|
||
- the revision it names is a partial: otherwise `supersede_final`;
|
||
- no other revision supersedes it: otherwise `supersede_forked`;
|
||
- it has the same `lang` and the same anchor as this one: otherwise
|
||
`supersede_mismatch`;
|
||
- this revision's text begins with the first `stable_cp` code points of its text:
|
||
otherwise `stability_broken`.
|
||
5. Any other event changes nothing here.
|
||
|
||
At the end of the stream, a caption still awaiting its annotation has none. The outcome is
|
||
the one [live-events](live-events.md) §6.4 gives, with a refusal of this section in its
|
||
place: **refuse**, with the reason and the index of the record; **gap**; or **accept**.
|
||
|
||
Revisions are ordered by their chain: a revision supersedes exactly one partial, which no
|
||
other revision supersedes, of the same principal, so a cue's revisions form one line from
|
||
its first to its last. Since live-events holds a principal's `t` non-decreasing, a later
|
||
revision is shown no earlier than the one it supersedes. A principal MAY interleave the
|
||
revisions of several cues.
|
||
|
||
### 4.4 What a sink shows
|
||
|
||
At room-clock instant *x*, a sink shows each revision *r* it holds for which all of these
|
||
hold:
|
||
|
||
- `t`(*r*) ≤ *x*;
|
||
- no revision that supersedes *r* has `t` ≤ *x*;
|
||
- when *r*'s `duration_ms` is above 0, *x* < `t`(*r*) + `duration_ms` × 1000; when it is 0,
|
||
no later `caption` event of *r*'s principal has `t` ≤ *x*.
|
||
|
||
A sink:
|
||
|
||
- MUST replace a revision by the one that supersedes it in the same place, the anchor and
|
||
language being the same (§4.3 step 4.4);
|
||
- SHOULD NOT move or re-flow the stable prefix of a revision when its successor arrives,
|
||
and MAY set the rest of a partial's text apart, for example in a lighter weight;
|
||
- MUST show a revision whose annotation is unknown as a caption with no annotation is
|
||
shown;
|
||
- MUST show the label of every machine and machine-reviewed revision (§11).
|
||
|
||
### 4.5 The cues a stream resolves to
|
||
|
||
A stream that a verifier accepts with no gap **resolves** to cues, one per chain. For each
|
||
chain, from its first revision *r*₀ to its last revision *r*ₖ:
|
||
|
||
- *r*ₖ MUST be final: otherwise `not_final`, at the index of *r*ₖ's `caption` event;
|
||
- *r*ₖ's `duration_ms` MUST be above 0: otherwise `duration_unknown`, at that index;
|
||
- the cue: `start` = `pt0` + ⌊`t`(*r*₀) / 1000⌋; `end` = `pt0` + ⌊`t`(*r*ₖ) / 1000⌋ +
|
||
`duration_ms`(*r*ₖ), which MUST NOT exceed 359 999 999 (otherwise `time_out_of_range`);
|
||
`lang`, `origin`, `model` and `text` are *r*ₖ's caption's; `anchor` and `italic` are
|
||
*r*ₖ's;
|
||
- the cue was **published** at `pt0` + ⌊`t`(*r*ₖ) / 1000⌋ (§7.3).
|
||
|
||
The cues are in document order, cues of equal `start` in the order of their first
|
||
revisions in the stream. The first chain that fails, in that order, names the refusal. A cue
|
||
starts when its first revision was shown, so its final text is timed from when the viewer
|
||
first saw the cue. The partials are not in any document, timeline or 608 stream: none of
|
||
them can replace a cue already published (§3.5).
|
||
|
||
### 4.6 Joining mid-stream
|
||
|
||
A publisher of captions MUST add to each principal's join set ([live-events](live-events.md)
|
||
§8.2), at the start of group *g*:
|
||
|
||
- the latest revision of each cue that is shown at the instant (§4.4) and has no stated
|
||
duration: a partial whose `duration_ms` is 0, which no revision before the instant
|
||
supersedes, and which is its principal's last `caption` event before the instant;
|
||
- right after each `caption` event of the join set, its annotation, when it has one.
|
||
|
||
Live-events already requires every caption whose interval holds the instant. With these, a
|
||
subscriber that joins at group *g* verifies the join set and what follows, finds a gap
|
||
before it (§6.2 there) and refuses nothing (an annotation naming a revision it lacks is
|
||
unresolved, §4.3), and shows at every instant from the group's start what a subscriber
|
||
holding the whole stream shows.
|
||
|
||
## 5. WebVTT
|
||
|
||
### 5.1 Documents
|
||
|
||
A WebVTT document of cues in `lang` is checked (§3.4), then written as UTF-8 with U+000A
|
||
line ends, no byte order mark, as:
|
||
|
||
- `WEBVTT`, U+000A, U+000A;
|
||
- then each cue in document order, the cues separated by one U+000A, each cue:
|
||
- its timing line: `start`, ` --> `, `end`, a space, and the anchor's settings (§3.3),
|
||
each time written `HH:MM:SS.mmm` (hours always two digits, the milliseconds three);
|
||
- its payload: the lines of its text, separated by U+000A, then U+000A. In each line,
|
||
each run of italic code points (§3.2) is written between `<i>` and `</i>`, and in all
|
||
text `&`, `<` and `>` are written `&`, `<` and `>`. A machine or
|
||
machine-reviewed cue's payload begins with the label's start tag (§10.4) and ends, after
|
||
its last line, with `</c>`.
|
||
|
||
No cue identifier, region, style block or comment is written. The `>` escape also keeps
|
||
the substring `-->`, which WebVTT §4.1 forbids in a payload, out of every payload.
|
||
|
||
### 5.2 Segments
|
||
|
||
The segments of cues in `lang`, at bounds *b*₀ < *b*₁ < … < *b*ₙ in media milliseconds (at
|
||
least two bounds, the last at most 359 999 999), with `mpegts0` the 90 kHz MPEG-TS time of
|
||
media time 0 (below 2³³), are refused `parameter_malformed` when the bounds or `mpegts0` do
|
||
not hold. Segment *k*, for media time *b*ₖ to *b*ₖ₊₁, is:
|
||
|
||
- `WEBVTT`, U+000A;
|
||
- `X-TIMESTAMP-MAP=LOCAL:`, *b*ₖ as `HH:MM:SS.mmm`, `,MPEGTS:`, (`mpegts0` + 90 × *b*ₖ)
|
||
mod 2³³ in decimal, U+000A, U+000A;
|
||
- every cue whose `start` < *b*ₖ₊₁ and whose `end` > *b*ₖ, in document order and with its
|
||
whole interval, written and separated as in §5.1.
|
||
|
||
This follows `draft-pantos-hls-rfc8216bis-22` §3.1.4: a segment holds every cue shown during
|
||
its period, with each cue's total display time, and its `X-TIMESTAMP-MAP` maps the segment's
|
||
first cue time to the 90 kHz media time, re-anchored in every segment so that a wrap of the
|
||
33-bit time stays exact. A live publisher writes a segment from the cues final when it cuts
|
||
it; a cue that becomes final later is in the later segments it overlaps, and a publisher
|
||
SHOULD delay cutting a segment by the time its captions take to become final.
|
||
|
||
### 5.3 Reading the subset
|
||
|
||
A reader of WebVTT reads only what §5.1 and §5.2 write, given the document's `lang`. It
|
||
refuses as `vtt_malformed` anything else: another header; an `X-TIMESTAMP-MAP` not as §5.2
|
||
writes it; a timing line whose times are not `HH:MM:SS.mmm` (minutes and seconds below 60)
|
||
or whose settings are not one anchor's; an empty payload line; a blank line other than one
|
||
between cues; a character reference other than `&`, `<` and `>`; a tag other
|
||
than the label (§10.4), `<i>` and `</i>`; an `<i>` span that is empty, nested or holds a
|
||
line end; two runs of one style next to each other; and starts that go back. A label with no
|
||
model class is `model_missing`. Each cue it reads is then checked (§3.4). A WebVTT document
|
||
or segment this extension writes reads back to its cues in document order.
|
||
|
||
## 6. IMSC1 Text Profile
|
||
|
||
### 6.1 Documents
|
||
|
||
An IMSC1 document of cues in `lang` is checked (§3.4) and checked against the hypothetical
|
||
render model (§6.2), then written as UTF-8 with U+000A line ends:
|
||
|
||
```
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:ttp="http://www.w3.org/ns/ttml#parameter" xmlns:tts="http://www.w3.org/ns/ttml#styling" xmlns:ebuttm="urn:ebu:tt:metadata" xmlns:wai="tag:transaction.science,2026:wai/timed-text" xml:lang="LANG" ttp:profile="http://www.w3.org/ns/ttml/profile/imsc1/text">
|
||
<head>
|
||
<metadata>
|
||
<ebuttm:documentMetadata>
|
||
<ebuttm:conformsToStandard>http://www.w3.org/ns/ttml/profile/imsc1/text</ebuttm:conformsToStandard>
|
||
<ebuttm:conformsToStandard>http://www.w3.org/ns/ttml/profile/imsc1.3/text</ebuttm:conformsToStandard>
|
||
</ebuttm:documentMetadata>
|
||
</metadata>
|
||
<layout>
|
||
<region xml:id="top" tts:origin="10% 10%" tts:extent="80% 35%" tts:displayAlign="before"/>
|
||
<region xml:id="bottom" tts:origin="10% 55%" tts:extent="80% 35%" tts:displayAlign="after"/>
|
||
</layout>
|
||
</head>
|
||
<body>
|
||
<div>
|
||
P
|
||
</div>
|
||
</body>
|
||
</tt>
|
||
```
|
||
|
||
with `LANG` the document's `lang`, and `P` one line per cue in document order (no line when
|
||
there is no cue):
|
||
|
||
- `<p begin="START" end="END" region="R" tts:textAlign="A"`, the times as `HH:MM:SS.mmm`,
|
||
`R` and `A` the anchor's (§3.3);
|
||
- for a machine or machine-reviewed cue, ` wai:origin="O" wai:model="M"` (§10.4);
|
||
- `>`, the lines of the text separated by `<br/>`, each italic run written between
|
||
`<span tts:fontStyle="italic">` and `</span>`, `&`, `<` and `>` written `&`, `<`
|
||
and `>`, then `</p>`.
|
||
|
||
The document uses features the Text Profile of every IMSC edition from 1.0.1 to 1.3
|
||
permits: media timing in clock time, two regions given by `tts:origin` and `tts:extent` in
|
||
percent (IMSC 1.3 §8.1.19), `tts:displayAlign`, `tts:textAlign`, `tts:fontStyle`, `span`
|
||
and `br`. Its regions lie within the root container and do not overlap (IMSC 1.3
|
||
§8.13.2), and at most two are presented at once (§8.13.3).
|
||
|
||
It signals its profile in two ways. The `ttp:profile` attribute on `tt`, equal to the IMSC
|
||
1.0 Text Profile designator, is what IMSC 1.0.1 §6.9 says SHOULD be present in a document
|
||
that is not also an EBU-TT-D or SDP-US document; that edition is the IMSC1 that
|
||
`draft-pantos-hls-rfc8216bis-22` §3.1.5 names, and IMSC 1.3's feature table permits
|
||
`#profile`. The two `ebuttm:conformsToStandard` elements, the IMSC 1.0 and the IMSC 1.3
|
||
Text Profile designators, are the form IMSC 1.3 §8.10.2 specifies, which its profile
|
||
resolution reads to set the override content profile (§6.3.2). Neither names an Image
|
||
Profile, since IMSC 1.0.1 §6.9 forbids signalling both the Image and the Text Profile in one
|
||
document; no `ttp:profile` element is written. `ttp:contentProfiles` is not written: IMSC
|
||
1.3 §8.10.1 does not recommend it in a document meant for profiles based on TTML1, such as
|
||
IMSC 1.0.1. Attributes in this
|
||
extension's namespace are foreign attributes, which IMSC 1.3 §8.6 permits and a processor
|
||
prunes. Default cell resolution (32 by 15) and font size (one cell) apply.
|
||
|
||
### 6.2 The hypothetical render model
|
||
|
||
IMSC 1.3 §8.2 says a document SHOULD conform to the IMSC-HRM. A writer of this extension
|
||
MUST check that it does, as follows, and refuses `hrm_exceeded` when the check fails. The
|
||
check is sound: a document that passes conforms to the model. It may refuse a document that
|
||
conforms, since it takes the least favourable factor of each of the model's tables.
|
||
|
||
- The intermediate synchronic documents are the spans between consecutive distinct times
|
||
among every cue's `start` and `end`, each holding the cues with `start` ≤ its beginning <
|
||
`end`. One with no cue is empty.
|
||
- No region has a background, so painting a non-empty document clears the root container
|
||
and paints no region: 1 / `BDraw` = 1/12 s (IMSC-HRM §7, §8).
|
||
- Every glyph's normalised rendered area is (1/15)², the default font size of one cell of
|
||
fifteen (IMSC-HRM §9). A glyph is a code point with its italic flag; `br` is not one.
|
||
Rendering a glyph takes (1/15)² / 0.6 = 1/135 s and copying one (1/15)² / 3 = 1/675 s:
|
||
the slowest `Ren` and `GCpy` the model's tables give, whatever the script.
|
||
- For each non-empty document in order, the code points of the `top` region's cues, then of
|
||
the `bottom` region's, each region's cues in document order: a glyph in the glyph cache
|
||
is copied and flagged; any other is rendered, added to the cache and flagged. More than
|
||
225 flagged glyphs at any point (a normalised size above 1) fails the check.
|
||
- The time it has is the time since the previous non-empty document's presentation, when
|
||
that is less than one second, and one second otherwise (the initial painting delay). The
|
||
check fails when 1/12 s plus the time of every glyph exceeds it. In units of 1/2700 s:
|
||
225 + 20 per glyph rendered + 4 per glyph copied, against 2.7 per millisecond.
|
||
- At the document's presentation, glyphs not flagged leave the cache, and the rest lose
|
||
their flag. An empty document empties the cache, which can only lengthen a later
|
||
painting.
|
||
|
||
### 6.3 The structure a verifier checks
|
||
|
||
An IMSC1 document written here is well-formed XML 1.0 in UTF-8, and parsed it has: a `tt`
|
||
root in the TTML namespace with `xml:lang` and `ttp:profile` equal to the IMSC 1.0 Text
|
||
Profile designator, no `ttp:profile` element, and no Image Profile designator anywhere; a
|
||
`head` whose metadata names both designators of §6.1; a `layout` with exactly the two
|
||
regions of §6.1, each with an `xml:id`, `tts:origin`
|
||
and `tts:extent` in percent, no `tts:position`, within the root container and not
|
||
overlapping; a `body` with one `div` of `p` elements, each with `begin` < `end` in clock
|
||
time and a `region` that names one of them; in a `p`, only text, `span` with
|
||
`tts:fontStyle="italic"`, and `br`; no attribute in a TT namespace but those §6.1 writes;
|
||
and the `wai:` attributes of §10.4 exactly on the `p` of a machine or machine-reviewed cue.
|
||
|
||
## 7. The MoQ caption timelines
|
||
|
||
The two caption timelines are MSF event timelines (`draft-ietf-moq-msf-01` §8): each MOQT
|
||
object is a JSON array of records, and each record an index and a `data` object. This
|
||
extension writes JSON with no whitespace, its members in the order given, strings with `"`
|
||
and `\` escaped, U+000A as `\n`, any other code point below U+0020 as `\u00xx`, and every
|
||
other code point as itself. `draft-wilaw-moq-webvtt-msf-00` cites MSF section numbers that
|
||
are not those of `draft-ietf-moq-msf-01` (its event timeline is MSF §8, not §11); this
|
||
extension cites MSF -01's own.
|
||
|
||
### 7.1 The WebVTT timeline
|
||
|
||
A cue's record (`draft-wilaw-moq-webvtt-msf-00` §3, §3.1):
|
||
|
||
```
|
||
{"m":START,"data":{"start":START,"end":END,"text":TEXT,"settings":{"line":LINE,"position":"50%,center","size":"80%","align":ALIGN}}}
|
||
```
|
||
|
||
`START` and `END` are the cue's media times, `TEXT` its WebVTT payload (§5.1) without the
|
||
final U+000A, `LINE` `"90%,end"` for a bottom anchor and `"10%,start"` for a top one, and
|
||
`ALIGN` the anchor's alignment. No initialization record is written, since no region or
|
||
style is used (§4 there).
|
||
|
||
### 7.2 The IMSC1 timeline, cue mode
|
||
|
||
The initialization record (`draft-law-moq-imsc1-msf-00` §4.1):
|
||
|
||
```
|
||
{"m":0,"data":{"init":{"profile":"http://www.w3.org/ns/ttml/profile/imsc1/text","styles":{"center":{"textAlign":"center"},"left":{"textAlign":"left"},"right":{"textAlign":"right"}},"regions":{"bottom":{"origin":"10% 55%","extent":"80% 35%","displayAlign":"after"},"top":{"origin":"10% 10%","extent":"80% 35%","displayAlign":"before"}}}}}
|
||
```
|
||
|
||
A cue's record (§4.2 there):
|
||
|
||
```
|
||
{"m":START,"data":{"start":START,"end":END,"region":R,"style":A,"content":[...]}}
|
||
```
|
||
|
||
`R` is the anchor's region and `A` its alignment, as JSON strings. `content` holds, line by
|
||
line, each run as `{"text":T}` or, italic, `{"text":T,"fontStyle":"italic"}`, and
|
||
`{"br":true}` between lines. A machine or machine-reviewed cue's `data` ends with
|
||
`"wai_origin":O,"wai_model":M` (§10.4). The draft defines no such member; a consumer that
|
||
ignores members it does not know loses the label there, and §10.6 applies.
|
||
|
||
### 7.3 Groups and the repeat at a group's start
|
||
|
||
Each cue is placed by the media time it was published at: its `start` for cues given as a
|
||
list, and for a stream the time of §4.5. Group *g* of a timeline covers media time
|
||
*g* × `group_ms` to (*g* + 1) × `group_ms`, `group_ms` a parameter above 0. The mapping of
|
||
groups *g*₁ to *g*₂ (*g*₁ ≤ *g*₂; otherwise, or when a cue is published before its
|
||
`start`, `parameter_malformed`) writes, for each group:
|
||
|
||
- object 0: the IMSC1 initialization record (IMSC1 timeline only), then the record of every
|
||
cue published before the group's start whose `end` is after it;
|
||
- then one object per cue published within the group, of its one record.
|
||
|
||
Cues are taken in the order they were published, by media time, in the order given among
|
||
equal times. This is the first object of each group repeating "all accumulated cue
|
||
records up to that point" (`draft-wilaw-moq-webvtt-msf-00` §5) and the independence of
|
||
`draft-ietf-moq-msf-01` §8.3, read as [live-events](live-events.md) §8.1 reads it: the
|
||
records accessible at a group's start are those of cues still to be shown, and earlier ones
|
||
stay reachable by FETCH of earlier groups. A subscriber joining at any group holds the record
|
||
of every cue that has not ended.
|
||
|
||
### 7.4 Catalog entries
|
||
|
||
A caption timeline's MSF catalog track object:
|
||
|
||
```
|
||
{"name":NAME,"packaging":"eventtimeline","eventType":"urn:msf:captions:webvtt","mimeType":"application/json","depends":[...],"role":"caption","lang":LANG,"wai_machine_generated":B}
|
||
{"name":NAME,"packaging":"eventtimeline","eventType":"urn:msf:captions:imsc1","mimeType":"application/json","depends":[...],"role":"caption","lang":LANG,"imsc1Mode":"cue","wai_machine_generated":B}
|
||
```
|
||
|
||
`B` is `true` when any cue of the track is machine or machine-reviewed, and `false`
|
||
otherwise. `wai_machine_generated` is a custom field, which `draft-ietf-moq-msf-01` §5 lets
|
||
a producer add and requires a parser to ignore when it does not understand it.
|
||
|
||
## 8. CTA-608
|
||
|
||
### 8.1 Sources
|
||
|
||
The basic character set, the special characters, the mid-row codes, the miscellaneous
|
||
control codes, the preamble address codes, parity and the redundant sending of control
|
||
codes are as 47 CFR 79.101 reproduces CTA-608's decoder rules (paragraphs (e) to (j)), read
|
||
as in force on 2021-06-01. The extended characters (first byte 0x12 or 0x13) are not in that
|
||
regulation. This revision's table of them (§8.4) is the one the 608 standard's extended
|
||
character set is commonly given as, and it has not been checked against CTA-608-E: a reviewer
|
||
with the standard SHOULD check it. The one code whose character this revision could not
|
||
settle, 0x12 0x29, is left unassigned: the encoder never sends it, and the decoder refuses
|
||
it.
|
||
|
||
### 8.2 The slot clock and the frame rate
|
||
|
||
The encoder writes field 1, data channel 1 (CC1) only. It writes on a **slot clock**: one
|
||
byte pair per slot, at `num` / `den` slots a second, a parameter. Slot *k* is at media
|
||
time *k* × 1000 × `den` / `num` ms, and the slot of media time *t* ms is the first at or
|
||
after it, ⌈*t* × `num` / (1000 × `den`)⌉. The rate MUST be a positive `num` and `den` of at
|
||
most 1 000 000 each, otherwise `parameter_malformed`.
|
||
|
||
The stream depends on the rate in its timing and its capacity: every slot carries one pair,
|
||
so a faster clock carries more bytes a second, and each cue's display starts at a slot of
|
||
that clock. On line 21, and in 608 data carried with video of 30000/1001 frames a second,
|
||
the rate is 30000/1001. A carrier that gives field-1 pairs at another rate states it, and
|
||
the same cues then give the same pairs in other slots, or are refused `too_dense` where the
|
||
pairs no longer fit. How a carrier places slots in video frames (`cc_data` of CTA-708, for
|
||
example) is the carrier's and is not specified here.
|
||
|
||
### 8.3 Bytes
|
||
|
||
- Every byte has odd parity: bit 7 is set exactly when the other seven bits have an even
|
||
number of ones.
|
||
- A **code** is either one byte of the basic set or a two-byte code whose first byte is
|
||
0x10 to 0x1F. Standard bytes are packed two to a pair, in order. A lone standard byte
|
||
before a two-byte code, or at the end of a cue's codes, is paired with 0x00.
|
||
- Every two-byte code, control code or character alike, is sent twice, in consecutive
|
||
slots: a decoder ignores a pair that repeats, in the next slot, a two-byte pair it acted
|
||
on (47 CFR 79.101(i)(4)). So two equal two-byte characters in a row take four pairs.
|
||
- A slot that carries nothing carries 0x80 0x80. The **hexadecimal form** of a stream lists
|
||
only the slots that carry a pair, one per line in ascending order: the slot in decimal (no
|
||
leading zero), a space, the pair as four lowercase hexadecimal digits, then U+000A.
|
||
|
||
### 8.4 Characters
|
||
|
||
Each code point of a cue's text, after substitution, is sent as the first that applies:
|
||
|
||
- **Substitution.** U+2019 is sent as U+0027; U+2013, U+2010 and U+2011 as U+002D.
|
||
- **The basic set**, one byte: 0x20 to 0x7A as the ASCII character of that value, except
|
||
0x2A `á`, 0x5C `é`, 0x5E `í`, 0x5F `ó` and 0x60 `ú`; and 0x7B `ç`, 0x7C `÷`, 0x7D `Ñ`,
|
||
0x7E `ñ`. So ASCII `*`, `\`, `^`, `_`, `` ` ``, `{`, `|`, `}` and `~` are not basic. 0x7F
|
||
(the solid block) is not sent.
|
||
- **A special character**, 0x11 then:
|
||
|
||
| 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x3A | 0x3B | 0x3C | 0x3D | 0x3E | 0x3F |
|
||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||
| ® | ° | ½ | ¿ | ™ | ¢ | £ | ♪ | à | U+00A0 | è | â | ê | î | ô | û |
|
||
|
||
0x39 is the transparent space, written U+00A0 in a cue.
|
||
- **An extended character**, 0x12 or 0x13 then the code below, sent after a standard
|
||
**fallback** character, which the extended code backspaces over:
|
||
|
||
| second byte | 0x20 | 0x21 | 0x22 | 0x23 | 0x24 | 0x25 | 0x26 | 0x27 | 0x28 | 0x29 | 0x2A | 0x2B | 0x2C | 0x2D | 0x2E | 0x2F |
|
||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||
| 0x12 | Á | É | Ó | Ú | Ü | ü | ‘ | ¡ | * | (unassigned) | — | © | ℠ | • | “ | ” |
|
||
| 0x13 | Ã | ã | Í | Ì | ì | Ò | ò | Õ | õ | { | } | \ | ^ | _ | \| | ~ |
|
||
|
||
| second byte | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x3A | 0x3B | 0x3C | 0x3D | 0x3E | 0x3F |
|
||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||
| 0x12 | À | Â | Ç | È | Ê | Ë | ë | Î | Ï | ï | Ô | Ù | ù | Û | « | » |
|
||
| 0x13 | Ä | ä | Ö | ö | ß | ¥ | ¤ | ¦ | Å | å | Ø | ø | ┌ | ┐ | └ | ┘ |
|
||
|
||
The fallback of an accented capital is its unaccented capital (`Á` `À` `Â` `Ã` `Ä` `Å`
|
||
→ `A`; `É` `È` `Ê` `Ë` → `E`; `Í` `Ì` `Î` `Ï` → `I`; `Ó` `Ò` `Ô` `Õ` `Ö` `Ø` → `O`;
|
||
`Ú` `Ù` `Û` `Ü` → `U`; `Ç` → `C`), of an accented small letter its unaccented small
|
||
letter (`ã` `ä` `å` → `a`; `ë` → `e`; `ì` `ï` → `i`; `ò` `õ` `ö` `ø` → `o`; `ù` `ü` →
|
||
`u`; `ß` → `s`); of `“` `”` `«` `»` it is `"`, of `‘` it is `'`, of `¡` it is `!`, and of
|
||
every other extended character a space.
|
||
|
||
A code point none of these sends is `char_not_in_608`. A cue's text after substitution is
|
||
what a decoder shows.
|
||
|
||
### 8.5 Rows, columns and italics
|
||
|
||
A cue's lines are laid out, in this order of checks:
|
||
|
||
1. A pop-on cue has at most 4 lines, a roll-up cue at most as many as its window has rows:
|
||
otherwise `too_many_rows`.
|
||
2. Every code point is sent (§8.4): otherwise `char_not_in_608`.
|
||
3. Each line is at most 32 cells, one cell per code point: otherwise `row_too_long`.
|
||
4. Every italic run of a line begins at the line's start or right after a U+0020, and ends
|
||
at the line's end or right before a U+0020: otherwise `style_not_expressible`. A mid-row
|
||
code is shown as a space (47 CFR 79.101(h)(1)(i)), so italics can change only where a
|
||
space already stands.
|
||
|
||
The rows and columns are §3.3's; in roll-up every line is written on the base row (§8.7).
|
||
A row is written as:
|
||
|
||
- **Position.** When the line begins italic at column 0: the italic preamble address code
|
||
of its row (second byte 0x4E or 0x6E). When it begins italic at column *c* ≥ 1: the
|
||
cursor is placed at *c* − 1, then the italics mid-row code (0x11 0x2E), which takes that
|
||
cell as a space. Otherwise the cursor is placed at *c*. The cursor is placed at a column
|
||
*x* by the preamble address code of the row with indent 4 × ⌊*x* / 4⌋ (second byte 0x50 +
|
||
2 × ⌊*x* / 4⌋ above the row's base), then, when *x* mod 4 is not 0, tab offset *x* mod 4
|
||
(0x17 0x21 to 0x23).
|
||
- **Characters**, in order. A U+0020 just before an italic run is sent as the italics
|
||
mid-row code; a U+0020 just after one, as the white mid-row code (0x11 0x20), which ends
|
||
italics. Every other code point is sent as §8.4 says.
|
||
|
||
The first byte of a preamble address code for rows 1 to 15 is 0x11, 0x11, 0x12, 0x12, 0x15,
|
||
0x15, 0x16, 0x16, 0x17, 0x17, 0x10, 0x13, 0x13, 0x14, 0x14; the base of its second byte is
|
||
0x60 for rows 2, 4, 6, 8, 10, 13 and 15, and 0x40 for the others.
|
||
|
||
### 8.6 Pop-on
|
||
|
||
The cues are checked (§3.4, §8.5) in document order, then: two cues whose intervals overlap
|
||
(a cue starting before the one before it ends) are `cues_overlap`, since pop-on shows one
|
||
caption at a time. With *s*ᵢ and *e*ᵢ the slots of cue *i*'s `start` and `end`:
|
||
|
||
- *e*ᵢ MUST be at least *s*ᵢ + 2: otherwise `too_dense`.
|
||
- **End of Caption** (0x14 0x2F), sent twice, takes slots *s*ᵢ and *s*ᵢ + 1: the cue is
|
||
shown from the first.
|
||
- **Erase Displayed Memory** (0x14 0x2C), sent twice, takes slots *e*ᵢ and *e*ᵢ + 1 when cue
|
||
*i* is the last or *s*ᵢ₊₁ ≥ *e*ᵢ + 2. Otherwise the next caption replaces this one at
|
||
*s*ᵢ₊₁.
|
||
- **The load** of cue *i*: Resume Caption Loading (0x14 0x20), Erase Non-displayed Memory
|
||
(0x14 0x2E), then its rows top to bottom (§8.5), packed (§8.3). Its units, a pair or a
|
||
doubled code (two consecutive slots), are placed last first, each in the latest free
|
||
slots before the unit after it (before *s*ᵢ for the last), and none before slot
|
||
*s*ᵢ₋₁ + 2 (slot 0 for the first cue). Slots taken by an erase are not free. A load that
|
||
does not fit is `too_dense`.
|
||
|
||
### 8.7 Roll-up
|
||
|
||
With a window of *D* rows (2, 3 or 4), the cues are checked as for pop-on, and cues whose
|
||
anchors are not all bottom or all top are `style_not_expressible`: the window would move
|
||
with the lines in it. The base row is 15 for the bottom and *D* for the top. Each cue's
|
||
codes are: the roll-up command of *D* (0x14 0x25 to 0x27), for the first cue only; then for
|
||
each line, a Carriage Return (0x14 0x2D) when the base row holds a line (any line after the
|
||
first since the start or since an erase), then the row (§8.5). The codes, packed, take
|
||
consecutive slots from *s*ᵢ, which MUST NOT be before the slot after the previous cue's last
|
||
(its last pair, or its erase); they MUST end before *e*ᵢ when the cue is erased, and before
|
||
*s*ᵢ₊₁ when it is not. Otherwise `too_dense`. The erase is sent as in pop-on, at *e*ᵢ, when
|
||
cue *i* is the last or *s*ᵢ₊₁ ≥ *e*ᵢ + 2. Text is shown as it arrives, and a cue followed
|
||
directly by the next stays on screen above it until it rolls off.
|
||
|
||
### 8.8 Decoding
|
||
|
||
The decoder reads the hexadecimal form (§8.3) and refuses as `cc_malformed`: a line not in
|
||
that form, slots that do not ascend, a byte with even parity, a two-byte code the encoder
|
||
does not send, a standard byte outside the basic set, an extended code left unassigned
|
||
(§8.4), and a character before any mode is set. It keeps the displayed and the non-displayed
|
||
memory of 15 rows of 32 cells, the cursor, the italic attribute and the base row, and acts
|
||
on each pair as 47 CFR 79.101(f) to (i) say, ignoring a two-byte pair that repeats in the
|
||
next slot the one it acted on. It reports:
|
||
|
||
- at each End of Caption, `show` with its slot and the displayed rows;
|
||
- in roll-up, `line` with the slot of the line's preamble address code and the base row,
|
||
when a Carriage Return, an erase or the end of the stream commits it;
|
||
- at each Erase Displayed Memory, `erase` with its slot.
|
||
|
||
A row is read from its first to its last cell holding a character that is not a mid-row
|
||
code; cells of mid-row codes within it read as spaces. A code point is italic when its cell
|
||
is, and a space is in an italic range when the cells on both sides are italic characters.
|
||
What the decoder reports of an encoded stream is, for pop-on, each cue shown at *s*ᵢ with
|
||
its rows and erased at *e*ᵢ as §8.6 says; and for roll-up, each line of each cue in order,
|
||
from a slot of its cue, with the erases of §8.7.
|
||
|
||
## 9. CTA-708 (specified only)
|
||
|
||
This revision specifies the mapping of cues onto CTA-708 caption service blocks and does not
|
||
implement it. CTA-708-E could not be read for it, so this section gives the semantics of
|
||
each step and no byte values; an implementation MUST take the command codes, the window and
|
||
pen attribute layouts and the caption channel packet rules from CTA-708-E itself, and its
|
||
corpus would be the check of this section.
|
||
|
||
- **Service.** Captions in the document's language are caption service 1, the primary
|
||
service (47 CFR 79.102(c)); a second language is service 2. In HLS the rendition is a
|
||
`CLOSED-CAPTIONS` tag with `INSTREAM-ID="SERVICE1"` (`draft-pantos-hls-rfc8216bis-22`
|
||
§4.4.6.1), and in an MSF catalog an accessibility descriptor of scheme
|
||
`urn:scte:dash:cc:cea-708:2015` (`draft-ietf-moq-msf-01` §5.2.44).
|
||
- **Windows.** One window per anchor in use, its window id the anchor's code (§3.3), defined
|
||
with relative positioning: anchor point bottom-left, bottom-centre or bottom-right at 90%
|
||
of the height for a bottom anchor, top-left, top-centre or top-right at 10% for a top
|
||
one, at 10%, 50% or 90% of the width for left, centre and right; rows and columns locked;
|
||
at most 4 rows (47 CFR 79.102(f)(1)) and 32 columns, so that a 4:3 receiver shows it
|
||
(79.102(e)(3)).
|
||
- **Pop-on.** A cue's window is defined hidden and cleared, its lines written (a carriage
|
||
return between lines), and then displayed at the cue's start and hidden or deleted at its
|
||
end, the display and hide commands taking the place of 608's End of Caption and erase.
|
||
- **Roll-up.** One visible window of *D* rows at the base anchor, lines written with a
|
||
carriage return before each, as in §8.7.
|
||
- **Italics.** Set by the pen attributes before a run and cleared after it, at any
|
||
position: 708's pen attributes take no cell, so §8.5 rule 4 does not apply.
|
||
- **Characters.** The G0, G1 and G2 sets as 47 CFR 79.102(d) requires decoders to support
|
||
them, with its substitutions where a decoder lacks a G2 character; a code point in none of
|
||
them is refused, as §8.4 refuses one that 608 cannot send.
|
||
- **The label.** 708 has no field for it: §10.6 applies, as for 608.
|
||
|
||
## 10. The disclosure label
|
||
|
||
### 10.1 Its source
|
||
|
||
The label of a cue is its `origin` and, for origins 2 and 3, its model digest, as the
|
||
`caption` event carries them ([live-events](live-events.md) §4.10). A cue is
|
||
**machine-generated** when its origin is 2 or 3. Every mapping of §5 to §8 carries the
|
||
label, in band where the target can (§10.2 to §10.4) and in the sidecar where it cannot
|
||
(§10.5, §10.6). A machine caption with no model digest is refused wherever it is met:
|
||
`malformed_payload` in an event (§4.1), `model_missing` in a cue (§3.4) and in a WebVTT
|
||
label (§5.3).
|
||
|
||
### 10.2 HLS
|
||
|
||
`draft-pantos-hls-rfc8216bis-22` §4.4.6.1 defines, in `CHARACTERISTICS`: "Any Rendition MAY
|
||
include the "public.machine-generated" characteristic to indicate the Rendition was
|
||
authored or translated programmatically." This extension uses that characteristic as
|
||
the draft defines it; it is in the IETF draft itself, so no other document is needed for
|
||
it. The `EXT-X-MEDIA` tag of a caption rendition is written, attributes in this order:
|
||
|
||
- a WebVTT or IMSC1 rendition: `#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="G",NAME="N",LANGUAGE="L",CHARACTERISTICS="C",URI="U"`;
|
||
- 608 on CC1: `#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="G",NAME="N",LANGUAGE="L",INSTREAM-ID="CC1",CHARACTERISTICS="C"`.
|
||
|
||
`C` is the accessibility characteristics the producer states (for captions,
|
||
`public.accessibility.transcribes-spoken-dialog` and
|
||
`public.accessibility.describes-music-and-sound`), in its order, then
|
||
`public.machine-generated` when any cue of the rendition is machine-generated, joined by
|
||
commas; `CHARACTERISTICS` is left out when `C` is empty. A rendition with a machine-generated
|
||
cue MUST carry the characteristic and one without MUST NOT. `G`, `N`, `U` and each stated
|
||
characteristic are one or more of the ASCII letters, digits, space, `.`, `_`, `-`, `/` and
|
||
`:`; a stated characteristic holds no comma and is not `public.machine-generated` itself;
|
||
otherwise `parameter_malformed`. A `CLOSED-CAPTIONS` tag names a channel within the video
|
||
renditions rather than a rendition of its own (§4.4.6.1 there), and this extension carries
|
||
the characteristic there too.
|
||
|
||
### 10.3 C2PA and IPTC
|
||
|
||
For a document or rendition, its cues give:
|
||
|
||
- an IPTC digital source type (the vocabulary `http://cv.iptc.org/newscodes/digitalsourcetype/`):
|
||
`digitalCreation` when every cue is human (or there is none), `trainedAlgorithmicMedia`
|
||
when every cue is machine, `compositeWithTrainedAlgorithmicMedia` when every cue is
|
||
machine-reviewed, and `compositeSynthetic` when the cues have more than one origin. It is
|
||
the `digitalSourceType` of the `c2pa.created` action of the rendition's C2PA manifest
|
||
(C2PA 2.4 §18.15.4.5).
|
||
- one `c2pa.ai-disclosure` assertion (C2PA 2.4 §18.28) per model, in the order models first
|
||
appear in document order, labelled `c2pa.ai-disclosure`, `c2pa.ai-disclosure__1`, … (C2PA
|
||
2.4 §6.4), each with `modelType` `c2pa.types.model` (the generic model type of Table 12
|
||
there), `modelIdentifier` `tag:transaction.science,2026:wai/model/` and the digest in
|
||
lowercase hexadecimal, and `contentProfile.humanOversightLevel` `fully_autonomous` when any
|
||
cue of that model is machine, and `human_validated` otherwise. This pairing of digital
|
||
source type and oversight level is C2PA 2.4 §18.28.3's.
|
||
|
||
The statement is written as the JSON
|
||
`{"digitalSourceType":DST,"assertions":[{"label":L,"data":{"modelType":"c2pa.types.model","modelIdentifier":ID,"contentProfile":{"humanOversightLevel":H}}},…]}`.
|
||
Embedding it in a manifest, and signing that, is C2PA's and [provenance](provenance.md)'s.
|
||
|
||
### 10.4 In WebVTT and IMSC1
|
||
|
||
- **WebVTT.** A machine or machine-reviewed cue's payload is one cue class span (WebVTT
|
||
§4.2.2) whose classes are `wai-machine` or `wai-machine-reviewed`, then `wai-model-` and the
|
||
digest in 64 lowercase hexadecimal digits: `<c.wai-machine.wai-model-…>…</c>`. A WebVTT
|
||
parser keeps the classes on the cue's nodes, where a style sheet (`::cue(.wai-machine)`) or
|
||
a script reads them, and `draft-wilaw-moq-webvtt-msf-00` §3 keeps the tags in its `text`.
|
||
- **IMSC1.** The cue's `p` has `wai:origin` (`machine` or `machine-reviewed`) and
|
||
`wai:model` (the digest in lowercase hexadecimal), in the namespace
|
||
`tag:transaction.science,2026:wai/timed-text`.
|
||
- **IMSC1 timeline, cue mode.** `wai_origin` and `wai_model` in the cue entry's `data`
|
||
(§7.2).
|
||
|
||
A human cue carries none of these.
|
||
|
||
### 10.5 The sidecar
|
||
|
||
The **sidecar** of a document or rendition lists every cue, in document order, with its
|
||
label:
|
||
|
||
```
|
||
{"wai_timed_text":1,"lang":LANG,"cues":[{"start":S,"end":E,"origin":O},{"start":S,"end":E,"origin":O,"model":M},…]}
|
||
```
|
||
|
||
`O` is `human`, `machine` or `machine-reviewed`, and `model` is present for the last two.
|
||
In HLS the sidecar is published with the multivariant playlist by
|
||
`#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.timed-text",URI="U",LANGUAGE="L"`
|
||
(`draft-pantos-hls-rfc8216bis-22` §4.4.6.4; JSON is the default format), one per caption
|
||
rendition; in a package it is a component beside the rendition it describes.
|
||
|
||
### 10.6 A target that cannot carry it
|
||
|
||
A target that cannot carry the label in band (608, 708, and an IMSC1 cue-mode consumer that
|
||
ignores unknown members) MUST be published with the sidecar, and with the rendition-level
|
||
marks it can carry: the HLS characteristic (§10.2), the catalog field (§7.4) and the C2PA
|
||
statement (§10.3). A mapping MUST NOT publish machine-generated captions in a target that
|
||
carries no label while publishing no sidecar for them. A converter that reads a WAI caption
|
||
target and writes another MUST carry the label into the new one.
|
||
|
||
## 11. Obligations of a WAI player
|
||
|
||
These are requirements on a WAI player that shows captions. Each cites the provision it
|
||
answers by identifier; they are written as engineering requirements, and meeting them is not
|
||
a statement about any law.
|
||
|
||
1. **Captions can be shown.** A player of video with synchronised audio MUST have a mode
|
||
that shows the captions available, and MUST let the viewer choose to show closed captions
|
||
(EN 301 549 V4.1.1 clause 7.1.1; WCAG 2.2 Success Criteria 1.2.2 and 1.2.4, which ask
|
||
content to provide captions, prerecorded and live).
|
||
2. **In time.** A player MUST show a cue within 100 ms of its time (EN 301 549 V4.1.1 clause
|
||
7.1.2), and SHOULD within 20 ms.
|
||
3. **Preserved.** A player or converter that transmits, converts or records captions MUST
|
||
keep their position and style (EN 301 549 V4.1.1 clause 7.1.3): this extension's mappings
|
||
keep the anchor and italics, or refuse (§3.5, §8.5).
|
||
4. **Under the viewer's control.** A player that controls how captions look MUST let the
|
||
viewer change their characteristics: at least size, colours and background (EN 301 549
|
||
V4.1.1 clause 7.1.4). A viewer's settings override the anchors and italics of this
|
||
extension where they conflict, as [graphics-vector](graphics-vector.md) §5.7 has them
|
||
override drawn captions.
|
||
5. **Discoverable.** A player whose purpose is video MUST offer turning captions on and off as
|
||
a single operation, at the same level as the volume control (EN 301 549 V4.1.1 clause
|
||
7.3).
|
||
6. **Persistent.** A player MUST keep the viewer's choice, captions on or off, their language
|
||
and their characteristics, from one item and one session to the next, until the viewer
|
||
changes it (EN 301 549 V4.1.1 clause 7.3, NOTE 3).
|
||
7. **Labelled.** A player MUST show, with every machine or machine-reviewed caption, that it
|
||
is machine-generated, in a way the viewer can perceive without turning on another
|
||
feature, and SHOULD let the viewer see the model that produced it. A player MUST NOT
|
||
remove the label when the viewer's style settings apply.
|
||
|
||
## 12. Reasons
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `time_out_of_range` | §3.4, §4.5 |
|
||
| `cue_malformed` | §3.4, §5.3 |
|
||
| `model_missing` | §3.4, §5.3, §10.1 |
|
||
| `text_not_in_subset` | §3.4, §4.3 |
|
||
| `language_mismatch` | §3.4 |
|
||
| `annotation_malformed` | §4.2, §4.3 |
|
||
| `annotation_misplaced` | §4.3 |
|
||
| `supersede_unknown` | §4.3 |
|
||
| `supersede_final` | §4.3 |
|
||
| `supersede_forked` | §4.3 |
|
||
| `supersede_mismatch` | §4.3 |
|
||
| `stability_broken` | §4.3 |
|
||
| `not_final` | §4.5 |
|
||
| `duration_unknown` | §4.5 |
|
||
| `vtt_malformed` | §5.3 |
|
||
| `hrm_exceeded` | §6.2 |
|
||
| `parameter_malformed` | §3.4, §5.2, §7.3, §8.2, §10.2 |
|
||
| `cues_overlap` | §8.6, §8.7 |
|
||
| `too_many_rows` | §8.5 |
|
||
| `row_too_long` | §8.5 |
|
||
| `char_not_in_608` | §8.4, §8.5 |
|
||
| `style_not_expressible` | §8.5, §8.7 |
|
||
| `too_dense` | §8.6, §8.7 |
|
||
| `cc_malformed` | §8.8 |
|
||
|
||
A record [live-events](live-events.md) refuses keeps that extension's reason (§9 there).
|
||
|
||
## 13. Conformance
|
||
|
||
[`live-conformance/captions/`](../live-conformance/captions/) holds the corpus, written from
|
||
fixed seeds by `wai_timed_text_vectors gen`: `vectors.json` and every expected output, each a
|
||
file of its own (`.vtt`, `.ttml`, `.608.hex`). It has:
|
||
|
||
- **event vectors**: `WEV1` records in stream order, with the outcome §4.3 gives them, the
|
||
revisions, what a sink shows at stated instants (§4.4), the cues the stream resolves to or
|
||
the refusal (§4.5), and every target's output. They are a human-captioned lesson, a
|
||
machine-captioned lesson whose partials are revised to final, and a mid-stream join, both
|
||
on the events track (the joiner's records, its gap, and what it shows) and on the
|
||
timelines; and one vector for each refusal of §4.
|
||
- **cue vectors**: cues given as a list, with each target's output or refusal: every anchor
|
||
and italic placement, the disclosure label through every target, 608's special and
|
||
extended characters and substitutions, roll-up with windows of 2, 3 and 4 rows and on a
|
||
second slot clock, and one vector for each refusal of §3, §5, §6 and §8.
|
||
- **read cases**: WebVTT the reader refuses, each with its reason; and **decode cases**: 608
|
||
streams the decoder refuses.
|
||
|
||
Every 608 code path §8 uses is in a committed file: the preamble address codes of indent and
|
||
of italics, each tab offset, both mid-row codes, special and extended characters, Resume
|
||
Caption Loading, Erase Non-displayed Memory, End of Caption, Erase Displayed Memory, each
|
||
roll-up command and Carriage Return. Every reason of §12 refuses a case.
|
||
|
||
A conforming implementation gives every vector and case the outcome, the reason, the index,
|
||
the revisions, what is shown, the cues and every byte of every output the corpus states.
|
||
`wai_timed_text_vectors verify` and the test `tests/timed_text.rs` check that every file
|
||
regenerates byte for byte, that no other file is there, and that `wai-rs` gives every case its
|
||
outcome. `live-conformance/timed_text_verify.py` is the second implementation, written from
|
||
this text: it shares no code with `wai-rs`, verifies events with
|
||
`live_events_verify.py`, writes WebVTT, IMSC1, the timelines, 608 and the disclosure from §5
|
||
to §10, checks every IMSC1 document's structure (§6.3) with an XML parser and the
|
||
hypothetical render model (§6.2), and requires every mutated copy of the corpus to fail.
|
||
|
||
## 14. Security and privacy considerations
|
||
|
||
- Every revision is a signed event, and its annotation another: a relay can drop either and
|
||
cannot alter one. A dropped annotation makes its caption's annotation unknown, which a sink
|
||
shows as a plain caption (§4.4); a dropped revision is a delivery gap ([live-events](live-events.md)
|
||
§6.2).
|
||
- Only a revision's own principal can supersede it (§4.3), so one participant cannot rewrite
|
||
another's captions.
|
||
- A partial's stable prefix is a promise its producer's later revisions are checked against
|
||
(`stability_broken`); it is not a check that the speech was transcribed correctly.
|
||
- The label is the producer's statement. A model digest names a model; it does not prove that
|
||
model wrote the text. A producer that labels machine text human misleads its viewers, and
|
||
the signature on the event makes that statement attributable to it.
|
||
- Text is untrusted input. Writers escape it (§5.1, §6.1, §7) and the reader accepts only the
|
||
subset (§5.3), so cue text cannot inject markup, a cue setting or a timestamp into a target.
|
||
- The model digest can identify the vendor or version of a recognition service. A producer
|
||
that does not want to publish it can only use origin 1, which would mislabel machine text;
|
||
this extension does not offer an anonymous machine origin.
|