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
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
    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 = FFMS2VideoProvider()
    ) -> VideoTimestamps:
        """Create timestamps based on the ``video_path`` provided.

        Parameters:
            video_path: A video path.
            index: Index of the video stream.
            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.

        Returns:
            An VideoTimestamps instance representing the video file.
        """

        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)
        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 list(map(lambda pts: pts - pts_list[0], 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:
            with 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=FFMS2VideoProvider()) classmethod

Create timestamps based on the video_path provided.

Parameters:

Name Type Description Default
video_path Path

A video path.

required
index int

Index of the video stream.

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

The video provider to use to get the information about the video timestamps/fps.

FFMS2VideoProvider()

Returns:

Type Description
VideoTimestamps

An VideoTimestamps instance representing the video file.

Source code in video_timestamps/video_timestamps.py
 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
@classmethod
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 = FFMS2VideoProvider()
) -> VideoTimestamps:
    """Create timestamps based on the ``video_path`` provided.

    Parameters:
        video_path: A video path.
        index: Index of the video stream.
        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.

    Returns:
        An VideoTimestamps instance representing the video file.
    """

    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)
    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
141
142
143
144
145
146
147
148
149
150
151
152
153
@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 list(map(lambda pts: pts - pts_list[0], 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
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
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:
        with 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")