@@ -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"
@@ -214,9 +217,11 @@ def __init__(
214217 if isinstance (timecode , Timecode ):
215218 self ._timecode = timecode
216219 return
220+
217221 # Process the timecode value, storing it as an exact number of frames only if required.
218222 if isinstance (timecode , str ) and timecode .isdigit ():
219223 timecode = int (timecode )
224+
220225 if isinstance (timecode , str ):
221226 self ._seconds = self ._timecode_to_seconds (timecode )
222227 elif isinstance (timecode , float ):
@@ -232,6 +237,8 @@ def __init__(
232237
233238 @property
234239 def frame_num (self ) -> ty .Optional [int ]:
240+ """The frame number. This value will be an estimate if the video is VFR. Prefer using the
241+ `pts` property."""
235242 if self ._timecode :
236243 # We need to audit anything currently using this property to guarantee temporal
237244 # consistency when handling VFR videos (i.e. no assumptions on fixed frame rate).
@@ -249,8 +256,24 @@ def frame_num(self) -> ty.Optional[int]:
249256 return self ._frame_num
250257
251258 @property
252- def framerate (self ) -> ty .Optional [float ]:
253- return float (self ._framerate )
259+ def framerate (self ) -> float :
260+ """The framerate to use for distance between frames and to calculate frame numbers.
261+ For a VFR video, this may just be the average framerate."""
262+ return float (self ._rate )
263+
264+ @property
265+ def time_base (self ) -> Fraction :
266+ """The time base in which presentation time is calculated."""
267+ if self ._timecode :
268+ return self ._timecode .time_base
269+ return 1 / self ._rate
270+
271+ @property
272+ def pts (self ) -> int :
273+ """The presentation timestamp of the frame in units of `time_base`."""
274+ if self ._timecode :
275+ return self ._timecode .pts
276+ return self .frame_num
254277
255278 def get_frames (self ) -> int :
256279 """[DEPRECATED] Get the current time/position in number of frames.
@@ -302,8 +325,7 @@ def seconds(self) -> float:
302325 return self ._timecode .seconds
303326 if self ._seconds :
304327 return self ._seconds
305- # Assume constant framerate if we don't have timing information.
306- return float (self ._frame_num ) / self ._framerate
328+ return float (self ._frame_num / self ._rate )
307329
308330 def get_seconds (self ) -> float :
309331 """[DEPRECATED] Get the frame's position in number of seconds.
@@ -372,7 +394,7 @@ def _seconds_to_frames(self, seconds: float) -> int:
372394
373395 *NOTE*: This will not be correct for variable framerate videos.
374396 """
375- return round (seconds * self ._framerate )
397+ return round (seconds * self ._rate )
376398
377399 def _parse_timecode_number (self , timecode : ty .Union [int , float ]) -> int :
378400 """Parse a timecode number, storing it as the exact number of frames.
@@ -406,7 +428,7 @@ def _timecode_to_seconds(self, input: str) -> float:
406428 Raises:
407429 ValueError: Value could not be parsed correctly.
408430 """
409- assert self ._framerate is not None and self ._framerate > MAX_FPS_DELTA
431+ assert self ._rate is not None and self ._rate > MAX_FPS_DELTA
410432 input = input .strip ()
411433 # Exact number of frames N
412434 if input .isdigit ():
@@ -452,7 +474,7 @@ def _get_other_as_frames(self, other: ty.Union[int, float, str, "FrameTimecode"]
452474 return self ._seconds_to_frames (self ._timecode_to_seconds (other ))
453475 if isinstance (other , FrameTimecode ):
454476 # 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 ):
477+ if self ._rate and other ._rate and not self .equal_framerate (other ._rate ):
456478 raise ValueError (
457479 "FrameTimecode instances require equal framerate for frame-based arithmetic."
458480 )
@@ -530,7 +552,7 @@ def __iadd__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameT
530552 time_base = timecode .time_base ,
531553 )
532554 self ._seconds = None
533- self ._framerate = None
555+ self ._rate = None
534556 self ._frame_num = None
535557 return self
536558
@@ -573,7 +595,7 @@ def __isub__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameT
573595 time_base = timecode .time_base ,
574596 )
575597 self ._seconds = None
576- self ._framerate = None
598+ self ._rate = None
577599 self ._frame_num = None
578600 return self
579601
@@ -610,8 +632,8 @@ def __repr__(self) -> str:
610632 if self ._timecode :
611633 return f"{ self .get_timecode ()} [pts={ self ._timecode .pts } , time_base={ self ._timecode .time_base } ]"
612634 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 } ]"
635+ return f"{ self .get_timecode ()} [seconds={ self ._seconds } , fps={ self ._rate } ]"
636+ return f"{ self .get_timecode ()} [frame_num={ self ._frame_num } , fps={ self ._rate } ]"
615637
616638 def __hash__ (self ) -> int :
617639 if self ._timecode :
@@ -628,7 +650,7 @@ def _get_other_as_seconds(self, other: ty.Union[int, float, str, "FrameTimecode"
628650 if _USE_PTS_IN_DEVELOPMENT and other == 1 :
629651 return self .seconds
630652 raise NotImplementedError ()
631- return float (other ) / self ._framerate
653+ return float (other ) / self ._rate
632654 if isinstance (other , float ):
633655 return other
634656 if isinstance (other , str ):
@@ -639,4 +661,4 @@ def _get_other_as_seconds(self, other: ty.Union[int, float, str, "FrameTimecode"
639661
640662
641663def _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
664+ return a ._rate is not None and isinstance (b , FrameTimecode ) and b ._rate is not None
0 commit comments