Skip to content

DVB-TTML implementation - #1242

Draft
kierank wants to merge 12 commits into
Upipe:masterfrom
kierank:ttml
Draft

DVB-TTML implementation#1242
kierank wants to merge 12 commits into
Upipe:masterfrom
kierank:ttml

Conversation

@kierank

@kierank kierank commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

No description provided.

kierank and others added 10 commits August 26, 2026 19:19
…nition

The sub stored a new flow definition on its output the moment it arrived,
while the subpictures in its schedule keep being output long after: pages
decode ahead of their pts, so the pixels of the on-screen page went
downstream under the flow definition of the next one.  This was harmless
while all subpictures of a flow had the same dimensions, but with the pic
size and padding attributes changing per subpicture, downstream rescaled the
current page to the geometry of the next page for the whole decode-ahead
window, stretching it and putting it in the wrong place.

Pair each buffered subpicture with the flow definition it was received
under, and store that on the output when the subpicture starts being output.
The output helper deduplicates identical flow definitions, so flows with
constant dimensions behave as before.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The vertical scaling of the destination rectangle derived bpad from tpad, so
an asymmetric padding put the picture at the wrong size and position.  Only
subpictures whose flow definition carries pic padding attributes are
affected; for everything else both are zero.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The renderer read the coverage of a glyph at buffer[j * width + i], which is
only where it is when the bitmap has no padding between its lines.  Both the
small bitmap cache and a rendered glyph say what the distance between two
lines is, so use it: with a padded bitmap the glyph was sheared by a pixel per
line and read past the end of the buffer on the last ones.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The left of the box was taken from the first glyph under "if (!i)", but the
string index is advanced before the test, so it was never zero and the box
always started at zero however far into the line the first glyph sat.  Take it
from the first glyph that is actually looked up.

The height was assigned twice, the second time without the guard the first one
had, which made the guard do nothing; drop the second one.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The scaler handed to the cache manager is a stack structure with two fields
left unset; the manager keeps a copy of it.  They are the resolutions, which
it only reads when the sizes are not in pixels, so nothing came of it, but
zero them rather than hash and store whatever was on the stack.

FT_Get_Advance leaves its output untouched when it fails, and the result was
added to the total either way.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Three different mismatches shared one message that named none of them, so a
subpicture being dropped told you only that it was.  Split the unset
destination rectangle and the unreadable picture into their own messages, and
give the size mismatch the size that arrived and the size that was expected.

The flow format the sub provides upstream is what decides that size, so log
what it worked out and the canvas and padding it worked it out from.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
EN 303 560 signals a DVB TTML subtitle stream with stream_type 0x06 and
extension descriptor 0x20 in the PMT, so the descriptor is what makes the ES a
subtitle stream, as for teletext and DVB subtitles.  Give it the flow
definition block.dvb_ttml_subtitle.pic.sub. and export the language, the
subtitle purpose, the text-to-speech suitability, the processor profiles and
the qualifier on the flow definition.

Extension descriptors used to fall through to the default case and be copied
verbatim onto the flow definition; the other tag extensions keep that
behaviour.

The maximum retention time is the maximum period of activation of a segment,
5 seconds (5.2.3.3): nothing in the stream is buffered for longer.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The PES payload of a DVB TTML subtitle stream is not an elementary stream: it
is a PES_data_field (EN 303 560 5.2.2.2) carrying a segment media time, a list
of subtitle segments and a CRC.  Unwrap it into a stream of TTML documents,
block.ttml.pic.sub., each dated with the PTS of its PES packet and carrying
its segment media time, which is what converts TTML times to the MPEG
timeline (5.2.4.1).

The PES decapsulator hands over the payload of one transport stream packet at
a time, marking the first chunk of a PES packet and its last and dating the
first, so the packet is put back together before it is parsed: a
PES_data_field only has a checkable CRC once all of it is there.  A packet
that never ends is handed over at the next one and left to the CRC, and one
that outgrows a PES packet is dropped.

An uncompressed segment is output by moving the block window onto the
document, so the payload is never copied.  A gzip segment (type 0x02) is
inflated straight into a block taken from the ubuf manager, when zlib is
available.  Nothing is allocated per packet.

A segment whose CRC fails is dropped rather than passed on, which leaves the
previously received segment active until it expires, as 5.2.4.2 requires.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The pipe rendered one string, in one colour, at one size, on a baseline the
application picked.  A subtitle page is a handful of regions, each holding
runs with their own colour, size and background, wrapped and aligned inside
the region: describe one with the uref_text attributes and let the renderer
measure the runs, break them into lines and lay them out.

The picture is cropped to what is drawn and the distance to each edge of the
canvas goes on the flow definition as a pic padding, the convention the DVB
subtitle decoder already uses: downstream rebuilds the canvas from the size
and the paddings, so two lines of subtitle are not rescaled and alpha blended
over a whole frame-sized picture on every frame.  Nothing is advertised until
there is something to describe: a first negotiation at the size of the canvas
settles on a chain that does not scale, and no later crop gets a chance to
undo it.

Compositing divides by the resulting alpha rather than mixing the source
colour straight in.  On the opaque background of the single string path the
two are the same expression, but a page starts out transparent, and mixing in
the colour of a transparent pixel darkens the edge of every glyph.  Neither of
the two ends of that needs the division: nothing underneath leaves the source
colour as it stands, and an opaque result is a mix by 255, which is a pair of
shifts.  Subsampling is a shift too, so a pixel costs no division at all
unless it is translucent over translucent.

Only one font file is configured, so bold and italic are synthesized by
emboldening and shearing a copy of the cached outline; the plain style still
comes from the small bitmap cache.  Glyph blitting honours the pitch of the
bitmap, which is not its width for a glyph the small bitmap cache would not
hold.

A page is bounded, so the scratch it is laid out in comes with the pipe and
rendering one allocates nothing.  The smallest picture is the floor the DVB
subtitle decoder uses for an empty subtitle: a couple of pixels is not a legal
size once the chroma is subsampled.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The pipe reads the TTML documents the demuxer produces and turns each of them
into the styled text pages of uref_text, which upipe_freetype renders.

A document is a sequence of intermediate synchronic documents: whenever a
piece of content becomes active or inactive one ends and the next begins.
Collect the times at which that happens inside the activation window of the
segment, and emit one page per interval, dated with
PTS(Tx) = Pi + (Tx - Ti) as EN 303 560 5.2.4.1 defines, and given the
duration of the interval so the scheduler holds it for exactly that long.  An
interval with no active content emits a page with no region, which clears the
screen.

The times in a document are on the timeline of the document, which begins at
zero, and the segment media time is the point of that timeline the PTS
corresponds to.  Resolving them against the activation window instead would
offset every one of them by the media time and leave nothing inside it.

Styling covers what DVB TTML subtitles use: referential and nested styles,
regions with an origin, an extent and a padding, displayAlign and textAlign,
per span colour, background colour, font size, weight, style and decoration,
line height, wrapping, and the EBU-TT-D linePadding, multiRowAlign and
fillLineGap.  A referenced style is applied onto the inherited context rather
than resolved on its own, so a font size given as a percentage resolves
against the size the referencing element inherits.

Alignment and line styling are content properties that EBU-TT-D puts on the
paragraph as often as on the region, so the region takes what its first
paragraph gives and falls back on its own.

The canvas the regions are positioned on has to be set by the application:
there is no default, because a canvas of the wrong size puts every region in
the wrong place rather than merely rendering them at the wrong resolution.

The document is parsed out of the block the demuxer hands over rather than a
copy of it, and a page is laid out in scratch that comes with the pipe.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@cmassiot
cmassiot requested a review from nto August 27, 2026 10:57
The PES decapsulator hands over the payload of one transport stream packet at
a time, so a PES_data_field only exists, and only has a checkable CRC, once
the chunks marked with the block start and end flags have been put back
together.  That is what a framer does, and DVB subtitles and teletext, both
PES carried, segment structured and no more an elementary stream than this
one, are already framed by upipe_dvbsubf and upipe_telxf.

The CRC is the only structural check EN 303 560 offers, and it is what says
the reassembly was right, so it belongs in the framer as well.  It is taken
one ubuf at a time over the chain the reassembly builds, so the framer needs
no ubuf manager and never has to merge.

The framer also sets what the decapsulator never did: the flow definition is
marked complete, and the DTS to PTS delay is zeroed because the PES header of
a TTML subtitle stream carries a PTS and no DTS.  The T-STD octet rate and
buffer size are left unset for now.

autof allocates it on block.dvb_ttml_subtitle., as it does for the other two.
The pipe reassembled the PES packets itself, checked their CRC and then
unwrapped the TTML document out of them.  upipe_ttmlf does the first two now,
so drop them here: what is left is the decapsulation proper, the PES data
field header, the segment walk, the gzip inflation and the media time, and the
pipe receives one whole data field per uref.

ts_demux therefore plumbs the TTML output as pesd, autof and ts_ttmld, with
autof picking the framer, and takes that branch only once an autof manager has
been set.
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.

1 participant