Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions av/container/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,9 @@ def open(
:param str format: Specific format to use. Defaults to autodect.
:param dict options: Options to pass to the container and all streams.
:param dict container_options: Options to pass to the container.
Input containers set the ``genpts`` flag, which fills in missing packet PTS.
Pass ``{"fflags": "-genpts"}`` to turn it off. See also
:attr:`.Frame.best_effort_timestamp`.
:param int buffer_size: Size of buffer for Python input/output operations in bytes.
Honored only when ``file`` is a file-like object. Defaults to 32768 (32k).
:param timeout: How many seconds to wait for data before giving up, as a float, or a
Expand Down
17 changes: 17 additions & 0 deletions av/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,23 @@ def pts(self, value):
else:
self.ptr.pts = value

@property
def best_effort_timestamp(self):
"""
The presentation timestamp estimated by the decoder, in :attr:`time_base` units.

This is the timestamp FFmpeg's own tools use. It is :attr:`pts`, unless the
PTS values have been missing or out of order more often than the DTS
values, in which case it is :attr:`dts`. Prefer it over :attr:`pts` for
inputs that store only DTS, such as AVI, whose generated PTS can go
backwards when the video has B-frames.

:type: int | None
"""
if self.ptr.best_effort_timestamp == lib.AV_NOPTS_VALUE:
return None
return self.ptr.best_effort_timestamp

@property
def duration(self):
"""
Expand Down
2 changes: 2 additions & 0 deletions av/frame.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ from av.sidedata.sidedata import SideDataContainer
class Frame:
dts: int | None
pts: int | None
@property
def best_effort_timestamp(self) -> int | None: ...
duration: int
@property
def time_base(self) -> AVRational: ...
Expand Down
1 change: 1 addition & 0 deletions include/avcodec.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ cdef extern from "libavcodec/avcodec.h" nogil:

int64_t pts
int64_t pkt_dts
int64_t best_effort_timestamp
void *opaque
int sample_rate
AVBufferRef *buf[8]
Expand Down
9 changes: 9 additions & 0 deletions tests/test_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ def test_decoded_video_frame_count(self) -> None:

assert frame_count == video_stream.frames

def test_best_effort_timestamp(self) -> None:
assert av.VideoFrame(16, 16, "yuv420p").best_effort_timestamp is None

with av.open(fate_suite("h264/interlaced_crop.mp4")) as container:
for frame in container.decode(video=0):
# MP4 stores valid PTS, so the decoder's estimate is the PTS.
assert frame.pts is not None
assert frame.best_effort_timestamp == frame.pts

def test_flushed_frames_keep_time_base(self) -> None:
# `decode()` with no packet has no packet to take the time base from;
# it must fall back to the one the container set on the context.
Expand Down
Loading