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

`MatroskaDemuxContext.done` latches end of stream. `matroska_resync()` and the 
end-of-segment check set it, and only `matroska_read_seek()` clears it. Two 
seek paths never reach `read_seek`:

- `AVSEEK_FLAG_BYTE` goes straight to `seek_frame_byte()`,
- an index seek that fails falls back to `seek_frame_generic()`, which reads 
packets itself and then repositions.

Both only move the IO position and set `io_repositioned`. So once a file has 
been read to its end, or a read has failed, every later `av_read_frame()` 
returns `AVERROR_EOF` at once, wherever the seek landed.

**Reproduction on master:** read any Matroska file to EOF, call 
`av_seek_frame(s, -1, 0, AVSEEK_FLAG_BYTE)`, and read again. No packets come 
back. Measured on four files from the FATE suite:

| file | first pass | after the byte seek, master | with this patch |
|---|---|---|---|
| `mkv/codec_delay_opus.mkv` | 52 | 0 | 52 |
| `mkv/lzo.mka` | 4 | 0 | 4 |
| `mkv/prores_bz2.mkv` | 4 | 0 | 4 |
| `mkv/hdr10tags-both.mkv` | 10 | 0 | 10 |

This came up in a player with a custom AVIOContext over HTTP. A positioning 
read was cut by a deadline, the timestamp seek fell back, and the byte-position 
fallback afterwards read EOF at second 0 of an 888 s file.

**The fix:** `matroska_read_packet()` now honours `io_repositioned` the way 
oggdec and av1dec already do. It resets the parsing state at the new position, 
drops queued packets and a pending keyframe skip, and clears `done`. 
`matroska_read_seek()` clears `io_repositioned` on success, so a read after it 
does not reset the state it has just set up. The `resync_pos == -1` handling 
for the generic path stays as it is.

**Tests:** the second commit adds `tests/api/api-seek-byte-test.c`, which reads 
a file to its end, byte seeks to 0, reads it again and prints both passes. 
`fate-api-seek-byte-mkv` runs it on `mkv/lzo.mka`. It fails on master, where 
the second pass prints `packets:0`, and passes with the fix. The test is not 
Matroska specific, so other demuxers can be added to it. `make fate-source` and 
245 FATE tests matching matroska, mkv, webm, seek and api pass, including 
`fate-seek-lavf-mkv`.



>From 10088856d59748271d2c55271ba2817e88e83d7c Mon Sep 17 00:00:00 2001
From: Vincent Herbst <[email protected]>
Date: Sun, 1 Nov 2026 21:57:07 +0100
Subject: [PATCH 1/2] avformat/matroskadec: forget an end of stream after a
 generic or byte seek

The demuxer latches end of stream in MatroskaDemuxContext.done, which
matroska_resync() and the end-of-segment check set, and only
matroska_read_seek() clears it. A seek can bypass read_seek, though:
AVSEEK_FLAG_BYTE goes straight to seek_frame_byte(), and an index seek
that fails falls back to seek_frame_generic(). Both only move the IO
position and set io_repositioned. So once the file had been read to
its end, or a read had failed, every later read returned AVERROR_EOF
at once, wherever the seek had landed. Reading a file to EOF and byte
seeking back to 0 read no packets at all.

read_packet now honours io_repositioned the way oggdec and av1dec do:
it resets the parsing state at the new position, drops the queued
packets and a pending keyframe skip, and clears done.
matroska_read_seek() clears io_repositioned, so a read after it does
not reset the state it has just set up.

Signed-off-by: Vincent Herbst <[email protected]>
---
 libavformat/matroskadec.c | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/libavformat/matroskadec.c b/libavformat/matroskadec.c
index d36cc5c3452..f2a9735c951 100644
--- a/libavformat/matroskadec.c
+++ b/libavformat/matroskadec.c
@@ -4500,6 +4500,17 @@ static int matroska_read_packet(AVFormatContext *s, 
AVPacket *pkt)
     MatroskaDemuxContext *matroska = s->priv_data;
     int ret = 0;
 
+    if (s->io_repositioned) {
+        /* A generic or byte seek moved the read position without
+         * matroska_read_seek(): parse from the new position, and forget an
+         * end of stream reached before it. */
+        s->io_repositioned = 0;
+        matroska_reset_status(matroska, 0, -1);
+        matroska_clear_queue(matroska);
+        matroska->skip_to_keyframe = 0;
+        matroska->done             = 0;
+    }
+
     if (matroska->resync_pos == -1) {
         // This can only happen if generic seeking has been used.
         matroska->resync_pos = avio_tell(s->pb);
@@ -4569,6 +4580,7 @@ static int matroska_read_seek(AVFormatContext *s, int 
stream_index,
     }
     matroska->skip_to_keyframe = 1;
     matroska->done             = 0;
+    s->io_repositioned         = 0;
     avpriv_update_cur_dts(s, st, sti->index_entries[index].timestamp);
     return 0;
 err:
-- 
2.54.0


>From 61e5e7d0ede36c5cfb3f8dce90f4d162a6993667 Mon Sep 17 00:00:00 2001
From: Vincent Herbst <[email protected]>
Date: Sun, 1 Nov 2026 21:57:08 +0100
Subject: [PATCH 2/2] tests/api: add a byte seek test, run on Matroska

Read a file to its end, byte seek back to 0 and read it again, printing
every packet of both passes. Without the previous commit the second
pass of the Matroska run prints no packets.

Signed-off-by: Vincent Herbst <[email protected]>
---
 tests/api/Makefile               |  1 +
 tests/api/api-seek-byte-test.c   | 77 ++++++++++++++++++++++++++++++++
 tests/fate/api.mak               |  4 ++
 tests/ref/fate/api-seek-byte-mkv | 12 +++++
 4 files changed, 94 insertions(+)
 create mode 100644 tests/api/api-seek-byte-test.c
 create mode 100644 tests/ref/fate/api-seek-byte-mkv

diff --git a/tests/api/Makefile b/tests/api/Makefile
index c411e82e5f1..966673865bf 100644
--- a/tests/api/Makefile
+++ b/tests/api/Makefile
@@ -7,6 +7,7 @@ APITESTPROGS-$(CONFIG_MOV_MUXER)        += api-movenc
 APITESTPROGS-$(CONFIG_MOVTEXT_ENCODER)  += api-movtext
 APITESTPROGS-$(call ALLYES, HEVC_MP4TOANNEXB_BSF HEVC_DECODER) += 
api-hevc-mp4toannexb
 APITESTPROGS-yes += api-seek api-dump-stream-meta
+APITESTPROGS-yes += api-seek-byte
 APITESTPROGS-$(call DEMDEC, H263, H263) += api-band
 APITESTPROGS-$(HAVE_THREADS) += api-threadmessage
 APITESTPROGS-$(CONFIG_AVCODEC) += api-enc-info
diff --git a/tests/api/api-seek-byte-test.c b/tests/api/api-seek-byte-test.c
new file mode 100644
index 00000000000..b7a4ca42a09
--- /dev/null
+++ b/tests/api/api-seek-byte-test.c
@@ -0,0 +1,77 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * Read a file to its end, seek back to its first byte with AVSEEK_FLAG_BYTE
+ * and read it again, printing every packet of both passes. A demuxer that
+ * keeps an end of stream across the byte seek prints nothing for the second.
+ */
+
+#include <inttypes.h>
+#include <stdio.h>
+
+#include "libavformat/avformat.h"
+#include "libavutil/adler32.h"
+
+static int read_pass(AVFormatContext *s, AVPacket *pkt, const char *name)
+{
+    int ret, count = 0;
+
+    printf("%s\n", name);
+    while ((ret = av_read_frame(s, pkt)) >= 0) {
+        printf("st:%d pts:%" PRId64 " pos:%" PRId64 " size:%d 
adler:0x%08"PRIx32"\n",
+               pkt->stream_index, pkt->pts, pkt->pos, pkt->size,
+               av_adler32_update(0, pkt->data, pkt->size));
+        av_packet_unref(pkt);
+        count++;
+    }
+    printf("packets:%d\n", count);
+    return ret == AVERROR_EOF ? 0 : ret;
+}
+
+int main(int argc, char **argv)
+{
+    AVFormatContext *s = NULL;
+    AVPacket *pkt = NULL;
+    int ret;
+
+    if (argc < 2) {
+        fprintf(stderr, "usage: %s <input>\n", argv[0]);
+        return 1;
+    }
+
+    if ((ret = avformat_open_input(&s, argv[1], NULL, NULL)) < 0)
+        return 1;
+    if (!(pkt = av_packet_alloc())) {
+        ret = AVERROR(ENOMEM);
+        goto end;
+    }
+
+    if ((ret = read_pass(s, pkt, "first pass")) < 0)
+        goto end;
+    if ((ret = av_seek_frame(s, -1, 0, AVSEEK_FLAG_BYTE)) < 0) {
+        printf("byte seek failed\n");
+        goto end;
+    }
+    ret = read_pass(s, pkt, "after byte seek to 0");
+
+end:
+    av_packet_free(&pkt);
+    avformat_close_input(&s);
+    return ret < 0;
+}
diff --git a/tests/fate/api.mak b/tests/fate/api.mak
index 1659c7979c0..a6ad8fe50e6 100644
--- a/tests/fate/api.mak
+++ b/tests/fate/api.mak
@@ -43,6 +43,10 @@ FATE_API_SAMPLES_LIBAVFORMAT-$(call DEMDEC, H264, H264) += 
fate-api-h264-slice
 fate-api-h264-slice: $(APITESTSDIR)/api-h264-slice-test$(EXESUF)
 fate-api-h264-slice: CMD = run $(APITESTSDIR)/api-h264-slice-test$(EXESUF) 2 
$(TARGET_SAMPLES)/h264/crew_cif.nal
 
+FATE_API_SAMPLES_LIBAVFORMAT-$(CONFIG_MATROSKA_DEMUXER) += 
fate-api-seek-byte-mkv
+fate-api-seek-byte-mkv: $(APITESTSDIR)/api-seek-byte-test$(EXESUF)
+fate-api-seek-byte-mkv: CMD = run $(APITESTSDIR)/api-seek-byte-test$(EXESUF) 
$(TARGET_SAMPLES)/mkv/lzo.mka
+
 FATE_API_LIBAVFORMAT-yes += $(if $(findstring 
fate-lavf-flv,$(FATE_LAVF_CONTAINER)),fate-api-seek)
 fate-api-seek: $(APITESTSDIR)/api-seek-test$(EXESUF) fate-lavf-flv
 fate-lavf-flv: KEEP_FILES ?= 1
diff --git a/tests/ref/fate/api-seek-byte-mkv b/tests/ref/fate/api-seek-byte-mkv
new file mode 100644
index 00000000000..918b5be2ac6
--- /dev/null
+++ b/tests/ref/fate/api-seek-byte-mkv
@@ -0,0 +1,12 @@
+first pass
+st:0 pts:0 pos:279 size:16384 adler:0x00000000
+st:0 pts:4096 pos:279 size:16384 adler:0xad7eebf4
+st:0 pts:8192 pos:279 size:16384 adler:0x1d1ff9d9
+st:0 pts:12288 pos:279 size:16384 adler:0xf1d9e2e2
+packets:4
+after byte seek to 0
+st:0 pts:0 pos:279 size:16384 adler:0x00000000
+st:0 pts:4096 pos:279 size:16384 adler:0xad7eebf4
+st:0 pts:8192 pos:279 size:16384 adler:0x1d1ff9d9
+st:0 pts:12288 pos:279 size:16384 adler:0xf1d9e2e2
+packets:4
-- 
2.54.0

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

Reply via email to