Skip to content

VideoTimestamps

Bases: ABCTimestamps

Create a Timestamps object from a video file.

Source code in video_timestamps/video_timestamps.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
class VideoTimestamps(ABCTimestamps):
    """Create a Timestamps object from a video file.
    """

    def __init__(
        self,
        pts_list: list[int],
        time_scale: Fraction,
        normalize: bool = True,
        fps: Fraction | None = None,
    ):
        """Initialize the VideoTimestamps object.

        Parameters:
            pts_list: A list containing the Presentation Time Stamps (PTS) for all frames.

                The last pts correspond to the pts of the last frame + it's duration.
            time_scale: Unit of time (in seconds) in terms of which frame timestamps are represented.

                Important: Don't confuse time_scale with the time_base. As a reminder, time_base = 1 / time_scale.
            normalize: If True, it will shift the PTS to make them start from 0. If false, the option does nothing.
            fps: The frames per second of the video.

                It doesn't matter if you pass this parameter because the fps isn't used.
                It is only a parameter to avoid breaking change
        """
        # Validate the PTS
        if len(pts_list) <= 1:
            raise ValueError("There must be at least 2 pts.")

        if any(pts_list[i] >= pts_list[i + 1] for i in range(len(pts_list) - 1)):
            raise ValueError("PTS must be in non-decreasing order.")

        self.__pts_list = pts_list
        self.__time_scale = time_scale

        if normalize:
            self.__pts_list = VideoTimestamps.normalize(self.pts_list)

        self.__timestamps = [pts / self.time_scale for pts in self.pts_list]

        if fps is None:
            self.__fps = Fraction(len(pts_list) - 1, Fraction((self.pts_list[-1] - self.pts_list[0]), self.time_scale))
        else:
            self.__fps = fps

    @classmethod
    @overload
    def from_video_file(
        cls,
        video_path: Path,
        index: int = 0,
        normalize: bool = True,
        use_video_provider_to_guess_fps: bool = True,
        video_provider: ABCVideoProvider | None = None,
        video_stream_index: None = None
    ) -> VideoTimestamps:
        ...

    @classmethod
    @overload
    def from_video_file(
        cls,
        video_path: Path,
        index: None,
        normalize: bool = True,
        use_video_provider_to_guess_fps: bool = True,
        video_provider: ABCVideoProvider | None = None,
        *,
        video_stream_index: int
    ) -> VideoTimestamps:
        ...

    @classmethod
    def from_video_file(
        cls,
        video_path: Path,
        index: int | None = 0,
        normalize: bool = True,
        use_video_provider_to_guess_fps: bool = True,
        video_provider: ABCVideoProvider | None = None,
        video_stream_index: int | None = None
    ) -> VideoTimestamps:
        """Create timestamps based on the ``video_path`` provided.

        Parameters:
            video_path: A video path.
            index: Absolute index of the stream in the file. Its position among *all* streams
                (audio, video, subtitles, etc.), regardless of their type.

                This is equivalent to ffmpeg/ffprobe's global stream index (ex: the `0` in `-map 0:0`).
                The stream at this index must be a video stream.

                Mutually exclusive with `video_stream_index`. Exactly one of the two must be specified.
            normalize: If True, it will shift the PTS to make them start from 0. If false, the option does nothing.
            use_video_provider_to_guess_fps: If True, use the video_provider to guess the video fps.
                If not specified, the fps will be approximate from the first and last frame PTS.
            video_provider: The video provider to use to get the information about the video timestamps/fps.
                If not specified, it will default to [`FFMS2VideoProvider`][video_timestamps.video_provider.ffms2_video_provider.FFMS2VideoProvider].
            video_stream_index: Index of the video stream, relative to the other video streams in the file.

                This is equivalent to ffmpeg's `v` stream specifier (ex: `v:0` is the first video stream,
                `v:1` is the second video stream, etc).

                Mutually exclusive with `index`. Exactly one of the two must be specified.

        Returns:
            An VideoTimestamps instance representing the video file.
        """
        if video_provider is None:
            video_provider = FFMS2VideoProvider()

        if not video_path.is_file():
            raise FileNotFoundError(f'Invalid path for the video file: "{video_path}"')

        pts_list, time_base, fps_from_video_provider = video_provider.get_pts(str(video_path.resolve()), index, video_stream_index)
        time_scale = 1 / time_base

        if use_video_provider_to_guess_fps:
            fps = fps_from_video_provider
        else:
            fps = None

        timestamps = VideoTimestamps(
            pts_list,
            time_scale,
            normalize,
            fps
        )
        return timestamps

    @property
    def fps(self) -> Fraction:
        return self.__fps

    @property
    def time_scale(self) -> Fraction:
        return self.__time_scale

    @property
    def first_timestamps(self) -> Fraction:
        return self.timestamps[0]

    @property
    def pts_list(self) -> list[int]:
        """
        Returns:
            A list containing the Presentation Time Stamps (PTS) for all frames.
                The last pts correspond to the pts of the last frame + it's duration.
        """
        return self.__pts_list

    @property
    def timestamps(self) -> list[Fraction]:
        """
        Returns:
            A list of timestamps (in seconds) corresponding to each frame, stored as `Fraction` for precision.
        """
        return self.__timestamps

    @property
    def nbr_frames(self) -> int:
        """
        Returns:
            Number of frames in the video.
        """
        return len(self.__pts_list) - 1

    @staticmethod
    def normalize(pts_list: list[int]) -> list[int]:
        """Shift the pts_list to make them start from 0. This way, frame 0 will start at pts 0.

        Parameters:
            pts_list: A list containing the Presentation Time Stamps (PTS) for all frames.

        Returns:
            The pts_list normalized.
        """
        if pts_list[0]:
            return [pts - pts_list[0] for pts in pts_list]
        return pts_list


    def _time_to_frame(
        self,
        time: Fraction,
        time_type: TimeType,
    ) -> int:
        if time > self.timestamps[-1]:
            if time_type == TimeType.END:
                return self.nbr_frames
            else:
                raise ValueError(f"Time {time} is over the video duration. The video duration is {self.timestamps[-1]} seconds.")

        if time_type == TimeType.START:
            return bisect_left(self.timestamps, time)
        elif time_type == TimeType.END:
            return bisect_left(self.timestamps, time) - 1
        elif time_type == TimeType.EXACT:
            return bisect_right(self.timestamps, time) - 1
        else:
            raise ValueError(f'The TimeType "{time_type}" isn\'t supported.')


    def _frame_to_time(
        self,
        frame: int,
    ) -> Fraction:
        if frame > self.nbr_frames:
            raise ValueError(f"The frame {frame} is over the video duration. The video contains {self.nbr_frames} frames.")

        return self.timestamps[frame]


    def __eq__(self, other: object) -> bool:
        if not isinstance(other, VideoTimestamps):
            return False
        return (self.fps, self.time_scale, self.first_timestamps, self.pts_list, self.timestamps) == (
            other.fps, other.time_scale, other.first_timestamps, other.pts_list, other.timestamps
        )

    @overload
    def export_timestamps(
        self,
        timestamps_filename: Path,
        *,
        use_fraction: Literal[True],
    ) -> None:
        ...

    @overload
    def export_timestamps(
        self,
        timestamps_filename: Path,
        *,
        precision: int,
        precision_rounding: RoundingCallType,
        use_fraction: Literal[False] = False,
    ) -> None:
        ...

    def export_timestamps(
        self,
        timestamps_filename: Path,
        *,
        precision: int | None = 9,
        precision_rounding: RoundingCallType | None = RoundingMethod.ROUND,
        use_fraction: bool = False
    ) -> None:
        """Export the timestamps to [timestamp format v2 file](https://mkvtoolnix.download/doc/mkvmerge.html#d4e4659).

        Parameters:
            timestamps_filename: The file path where the timestamps will be saved.
            precision: Number of decimal places for timestamps (default: 9).
                The minimum value is 3. Note that for mkv file, you can always use 9 (the default value).

                Common values:

                - 3 means milliseconds
                - 6 means microseconds
                - 9 means nanoseconds
            precision_rounding: Rounding method to use for timestamps (default: round).

                Examples:

                - Timestamp: 453.4 ms,  precision=3, precision_rounding=RoundingMethod.ROUND --> 453
                - Timestamp: 453.4569 ms, precision=6, precision_rounding=RoundingMethod.ROUND --> 453.457
            use_fraction: The timestamps produced will be represented has a fraction (ex: "30/2") instead of decimal (ex: "3.434").
                Note that this is not a conform to the specification.
        """
        if precision is not None and precision < 3:
            raise ValueError("The precision needs to be at least 3 (milliseconds).")

        with localcontext() as ctx, open(timestamps_filename, "w", encoding="utf-8") as f:
            f.write("# timestamp format v2\n")

            for pts in self.pts_list:
                if use_fraction:
                    time_ms = pts / self.time_scale * 1000
                    f.write(f"{time_ms}\n")
                else:
                    assert precision is not None # Make mypy happy
                    assert precision_rounding is not None # Make mypy happy

                    time_precision = precision_rounding(pts / self.time_scale * pow(10, precision))
                    time_ms = Fraction(time_precision, pow(10, precision - 3))

                    # Be sure that decimal.Context.prec is high enough to do the conversion
                    num_digits = len(str(time_ms.numerator // time_ms.denominator))
                    ctx.prec = (precision - 3) + num_digits

                    time_ms_d = Decimal(time_ms.numerator) / Decimal(time_ms.denominator)
                    f.write(f"{time_ms_d}\n")


    def __hash__(self) -> int:
        return hash(
            (
                self.fps,
                self.time_scale,
                self.first_timestamps,
                tuple(self.pts_list),
                tuple(self.timestamps),
            )
        )

pts_list property

Returns:

Type Description
list[int]

A list containing the Presentation Time Stamps (PTS) for all frames. The last pts correspond to the pts of the last frame + it's duration.

timestamps property

Returns:

Type Description
list[Fraction]

A list of timestamps (in seconds) corresponding to each frame, stored as Fraction for precision.

nbr_frames property

Returns:

Type Description
int

Number of frames in the video.

__init__(pts_list, time_scale, normalize=True, fps=None)

Initialize the VideoTimestamps object.

Parameters:

Name Type Description Default
pts_list list[int]

A list containing the Presentation Time Stamps (PTS) for all frames.

The last pts correspond to the pts of the last frame + it's duration.

required
time_scale Fraction

Unit of time (in seconds) in terms of which frame timestamps are represented.

Important: Don't confuse time_scale with the time_base. As a reminder, time_base = 1 / time_scale.

required
normalize bool

If True, it will shift the PTS to make them start from 0. If false, the option does nothing.

True
fps Fraction | None

The frames per second of the video.

It doesn't matter if you pass this parameter because the fps isn't used. It is only a parameter to avoid breaking change

None
Source code in video_timestamps/video_timestamps.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def __init__(
    self,
    pts_list: list[int],
    time_scale: Fraction,
    normalize: bool = True,
    fps: Fraction | None = None,
):
    """Initialize the VideoTimestamps object.

    Parameters:
        pts_list: A list containing the Presentation Time Stamps (PTS) for all frames.

            The last pts correspond to the pts of the last frame + it's duration.
        time_scale: Unit of time (in seconds) in terms of which frame timestamps are represented.

            Important: Don't confuse time_scale with the time_base. As a reminder, time_base = 1 / time_scale.
        normalize: If True, it will shift the PTS to make them start from 0. If false, the option does nothing.
        fps: The frames per second of the video.

            It doesn't matter if you pass this parameter because the fps isn't used.
            It is only a parameter to avoid breaking change
    """
    # Validate the PTS
    if len(pts_list) <= 1:
        raise ValueError("There must be at least 2 pts.")

    if any(pts_list[i] >= pts_list[i + 1] for i in range(len(pts_list) - 1)):
        raise ValueError("PTS must be in non-decreasing order.")

    self.__pts_list = pts_list
    self.__time_scale = time_scale

    if normalize:
        self.__pts_list = VideoTimestamps.normalize(self.pts_list)

    self.__timestamps = [pts / self.time_scale for pts in self.pts_list]

    if fps is None:
        self.__fps = Fraction(len(pts_list) - 1, Fraction((self.pts_list[-1] - self.pts_list[0]), self.time_scale))
    else:
        self.__fps = fps

from_video_file(video_path, index=0, normalize=True, use_video_provider_to_guess_fps=True, video_provider=None, video_stream_index=None) classmethod

from_video_file(
    video_path: Path,
    index: int = 0,
    normalize: bool = True,
    use_video_provider_to_guess_fps: bool = True,
    video_provider: ABCVideoProvider | None = None,
    video_stream_index: None = None,
) -> VideoTimestamps
from_video_file(
    video_path: Path,
    index: None,
    normalize: bool = True,
    use_video_provider_to_guess_fps: bool = True,
    video_provider: ABCVideoProvider | None = None,
    *,
    video_stream_index: int,
) -> VideoTimestamps

Create timestamps based on the video_path provided.

Parameters:

Name Type Description Default
video_path Path

A video path.

required
index int | None

Absolute index of the stream in the file. Its position among all streams (audio, video, subtitles, etc.), regardless of their type.

This is equivalent to ffmpeg/ffprobe's global stream index (ex: the 0 in -map 0:0). The stream at this index must be a video stream.

Mutually exclusive with video_stream_index. Exactly one of the two must be specified.

0
normalize bool

If True, it will shift the PTS to make them start from 0. If false, the option does nothing.

True
use_video_provider_to_guess_fps bool

If True, use the video_provider to guess the video fps. If not specified, the fps will be approximate from the first and last frame PTS.

True
video_provider ABCVideoProvider | None

The video provider to use to get the information about the video timestamps/fps. If not specified, it will default to FFMS2VideoProvider.

None
video_stream_index int | None

Index of the video stream, relative to the other video streams in the file.

This is equivalent to ffmpeg's v stream specifier (ex: v:0 is the first video stream, v:1 is the second video stream, etc).

Mutually exclusive with index. Exactly one of the two must be specified.

None

Returns:

Type Description
VideoTimestamps

An VideoTimestamps instance representing the video file.

Source code in video_timestamps/video_timestamps.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
@classmethod
def from_video_file(
    cls,
    video_path: Path,
    index: int | None = 0,
    normalize: bool = True,
    use_video_provider_to_guess_fps: bool = True,
    video_provider: ABCVideoProvider | None = None,
    video_stream_index: int | None = None
) -> VideoTimestamps:
    """Create timestamps based on the ``video_path`` provided.

    Parameters:
        video_path: A video path.
        index: Absolute index of the stream in the file. Its position among *all* streams
            (audio, video, subtitles, etc.), regardless of their type.

            This is equivalent to ffmpeg/ffprobe's global stream index (ex: the `0` in `-map 0:0`).
            The stream at this index must be a video stream.

            Mutually exclusive with `video_stream_index`. Exactly one of the two must be specified.
        normalize: If True, it will shift the PTS to make them start from 0. If false, the option does nothing.
        use_video_provider_to_guess_fps: If True, use the video_provider to guess the video fps.
            If not specified, the fps will be approximate from the first and last frame PTS.
        video_provider: The video provider to use to get the information about the video timestamps/fps.
            If not specified, it will default to [`FFMS2VideoProvider`][video_timestamps.video_provider.ffms2_video_provider.FFMS2VideoProvider].
        video_stream_index: Index of the video stream, relative to the other video streams in the file.

            This is equivalent to ffmpeg's `v` stream specifier (ex: `v:0` is the first video stream,
            `v:1` is the second video stream, etc).

            Mutually exclusive with `index`. Exactly one of the two must be specified.

    Returns:
        An VideoTimestamps instance representing the video file.
    """
    if video_provider is None:
        video_provider = FFMS2VideoProvider()

    if not video_path.is_file():
        raise FileNotFoundError(f'Invalid path for the video file: "{video_path}"')

    pts_list, time_base, fps_from_video_provider = video_provider.get_pts(str(video_path.resolve()), index, video_stream_index)
    time_scale = 1 / time_base

    if use_video_provider_to_guess_fps:
        fps = fps_from_video_provider
    else:
        fps = None

    timestamps = VideoTimestamps(
        pts_list,
        time_scale,
        normalize,
        fps
    )
    return timestamps

normalize(pts_list) staticmethod

Shift the pts_list to make them start from 0. This way, frame 0 will start at pts 0.

Parameters:

Name Type Description Default
pts_list list[int]

A list containing the Presentation Time Stamps (PTS) for all frames.

required

Returns:

Type Description
list[int]

The pts_list normalized.

Source code in video_timestamps/video_timestamps.py
184
185
186
187
188
189
190
191
192
193
194
195
196
@staticmethod
def normalize(pts_list: list[int]) -> list[int]:
    """Shift the pts_list to make them start from 0. This way, frame 0 will start at pts 0.

    Parameters:
        pts_list: A list containing the Presentation Time Stamps (PTS) for all frames.

    Returns:
        The pts_list normalized.
    """
    if pts_list[0]:
        return [pts - pts_list[0] for pts in pts_list]
    return pts_list

export_timestamps(timestamps_filename, *, precision=9, precision_rounding=RoundingMethod.ROUND, use_fraction=False)

export_timestamps(
    timestamps_filename: Path,
    *,
    use_fraction: Literal[True],
) -> None
export_timestamps(
    timestamps_filename: Path,
    *,
    precision: int,
    precision_rounding: RoundingCallType,
    use_fraction: Literal[False] = False,
) -> None

Export the timestamps to timestamp format v2 file.

Parameters:

Name Type Description Default
timestamps_filename Path

The file path where the timestamps will be saved.

required
precision int | None

Number of decimal places for timestamps (default: 9). The minimum value is 3. Note that for mkv file, you can always use 9 (the default value).

Common values:

  • 3 means milliseconds
  • 6 means microseconds
  • 9 means nanoseconds
9
precision_rounding RoundingCallType | None

Rounding method to use for timestamps (default: round).

Examples:

  • Timestamp: 453.4 ms, precision=3, precision_rounding=RoundingMethod.ROUND --> 453
  • Timestamp: 453.4569 ms, precision=6, precision_rounding=RoundingMethod.ROUND --> 453.457
ROUND
use_fraction bool

The timestamps produced will be represented has a fraction (ex: "30/2") instead of decimal (ex: "3.434"). Note that this is not a conform to the specification.

False
Source code in video_timestamps/video_timestamps.py
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
def export_timestamps(
    self,
    timestamps_filename: Path,
    *,
    precision: int | None = 9,
    precision_rounding: RoundingCallType | None = RoundingMethod.ROUND,
    use_fraction: bool = False
) -> None:
    """Export the timestamps to [timestamp format v2 file](https://mkvtoolnix.download/doc/mkvmerge.html#d4e4659).

    Parameters:
        timestamps_filename: The file path where the timestamps will be saved.
        precision: Number of decimal places for timestamps (default: 9).
            The minimum value is 3. Note that for mkv file, you can always use 9 (the default value).

            Common values:

            - 3 means milliseconds
            - 6 means microseconds
            - 9 means nanoseconds
        precision_rounding: Rounding method to use for timestamps (default: round).

            Examples:

            - Timestamp: 453.4 ms,  precision=3, precision_rounding=RoundingMethod.ROUND --> 453
            - Timestamp: 453.4569 ms, precision=6, precision_rounding=RoundingMethod.ROUND --> 453.457
        use_fraction: The timestamps produced will be represented has a fraction (ex: "30/2") instead of decimal (ex: "3.434").
            Note that this is not a conform to the specification.
    """
    if precision is not None and precision < 3:
        raise ValueError("The precision needs to be at least 3 (milliseconds).")

    with localcontext() as ctx, open(timestamps_filename, "w", encoding="utf-8") as f:
        f.write("# timestamp format v2\n")

        for pts in self.pts_list:
            if use_fraction:
                time_ms = pts / self.time_scale * 1000
                f.write(f"{time_ms}\n")
            else:
                assert precision is not None # Make mypy happy
                assert precision_rounding is not None # Make mypy happy

                time_precision = precision_rounding(pts / self.time_scale * pow(10, precision))
                time_ms = Fraction(time_precision, pow(10, precision - 3))

                # Be sure that decimal.Context.prec is high enough to do the conversion
                num_digits = len(str(time_ms.numerator // time_ms.denominator))
                ctx.prec = (precision - 3) + num_digits

                time_ms_d = Decimal(time_ms.numerator) / Decimal(time_ms.denominator)
                f.write(f"{time_ms_d}\n")