@@ -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
641654def _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
0 commit comments