diff --git a/av/frame.pyi b/av/frame.pyi index 9af81dcfe..38a273afc 100644 --- a/av/frame.pyi +++ b/av/frame.pyi @@ -9,6 +9,7 @@ class SideData(TypedDict, total=False): class Frame: dts: int | None pts: int | None + duration: int | None time_base: Fraction side_data: SideData opaque: object diff --git a/av/frame.pyx b/av/frame.pyx index 57681bbcd..fefdd2dee 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -53,6 +53,9 @@ cdef class Frame: if self.ptr.pts != lib.AV_NOPTS_VALUE: self.ptr.pts = lib.av_rescale_q(self.ptr.pts, self._time_base, dst) + if self.ptr.duration != 0: + self.ptr.duration = lib.av_rescale_q(self.ptr.duration, self._time_base, dst) + self._time_base = dst @property @@ -95,6 +98,24 @@ cdef class Frame: else: self.ptr.pts = value + @property + def duration(self): + """ + The duration of the frame in :attr:`time_base` units + + :type: int + """ + if self.ptr.duration == 0: + return None + return self.ptr.duration + + @duration.setter + def duration(self, value): + if value is None: + self.ptr.duration = 0 + else: + self.ptr.duration = value + @property def time(self): """ diff --git a/av/video/frame.pyi b/av/video/frame.pyi index bba60cc5d..313e184f9 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -30,6 +30,7 @@ class PictureType(IntEnum): class VideoFrame(Frame): format: VideoFormat pts: int + duration: int planes: tuple[VideoPlane, ...] pict_type: int colorspace: int diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 5bed3583d..0c8713cf8 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -461,6 +461,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVColorTransferCharacteristic color_trc AVColorSpace colorspace + int64_t duration + cdef AVFrame* avcodec_alloc_frame() cdef struct AVPacket: diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 26549b31b..677bc1fc1 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -49,6 +49,21 @@ def test_opaque() -> None: assert type(frame.opaque) is tuple and len(frame.opaque) == 2 +def test_frame_duration_matches_packet() -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + packet_durations = [ + (p.pts, p.duration) for p in container.demux() if p.pts is not None + ] + packet_durations.sort(key=lambda x: x[0]) + + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + frame_durations = [(f.pts, f.duration) for f in container.decode(video=0)] + frame_durations.sort(key=lambda x: x[0]) + + assert len(packet_durations) == len(frame_durations) + assert all(pd[1] == fd[1] for pd, fd in zip(packet_durations, frame_durations)) + + def test_invalid_pixel_format() -> None: with pytest.raises(ValueError, match="not a pixel format: '__unknown_pix_fmt'"): VideoFrame(640, 480, "__unknown_pix_fmt")