PR #23852 opened by superuser404
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23852
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23852.patch

The current handling of TrackTimestampScale produces timestamps on an axis that 
matches no interpretation of the element.

`matroska_read_header()` bakes the scale into the stream time base:

```c
avpriv_set_pts_info(st, 64, matroska->time_scale * track->time_scale,
                    1000 * 1000 * 1000);
```

but `matroska_parse_block()` divides only the cluster component by it:

```c
uint64_t timecode_cluster_in_track_tb = (double) cluster_time / 
track->time_scale;
timecode = timecode_cluster_in_track_tb + block_time - 
track->codec_delay_in_track_tb;
```

so a block's effective timestamp is

```
seconds = cluster * TimestampScale + rel * TimestampScale * TTS
```

RFC 9559 section 4.5 defines the block's effective timestamp as 
ClusterTimestamp + relative timestamp on the segment axis, with 
TrackTimestampScale acting as a playback speed adjustment relative to other 
tracks. The historical matroska.org reading applies TTS to the whole sum. The 
code implements neither: the cluster component is unscaled while the 
block-relative component is scaled. For TTS > 1 the result is non-monotonic in 
storage order whenever a late relative timestamp of one cluster exceeds an 
early one of the next cluster, and the track drifts against its sibling tracks. 
All of this is silent today, since the existing guard only warns for TTS < 0.01.

### Reproducer

A file with TTS = 10.0 and clusters at 0 and 1000 ms containing blocks at 
relative 0/100/200 and 0/100 (generator below):

```
$ ffprobe -show_entries packet=pts_time tts10.mkv
```

Before (non-monotonic, matches neither interpretation):

```
0.000000
1.000000
2.000000
1.000000
2.000000
```

After (segment axis, monotonic, plus a warning):

```
[matroska,webm @ ...] TrackTimestampScale 10.000000 not supported, assuming 1.0 
(timestamps stay on the segment axis).
0.000000
0.100000
0.200000
1.000000
1.100000
```

### Why clamp instead of implementing the element

TrackTimestampScale is deprecated (RFC 9559 caps it at Matroska version 3), and 
readers commonly ignore it; matroska.org's own notes state that using a value 
other than 1.0 "might not work in many places". Clamping to 1.0 with a warning 
keeps every track on the coherent segment axis and in sync with its sibling 
tracks, and makes the deviation visible instead of silently emitting broken 
timestamps.

If implementing one of the two real interpretations is preferred instead, the 
analysis above still applies; I picked the clamp because it matches what other 
mainstream demuxers effectively do with this element. Happy to adjust.

<details>
<summary>gen_mkv_tts.py</summary>

```python
#!/usr/bin/env python3
import struct
import sys

def vint_size(n):
    for length in range(1, 9):
        if n < (1 << (7 * length)) - 1:
            data = n | (1 << (7 * length))
            return data.to_bytes(length, "big")
    raise ValueError(n)

def elem(eid, payload):
    return bytes.fromhex(eid) + vint_size(len(payload)) + payload

def uint(n):
    return n.to_bytes(8, "big").lstrip(b"\x00") or b"\x00"

def simpleblock(track, rel, data, keyframe=True):
    return elem("A3", b"\x81" + struct.pack(">h", rel) +
                bytes([0x80 if keyframe else 0x00]) + data)

ebml = elem("1A45DFA3",
    elem("4286", uint(1)) + elem("42F7", uint(1)) +
    elem("42F2", uint(4)) + elem("42F3", uint(8)) +
    elem("4282", b"matroska") + elem("4287", uint(4)) + elem("4285", uint(2)))

info = elem("1549A966",
    elem("2AD7B1", uint(1000000)) +
    elem("4D80", b"tts-testgen") + elem("5741", b"tts-testgen"))

track = elem("1654AE6B", elem("AE",
    elem("D7", uint(1)) + elem("73C5", uint(1)) +
    elem("83", uint(0x11)) + elem("9C", uint(0)) +
    elem("86", b"S_TEXT/UTF8") +
    elem("23314F", struct.pack(">d", 10.0))))

cluster_a = elem("1F43B675",
    elem("E7", uint(0)) +
    simpleblock(1, 0, b"a") + simpleblock(1, 100, b"b") + simpleblock(1, 200, 
b"c"))

cluster_b = elem("1F43B675",
    elem("E7", uint(1000)) +
    simpleblock(1, 0, b"d") + simpleblock(1, 100, b"e"))

segment = elem("18538067", info + track + cluster_a + cluster_b)

with open(sys.argv[1] if len(sys.argv) > 1 else "tts10.mkv", "wb") as f:
    f.write(ebml + segment)
```

</details>



>From c555e434daf993e8c945c1e561eb15c16c92feb0 Mon Sep 17 00:00:00 2001
From: Vincent Herbst <[email protected]>
Date: Sun, 19 Jul 2026 20:11:45 +0200
Subject: [PATCH] avformat/matroskadec: clamp TrackTimestampScale to 1.0

The current handling of TrackTimestampScale is incoherent.
matroska_read_header() bakes the scale into the stream time base
(TimestampScale * TrackTimestampScale nanoseconds), but
matroska_parse_block() divides only the cluster component by it:

    timecode_cluster_in_track_tb = (double) cluster_time / track->time_scale;
    timecode = timecode_cluster_in_track_tb + block_time - ...

so a block's timestamp lands on a hybrid axis:

    seconds = (cluster * TimestampScale) + (rel * TimestampScale * TTS)

RFC 9559 (section 4.5) defines the block's effective timestamp as
Cluster Timestamp + relative timestamp on the segment axis, with
TrackTimestampScale as a playback speed adjustment relative to other
tracks; the historical matroska.org reading applies TTS to the whole
sum. The current code implements neither: for TTS > 1 timestamps are
non-monotonic in storage order whenever a late relative timestamp of
one cluster exceeds an early one of the next, and the track drifts
against its siblings. All of it silent, since the existing guard only
warns for TTS < 0.01.

A file with TTS = 10.0 and clusters at 0 and 1000ms containing blocks
at relative 0/100/200 and 0/100 currently demuxes to
pts 0.0, 1.0, 2.0, 1.0, 2.0 seconds.

TrackTimestampScale is deprecated (RFC 9559 caps it at Matroska
version 3) and readers commonly ignore it. Do the same, explicitly:
clamp to 1.0 with a warning, so every track stays on the coherent
segment axis and in sync with its sibling tracks. The same file then
demuxes to pts 0.0, 0.1, 0.2, 1.0, 1.1 seconds.
---
 libavformat/matroskadec.c | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/libavformat/matroskadec.c b/libavformat/matroskadec.c
index 18849af689..b922ff9919 100644
--- a/libavformat/matroskadec.c
+++ b/libavformat/matroskadec.c
@@ -3281,9 +3281,10 @@ static int matroska_parse_tracks(AVFormatContext *s)
             av_dict_set(&st->metadata, "language", track->language, 0);
         av_dict_set(&st->metadata, "title", track->name, 0);
 
-        if (track->time_scale < 0.01) {
+        if (track->time_scale != 1.0) {
             av_log(matroska->ctx, AV_LOG_WARNING,
-                   "Track TimestampScale too small %f, assuming 1.0.\n",
+                   "TrackTimestampScale %f not supported, assuming 1.0 "
+                   "(timestamps stay on the segment axis).\n",
                    track->time_scale);
             track->time_scale = 1.0;
         }
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to