Skip to content

Commit e389111

Browse files
committed
Add Packet.rescale_ts
1 parent 2bd7933 commit e389111

4 files changed

Lines changed: 64 additions & 0 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ v18.1.0 (Unreleased)
3333

3434
Features:
3535

36+
- Add ``Packet.rescale_ts()`` to rescale packet PTS, DTS, and duration to a new ``AVRational`` time base by :gh-user:`WyattBlue`.
3637
- Support reusing the thread's current CUDA context via a ``current_ctx`` flag on ``CudaContext`` and ``VideoFrame.from_dlpack``, for interop with libraries like PyTorch that initialize CUDA first by :gh-user:`Yozer` (:pr:`2339`).
3738
- ``VideoFrame.from_dlpack`` no longer requires restating ``primary_ctx``/``current_ctx`` when passing an explicit ``cuda_context``; the flags are only validated when explicitly given by :gh-user:`WyattBlue`.
3839

av/packet.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from cython.cimports.libc.stdint import uint8_t
1212
from cython.cimports.libc.string import memcpy
1313

14+
from av.rational import AVRational
15+
1416
# Check https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/packet.h#L41
1517
# for new additions in the future ffmpeg releases
1618
# Note: the order must follow that of the AVPacketSideDataType enum def
@@ -289,6 +291,25 @@ def _rebase_time(self, dst: lib.AVRational):
289291
lib.av_packet_rescale_ts(self.ptr, self.ptr.time_base, dst)
290292
self.ptr.time_base = dst
291293

294+
def rescale_ts(self, time_base):
295+
"""Rescale the packet timestamps to a new time base.
296+
297+
This rescales :attr:`pts`, :attr:`dts`, and :attr:`duration`, then updates
298+
:attr:`time_base`. If the current time base is unset, the timestamp values
299+
are unchanged and the new time base is assigned.
300+
301+
:meth:`~av.container.OutputContainer.mux` already performs this operation
302+
automatically when necessary.
303+
304+
Wraps :ffmpeg:`av_packet_rescale_ts`.
305+
"""
306+
if not isinstance(time_base, AVRational):
307+
raise TypeError("time_base must be an AVRational")
308+
309+
dst: lib.AVRational
310+
to_avrational(time_base, cython.address(dst))
311+
self._rebase_time(dst)
312+
292313
def decode(self):
293314
"""
294315
Send the packet's data to the decoder and return a list of

av/packet.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ from typing import Generic, Literal, TypeVar, overload
44

55
from av.audio.frame import AudioFrame
66
from av.audio.stream import AudioStream
7+
from av.rational import AVRational
78
from av.stream import Stream
89
from av.subtitles.stream import SubtitleStream
910
from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle
@@ -92,6 +93,7 @@ class Packet(Buffer, Generic[StreamT]):
9293
is_disposable: bool
9394

9495
def __init__(self: Packet[Stream], input: int | bytes | None = None) -> None: ...
96+
def rescale_ts(self, time_base: AVRational) -> None: ...
9597

9698
# Overloads that return the same type as the stream's decode method
9799
@overload

tests/test_packet.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from unittest import SkipTest
77

88
import numpy
9+
import pytest
910

1011
import av
1112

@@ -63,6 +64,45 @@ def test_data_packet_bytes(self):
6364

6465

6566
class TestProperties:
67+
def test_rescale_ts(self) -> None:
68+
packet = av.Packet()
69+
packet.time_base = fractions.Fraction(1, 1000)
70+
packet.pts = 1000
71+
packet.dts = 900
72+
packet.duration = 40
73+
74+
packet.rescale_ts(av.AVRational(1, 100))
75+
76+
assert packet.time_base == fractions.Fraction(1, 100)
77+
assert packet.pts == 100
78+
assert packet.dts == 90
79+
assert packet.duration == 4
80+
81+
def test_rescale_ts_without_source_time_base(self) -> None:
82+
packet = av.Packet()
83+
packet.pts = 1000
84+
packet.dts = 900
85+
packet.duration = 40
86+
87+
packet.rescale_ts(av.AVRational(1, 100))
88+
89+
assert packet.time_base == fractions.Fraction(1, 100)
90+
assert packet.pts == 1000
91+
assert packet.dts == 900
92+
assert packet.duration == 40
93+
94+
def test_rescale_ts_rejects_zero_time_base(self) -> None:
95+
packet = av.Packet()
96+
97+
with pytest.raises(ValueError, match="Cannot rebase to zero time"):
98+
packet.rescale_ts(av.AVRational(0, 1))
99+
100+
def test_rescale_ts_requires_avrational(self) -> None:
101+
packet = av.Packet()
102+
103+
with pytest.raises(TypeError, match="time_base must be an AVRational"):
104+
packet.rescale_ts(fractions.Fraction(1, 100)) # type: ignore[arg-type]
105+
66106
def test_is_keyframe(self) -> None:
67107
with av.open(fate_suite("h264/interlaced_crop.mp4")) as container:
68108
stream = container.streams.video[0]

0 commit comments

Comments
 (0)