Skip to content

Commit 9b91a8f

Browse files
committed
[timecode] Add pts and time base properties
1 parent e86147f commit 9b91a8f

File tree

3 files changed

+40
-25
lines changed

3 files changed

+40
-25
lines changed

scenedetect/common.py

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -172,18 +172,14 @@ def __init__(
172172
TypeError: Thrown if either `timecode` or `fps` are unsupported types.
173173
ValueError: Thrown when specifying a negative timecode or framerate.
174174
"""
175-
# The following two properties are what is used to keep track of time
176-
# in a frame-specific manner. Note that once the framerate is set,
177-
# the value should never be modified (only read if required).
178-
# TODO(v1.0): Make these actual @properties.
179-
self._framerate: Fraction = None
175+
self._rate: Fraction = None
180176
self._frame_num = None
181177
self._timecode: ty.Optional[Timecode] = None
182178
self._seconds: ty.Optional[float] = None
183179

184180
# Copy constructor.
185181
if isinstance(timecode, FrameTimecode):
186-
self._framerate = timecode._framerate if fps is None else fps
182+
self._rate = timecode._rate if fps is None else fps
187183
self._frame_num = timecode._frame_num
188184
self._timecode = timecode._timecode
189185
self._seconds = timecode._seconds
@@ -196,15 +192,15 @@ def __init__(
196192
if fps is None:
197193
raise TypeError("fps is a required argument.")
198194
if isinstance(fps, FrameTimecode):
199-
self._framerate = fps._framerate
195+
self._rate = fps._rate
200196
elif isinstance(fps, float):
201197
if fps <= MAX_FPS_DELTA:
202198
raise ValueError("Framerate must be positive and greater than zero.")
203-
self._framerate = Fraction.from_float(fps)
199+
self._rate = Fraction.from_float(fps)
204200
elif isinstance(fps, Fraction):
205201
if float(fps) <= MAX_FPS_DELTA:
206202
raise ValueError("Framerate must be positive and greater than zero.")
207-
self._framerate = fps
203+
self._rate = fps
208204
else:
209205
raise TypeError(
210206
f"Wrong type for fps: {type(fps)} - expected float, Fraction, or FrameTimecode"
@@ -232,6 +228,8 @@ def __init__(
232228

233229
@property
234230
def frame_num(self) -> ty.Optional[int]:
231+
"""The frame number. This value will be an estimate if the video is VFR. Prefer using the
232+
`pts` property."""
235233
if self._timecode:
236234
# We need to audit anything currently using this property to guarantee temporal
237235
# consistency when handling VFR videos (i.e. no assumptions on fixed frame rate).
@@ -249,8 +247,24 @@ def frame_num(self) -> ty.Optional[int]:
249247
return self._frame_num
250248

251249
@property
252-
def framerate(self) -> ty.Optional[float]:
253-
return float(self._framerate)
250+
def framerate(self) -> float:
251+
"""The framerate to use for distance between frames and to calculate frame numbers.
252+
For a VFR video, this may just be the average framerate."""
253+
return float(self._rate)
254+
255+
@property
256+
def time_base(self) -> Fraction:
257+
"""The time base in which presentation time is calculated."""
258+
if self._timecode:
259+
return self._timecode.time_base
260+
return 1 / self._rate
261+
262+
@property
263+
def pts(self) -> int:
264+
"""The presentation timestamp of the frame in units of `time_base`."""
265+
if self._timecode:
266+
return self._timecode.pts
267+
return self.frame_num
254268

255269
def get_frames(self) -> int:
256270
"""[DEPRECATED] Get the current time/position in number of frames.
@@ -302,8 +316,7 @@ def seconds(self) -> float:
302316
return self._timecode.seconds
303317
if self._seconds:
304318
return self._seconds
305-
# Assume constant framerate if we don't have timing information.
306-
return float(self._frame_num) / self._framerate
319+
return float(self._frame_num / self._rate)
307320

308321
def get_seconds(self) -> float:
309322
"""[DEPRECATED] Get the frame's position in number of seconds.
@@ -372,7 +385,7 @@ def _seconds_to_frames(self, seconds: float) -> int:
372385
373386
*NOTE*: This will not be correct for variable framerate videos.
374387
"""
375-
return round(seconds * self._framerate)
388+
return round(seconds * self._rate)
376389

377390
def _parse_timecode_number(self, timecode: ty.Union[int, float]) -> int:
378391
"""Parse a timecode number, storing it as the exact number of frames.
@@ -406,7 +419,7 @@ def _timecode_to_seconds(self, input: str) -> float:
406419
Raises:
407420
ValueError: Value could not be parsed correctly.
408421
"""
409-
assert self._framerate is not None and self._framerate > MAX_FPS_DELTA
422+
assert self._rate is not None and self._rate > MAX_FPS_DELTA
410423
input = input.strip()
411424
# Exact number of frames N
412425
if input.isdigit():
@@ -452,7 +465,7 @@ def _get_other_as_frames(self, other: ty.Union[int, float, str, "FrameTimecode"]
452465
return self._seconds_to_frames(self._timecode_to_seconds(other))
453466
if isinstance(other, FrameTimecode):
454467
# If comparing two FrameTimecodes, they must have the same framerate for frame-based operations.
455-
if self._framerate and other._framerate and not self.equal_framerate(other._framerate):
468+
if self._rate and other._rate and not self.equal_framerate(other._rate):
456469
raise ValueError(
457470
"FrameTimecode instances require equal framerate for frame-based arithmetic."
458471
)
@@ -530,7 +543,7 @@ def __iadd__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameT
530543
time_base=timecode.time_base,
531544
)
532545
self._seconds = None
533-
self._framerate = None
546+
self._rate = None
534547
self._frame_num = None
535548
return self
536549

@@ -573,7 +586,7 @@ def __isub__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameT
573586
time_base=timecode.time_base,
574587
)
575588
self._seconds = None
576-
self._framerate = None
589+
self._rate = None
577590
self._frame_num = None
578591
return self
579592

@@ -610,8 +623,8 @@ def __repr__(self) -> str:
610623
if self._timecode:
611624
return f"{self.get_timecode()} [pts={self._timecode.pts}, time_base={self._timecode.time_base}]"
612625
if self._seconds is not None:
613-
return f"{self.get_timecode()} [seconds={self._seconds}, fps={self._framerate}]"
614-
return f"{self.get_timecode()} [frame_num={self._frame_num}, fps={self._framerate}]"
626+
return f"{self.get_timecode()} [seconds={self._seconds}, fps={self._rate}]"
627+
return f"{self.get_timecode()} [frame_num={self._frame_num}, fps={self._rate}]"
615628

616629
def __hash__(self) -> int:
617630
if self._timecode:
@@ -628,7 +641,7 @@ def _get_other_as_seconds(self, other: ty.Union[int, float, str, "FrameTimecode"
628641
if _USE_PTS_IN_DEVELOPMENT and other == 1:
629642
return self.seconds
630643
raise NotImplementedError()
631-
return float(other) / self._framerate
644+
return float(other) / self._rate
632645
if isinstance(other, float):
633646
return other
634647
if isinstance(other, str):
@@ -639,4 +652,4 @@ def _get_other_as_seconds(self, other: ty.Union[int, float, str, "FrameTimecode"
639652

640653

641654
def _compare_as_fixed(a: FrameTimecode, b: ty.Any) -> bool:
642-
return a._framerate is not None and isinstance(b, FrameTimecode) and b._framerate is not None
655+
return a._rate is not None and isinstance(b, FrameTimecode) and b._rate is not None

scenedetect/output/image.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,8 @@ def image_save_thread(self, save_queue: queue.Queue, progress_bar: tqdm):
293293
def generate_timecode_list(self, scene_list: SceneList) -> ty.List[ty.Iterable[FrameTimecode]]:
294294
"""Generates a list of timecodes for each scene in `scene_list` based on the current config
295295
parameters."""
296-
framerate = scene_list[0][0]._framerate
296+
# TODO(v0.7): This needs to be fixed as part of PTS overhaul.
297+
framerate = scene_list[0][0].framerate
297298
# TODO(v1.0): Split up into multiple sub-expressions so auto-formatter works correctly.
298299
return [
299300
(
@@ -450,7 +451,7 @@ def save_images(
450451
image_num_format = "%0"
451452
image_num_format += str(math.floor(math.log(num_images, 10)) + 2) + "d"
452453

453-
framerate = scene_list[0][0]._framerate
454+
framerate = scene_list[0][0]._rate
454455

455456
# TODO(v1.0): Split up into multiple sub-expressions so auto-formatter works correctly.
456457
timecode_list = [

website/pages/changelog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -713,4 +713,5 @@ Although there have been minimal changes to most API examples, there are several
713713
* Deprecated functionality preserved from v0.6 now uses the `warnings` module
714714
* Add properties to access `frame_num`, `framerate`, and `seconds` from `FrameTimecode` instead of getter methods
715715
* Add new `Timecode` type to represent frame timings in terms of the video's source timebase
716-
* Expand `FrameTimecode` representations to preserve accuracy (previously all timecodes were rounded to frame boundaries)
716+
* Add new `time_base` and `pts` properties to `FrameTimecode` to provide more accurate timing information
717+

0 commit comments

Comments
 (0)