@@ -172,18 +172,21 @@ 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+ # NOTE: FrameTimecode will have either a `Timecode` representation, a `seconds`
176+ # representation, or only a frame number. We cache the calculated values for later use
177+ # for the parameters that are missing .
178+ self . _rate : Fraction = None
179+ """Rate at which time passes between frames, measured in frames/sec."""
180180 self ._frame_num = None
181+ """Frame number which may be estimated."""
181182 self ._timecode : ty .Optional [Timecode ] = None
183+ """Presentation timestamp from the backend."""
182184 self ._seconds : ty .Optional [float ] = None
185+ """An explicit point in time."""
183186
184187 # Copy constructor.
185188 if isinstance (timecode , FrameTimecode ):
186- self ._framerate = timecode ._framerate if fps is None else fps
189+ self ._rate = timecode ._rate if fps is None else fps
187190 self ._frame_num = timecode ._frame_num
188191 self ._timecode = timecode ._timecode
189192 self ._seconds = timecode ._seconds
@@ -196,15 +199,15 @@ def __init__(
196199 if fps is None :
197200 raise TypeError ("fps is a required argument." )
198201 if isinstance (fps , FrameTimecode ):
199- self ._framerate = fps ._framerate
202+ self ._rate = fps ._rate
200203 elif isinstance (fps , float ):
201204 if fps <= MAX_FPS_DELTA :
202205 raise ValueError ("Framerate must be positive and greater than zero." )
203- self ._framerate = Fraction .from_float (fps )
206+ self ._rate = Fraction .from_float (fps )
204207 elif isinstance (fps , Fraction ):
205208 if float (fps ) <= MAX_FPS_DELTA :
206209 raise ValueError ("Framerate must be positive and greater than zero." )
207- self ._framerate = fps
210+ self ._rate = fps
208211 else :
209212 raise TypeError (
210213 f"Wrong type for fps: { type (fps )} - expected float, Fraction, or FrameTimecode"
@@ -232,6 +235,8 @@ def __init__(
232235
233236 @property
234237 def frame_num (self ) -> ty .Optional [int ]:
238+ """The frame number. This value will be an estimate if the video is VFR. Prefer using the
239+ `pts` property."""
235240 if self ._timecode :
236241 # We need to audit anything currently using this property to guarantee temporal
237242 # consistency when handling VFR videos (i.e. no assumptions on fixed frame rate).
@@ -249,8 +254,24 @@ def frame_num(self) -> ty.Optional[int]:
249254 return self ._frame_num
250255
251256 @property
252- def framerate (self ) -> ty .Optional [float ]:
253- return float (self ._framerate )
257+ def framerate (self ) -> float :
258+ """The framerate to use for distance between frames and to calculate frame numbers.
259+ For a VFR video, this may just be the average framerate."""
260+ return float (self ._rate )
261+
262+ @property
263+ def time_base (self ) -> Fraction :
264+ """The time base in which presentation time is calculated."""
265+ if self ._timecode :
266+ return self ._timecode .time_base
267+ return 1 / self ._rate
268+
269+ @property
270+ def pts (self ) -> int :
271+ """The presentation timestamp of the frame in units of `time_base`."""
272+ if self ._timecode :
273+ return self ._timecode .pts
274+ return self .frame_num
254275
255276 def get_frames (self ) -> int :
256277 """[DEPRECATED] Get the current time/position in number of frames.
@@ -302,8 +323,7 @@ def seconds(self) -> float:
302323 return self ._timecode .seconds
303324 if self ._seconds :
304325 return self ._seconds
305- # Assume constant framerate if we don't have timing information.
306- return float (self ._frame_num ) / self ._framerate
326+ return float (self ._frame_num / self ._rate )
307327
308328 def get_seconds (self ) -> float :
309329 """[DEPRECATED] Get the frame's position in number of seconds.
@@ -372,7 +392,7 @@ def _seconds_to_frames(self, seconds: float) -> int:
372392
373393 *NOTE*: This will not be correct for variable framerate videos.
374394 """
375- return round (seconds * self ._framerate )
395+ return round (seconds * self ._rate )
376396
377397 def _parse_timecode_number (self , timecode : ty .Union [int , float ]) -> int :
378398 """Parse a timecode number, storing it as the exact number of frames.
@@ -406,7 +426,7 @@ def _timecode_to_seconds(self, input: str) -> float:
406426 Raises:
407427 ValueError: Value could not be parsed correctly.
408428 """
409- assert self ._framerate is not None and self ._framerate > MAX_FPS_DELTA
429+ assert self ._rate is not None and self ._rate > MAX_FPS_DELTA
410430 input = input .strip ()
411431 # Exact number of frames N
412432 if input .isdigit ():
@@ -452,7 +472,7 @@ def _get_other_as_frames(self, other: ty.Union[int, float, str, "FrameTimecode"]
452472 return self ._seconds_to_frames (self ._timecode_to_seconds (other ))
453473 if isinstance (other , FrameTimecode ):
454474 # 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 ):
475+ if self ._rate and other ._rate and not self .equal_framerate (other ._rate ):
456476 raise ValueError (
457477 "FrameTimecode instances require equal framerate for frame-based arithmetic."
458478 )
@@ -530,7 +550,7 @@ def __iadd__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameT
530550 time_base = timecode .time_base ,
531551 )
532552 self ._seconds = None
533- self ._framerate = None
553+ self ._rate = None
534554 self ._frame_num = None
535555 return self
536556
@@ -573,7 +593,7 @@ def __isub__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameT
573593 time_base = timecode .time_base ,
574594 )
575595 self ._seconds = None
576- self ._framerate = None
596+ self ._rate = None
577597 self ._frame_num = None
578598 return self
579599
@@ -610,8 +630,8 @@ def __repr__(self) -> str:
610630 if self ._timecode :
611631 return f"{ self .get_timecode ()} [pts={ self ._timecode .pts } , time_base={ self ._timecode .time_base } ]"
612632 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 } ]"
633+ return f"{ self .get_timecode ()} [seconds={ self ._seconds } , fps={ self ._rate } ]"
634+ return f"{ self .get_timecode ()} [frame_num={ self ._frame_num } , fps={ self ._rate } ]"
615635
616636 def __hash__ (self ) -> int :
617637 if self ._timecode :
@@ -628,7 +648,7 @@ def _get_other_as_seconds(self, other: ty.Union[int, float, str, "FrameTimecode"
628648 if _USE_PTS_IN_DEVELOPMENT and other == 1 :
629649 return self .seconds
630650 raise NotImplementedError ()
631- return float (other ) / self ._framerate
651+ return float (other ) / self ._rate
632652 if isinstance (other , float ):
633653 return other
634654 if isinstance (other , str ):
@@ -639,4 +659,4 @@ def _get_other_as_seconds(self, other: ty.Union[int, float, str, "FrameTimecode"
639659
640660
641661def _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
662+ return a ._rate is not None and isinstance (b , FrameTimecode ) and b ._rate is not None
0 commit comments