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]
