Repository navigation
[backends] Remove genpts option from fflags in PyAV backend #590 - #592
Breakthrough wants to merge 1 commit into
Conversation
fdb87ce to
8a0991e
Compare
2251d26 to
21877f0
Compare
|
The policy selects a source based on the first frame in the order PTS -> DTS -> frames. This approach is mostly sound, but I noticed a few issues: Example 1: Assume the source time base is PTS: m 2 5 7 Since both PTS and DTS are missing for the first frame, we choose frames mode. This ignores the valid timestamps on later frames and can produce positions that differ from the source timestamps. Example 2: Assume a fixed time base and a zero stream start offset. PTS: 1 4 m m 7 Extrapolation causes the output timestamps to become non-monotonic, even though the available source PTS are monotonic. |
|
If we are going to use a new policy for handling missing timestamps, I think the same policy should be applied across all backends. The main issue here is that OpenCV's FFmpeg backend doesn't expose separate raw decoded PTS or DTS through This means that, under the current backend interfaces, such a fixed-source policy can only be applied directly to PyAV. Either applied across all versions or only versions that do not expose My opinion is that this change is too complex for either case, especially since the new policy has its own problems. We can use |
MuhammadBilalKhan267
left a comment
There was a problem hiding this comment.
I'd prefer we wait for PyAV's response on the backport request. I think this policy is worthwhile only if we can address the extrapolation issues.
21877f0 to
e1cdebc
Compare
|
Thanks for taking the time to go through this so carefully. Both examples you gave are correct, I was able to reproduce them with real videos. Remuxing a VFR clip to MPEG TS and removing the PTS/DTS in some of the PES headers demonstrates the two examples you gave. I added a regression test ( With these changes I think it addresses the extrapolation issue you pointed out. Those examples should now result in: Both are in On waiting for OpenCV already applies this rule internally (re: |
8341488 to
10ad2d8
Compare
10ad2d8 to
7114d31
Compare
Breakthrough
left a comment
There was a problem hiding this comment.
Thanks again, agreed on all five comments and reproduced the cases you described. Pushed fixes for everything you pointed out and updated the outdated PR description.
PyAV sets `AVFMT_FLAG_GENPTS` on every input (see PyAV-Org/PyAV#2477 / PyAV-Org/PyAV#2478). On containers that store DTS only (AVI, MPEG-TS) and contain B-frames, or streams that have no timestamps at all, the generated PTS can come out in the incorrect decode order. This was causing the PyAV backend to report non-monotonic timestamps in these cases, resulting in incorrect splits (#590). This PR now ensures we set `fflags=-genpts` to override the `+genpts` flag from PyAV. We also fallback from PTS to DTS, and count frames when there are no timestamps entirely. We also add support for when PyAV exposes `Frame.best_effort_timestamp` in a future release (tested locally with the development changes linked above). Lastly, a one-time warning is logged if timestamps are detected going backwards. Users hitting this issue should update PyAV to 19.1+ to resolve it. We cannot pin to PyAV 19.1+ because it requires Python 3.12, but `scenedetect` supports 3.10+ and 3.11 is supported until October 2027. Follow-ups kept as TODOs: - AVI tick rate and frame counts in `_get_duration` - MPEG-TS seeks that land after the target position
7114d31 to
92a0638
Compare
MuhammadBilalKhan267
left a comment
There was a problem hiding this comment.
Thanks for addressing the earlier cases. I found two more seek issues.
| "unknown position" if landed is None else landed, | ||
| target, | ||
| ) | ||
| self._reset_for_seek() |
There was a problem hiding this comment.
On issue-590-missing-pts.ts, seek(1) followed by read() returns the first image instead of the second. After the reset, both the position and adjusted target are 0, so the loop doesn't read the first frame. Could we read one frame after resetting before entering the loop?
| # any timestamps (e.g. raw H.264) cannot be seeked, so give up after | ||
| # `MAX_SEEK_PROBE_FRAMES` frames. As with any failed seek, the stream position is then | ||
| # undefined until `reset()` is called. | ||
| if beginning and self._offset is None: |
There was a problem hiding this comment.
After reading five frames from issue-590-missing-pts.ts, seeking back to frame 0 returns frame 61 on the next read. This also happens on main.
Could we reset the stream whenever seeking to frame 0? Currently, we only reset when the offset is unknown, so the container seek can land later in the video.
PyAV sets
AVFMT_FLAG_GENPTSon every input (see PyAV-Org/PyAV#933, PyAV-Org/PyAV#2477, and PyAV-Org/PyAV#2478).On containers that store DTS only (AVI, some MPEG-TS) and contain B-frames, the generated PTS comes out permuted in decode order after reordering B-frames. This currently causes the PyAV backend to report non-monotonic timestamps, resulting in shifted or missing cuts (#590). Furthermore, streams without timestamps stay stuck at time 0.
Frame.best_effort_timestamp(PyAV 19.1+, Python 3.12+) does not help on its own, since withgenptson it is still wrong for the initial few frames (until FFmpeg's heuristic switches to DTS).This PR sets
fflags=-genptswhen opening containers to override the+genptsflag from PyAV on all PyAV versions, and places each frame at its PTS, falling back to its DTS when the PTS is missing (best_effort_timestampis not used on any version). This is the same per-frame rule OpenCV's FFmpeg backend uses. Withgenptsoff, FFmpeg'sbest_effort_timestampgives the same result on streams with monotonic PTS values. FFmpeg's heuristic switches to DTS once PTS has gone backwards more often than DTS - this PR does not to keep things simpler for now. We follow the PTS and warn. A frame with neither timestamp is placed one (nominal) frame time after the previous frame. A single start offset is decided from the first frame that carries a timestamp such that frame 1 is at time 0. This is the stream'sstart_timewhen frame 1 has a PTS (as before), otherwise the timestamp minus the nominal duration of the frames before it. This covers DTS-only streams, as the first DTS includes the decoder's reordering delay, as well as streams that lack timestamps at the beginning.VideoStreamAv.seek()changes:SeekErroron streams that cannot be seeked, instead of a backend-specific exception. As documented onVideoStream.seek(), the stream position is undefined after aSeekErroruntilreset()is called.start_time, which differs on DTS-only streams and streams whose first frames lack timestamps).read()will never land before the target.MAX_SEEK_PROBE_FRAMESwith a valid timestamp, or the first timestamp we get is past the target, we reset back to the beginning of the stream and slow seek from there. This should only happen on malformed inputs whose landing keyframe lacks timestamps.seek(0)re-opens the stream while the offset is still unknown, since seeking the container could land on a later keyframe and anchor the offset there.A one-time warning is logged if positions are still observed going backwards, including when a timestamp lands before the estimated position of the frames without timestamps preceding it.
Test fixtures (on the
resourcesbranch):issue-590-bframes.avi/.h264: first 480 frames of goldeneye.mp4 with B-frames, as a DTS-only container and as a raw elementary stream.issue-590-missing-pts.ts: goldeneye-vfr-drop3.mp4 remuxed to MPEG-TS with the PTS/DTS of the first six video PES packets, and two in every seven after the tenth, replaced by PES stuffing bytes. After decoding, frames 1 through 4 have no timestamps, frame 5 only a DTS, and later frames sporadically lack PTS, DTS or both.Follow-ups kept as TODOs:
_get_durationstill need some work.