@@ -147,6 +147,20 @@ def seconds(self) -> float:
147147 return float (self .time_base * self .pts )
148148
149149
150+ @dataclass (frozen = True )
151+ class _FrameNumber :
152+ """Represents a time as a frame number."""
153+
154+ value : int
155+
156+
157+ @dataclass (frozen = True )
158+ class _Seconds :
159+ """Represents a time in seconds."""
160+
161+ value : float
162+
163+
150164class FrameTimecode :
151165 """Object for frame-based timecodes, using the video framerate to compute back and
152166 forth between frame number and seconds/timecode.
@@ -172,24 +186,15 @@ def __init__(
172186 TypeError: Thrown if either `timecode` or `fps` are unsupported types.
173187 ValueError: Thrown when specifying a negative timecode or framerate.
174188 """
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.
189+ self ._time : ty .Union [_FrameNumber , _Seconds , Timecode ]
190+ """Internal time representation."""
178191 self ._rate : Fraction = None
179192 """Rate at which time passes between frames, measured in frames/sec."""
180- self ._frame_num = None
181- """Frame number which may be estimated."""
182- self ._timecode : ty .Optional [Timecode ] = None
183- """Presentation timestamp from the backend."""
184- self ._seconds : ty .Optional [float ] = None
185- """An explicit point in time."""
186193
187194 # Copy constructor.
188195 if isinstance (timecode , FrameTimecode ):
189196 self ._rate = timecode ._rate if fps is None else fps
190- self ._frame_num = timecode ._frame_num
191- self ._timecode = timecode ._timecode
192- self ._seconds = timecode ._seconds
197+ self ._time = timecode ._time
193198 return
194199
195200 if not isinstance (fps , (float , Fraction , FrameTimecode )):
@@ -215,31 +220,31 @@ def __init__(
215220
216221 # Timecode with a time base.
217222 if isinstance (timecode , Timecode ):
218- self ._timecode = timecode
223+ self ._time = timecode
219224 return
220225
221226 # Process the timecode value, storing it as an exact number of frames only if required.
222227 if isinstance (timecode , str ) and timecode .isdigit ():
223228 timecode = int (timecode )
224229
225230 if isinstance (timecode , str ):
226- self ._seconds = self ._timecode_to_seconds (timecode )
231+ self ._time = _Seconds ( self ._timecode_to_seconds (timecode ) )
227232 elif isinstance (timecode , float ):
228233 if timecode < 0.0 :
229234 raise ValueError ("Timecode frame number must be positive and greater than zero." )
230- self ._seconds = timecode
235+ self ._time = _Seconds ( timecode )
231236 elif isinstance (timecode , int ):
232237 if timecode < 0 :
233238 raise ValueError ("Timecode frame number must be positive and greater than zero." )
234- self ._frame_num = timecode
239+ self ._time = _FrameNumber ( timecode )
235240 else :
236241 raise TypeError ("Timecode format/type unrecognized." )
237242
238243 @property
239244 def frame_num (self ) -> ty .Optional [int ]:
240245 """The frame number. This value will be an estimate if the video is VFR. Prefer using the
241246 `pts` property."""
242- if self ._timecode :
247+ if isinstance ( self ._time , Timecode ) :
243248 # We need to audit anything currently using this property to guarantee temporal
244249 # consistency when handling VFR videos (i.e. no assumptions on fixed frame rate).
245250 warnings .warn (
@@ -249,11 +254,11 @@ def frame_num(self) -> ty.Optional[int]:
249254 )
250255 # We can calculate the approx. # of frames by taking the presentation time and the
251256 # time base itself.
252- (num , den ) = (self ._timecode .time_base * self ._timecode .pts ).as_integer_ratio ()
257+ (num , den ) = (self ._time .time_base * self ._time .pts ).as_integer_ratio ()
253258 return num / den
254- if self ._seconds is not None :
255- return self ._seconds_to_frames (self ._seconds )
256- return self ._frame_num
259+ if isinstance ( self ._time , _Seconds ) :
260+ return self ._seconds_to_frames (self ._time . value )
261+ return self ._time . value
257262
258263 @property
259264 def framerate (self ) -> float :
@@ -264,15 +269,15 @@ def framerate(self) -> float:
264269 @property
265270 def time_base (self ) -> Fraction :
266271 """The time base in which presentation time is calculated."""
267- if self ._timecode :
268- return self ._timecode .time_base
272+ if isinstance ( self ._time , Timecode ) :
273+ return self ._time .time_base
269274 return 1 / self ._rate
270275
271276 @property
272277 def pts (self ) -> int :
273278 """The presentation timestamp of the frame in units of `time_base`."""
274- if self ._timecode :
275- return self ._timecode .pts
279+ if isinstance ( self ._time , Timecode ) :
280+ return self ._time .pts
276281 return self .frame_num
277282
278283 def get_frames (self ) -> int :
@@ -321,11 +326,11 @@ def equal_framerate(self, fps) -> bool:
321326 @property
322327 def seconds (self ) -> float :
323328 """The frame's position in number of seconds."""
324- if self ._timecode :
325- return self ._timecode .seconds
326- if self ._seconds :
327- return self ._seconds
328- return float (self ._frame_num / self ._rate )
329+ if isinstance ( self ._time , Timecode ) :
330+ return self ._time .seconds
331+ if isinstance ( self ._time , _Seconds ) :
332+ return self ._time . value
333+ return float (self ._time . value / self ._rate )
329334
330335 def get_seconds (self ) -> float :
331336 """[DEPRECATED] Get the frame's position in number of seconds.
@@ -478,8 +483,8 @@ def _get_other_as_frames(self, other: ty.Union[int, float, str, "FrameTimecode"]
478483 raise ValueError (
479484 "FrameTimecode instances require equal framerate for frame-based arithmetic."
480485 )
481- if other ._frame_num is not None :
482- return other ._frame_num
486+ if isinstance ( other ._time , _FrameNumber ) :
487+ return other ._time . value
483488 # If other has no frame_num, it must have a timecode. Convert to frames.
484489 return self ._seconds_to_frames (other .seconds )
485490 raise TypeError ("Cannot obtain frame number for this timecode." )
@@ -489,7 +494,7 @@ def __eq__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> bool:
489494 return False
490495 if _compare_as_fixed (self , other ):
491496 return self .frame_num == other .frame_num
492- if self ._timecode or self . _seconds is not None :
497+ if isinstance ( self ._time , ( Timecode , _Seconds )) :
493498 return self .seconds == self ._get_other_as_seconds (other )
494499 return self .frame_num == self ._get_other_as_frames (other )
495500
@@ -498,74 +503,76 @@ def __ne__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> bool:
498503 return True
499504 if _compare_as_fixed (self , other ):
500505 return self .frame_num != other .frame_num
501- if self ._timecode or self . _seconds is not None :
506+ if isinstance ( self ._time , ( Timecode , _Seconds )) :
502507 return self .seconds != self ._get_other_as_seconds (other )
503508 return self .frame_num != self ._get_other_as_frames (other )
504509
505510 def __lt__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
506511 if _compare_as_fixed (self , other ):
507512 return self .frame_num < other .frame_num
508- if self ._timecode or self . _seconds is not None :
513+ if isinstance ( self ._time , ( Timecode , _Seconds )) :
509514 return self .seconds < self ._get_other_as_seconds (other )
510515 return self .frame_num < self ._get_other_as_frames (other )
511516
512517 def __le__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
513518 if _compare_as_fixed (self , other ):
514519 return self .frame_num <= other .frame_num
515- if self ._timecode or self . _seconds is not None :
520+ if isinstance ( self ._time , ( Timecode , _Seconds )) :
516521 return self .seconds <= self ._get_other_as_seconds (other )
517522 return self .frame_num <= self ._get_other_as_frames (other )
518523
519524 def __gt__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
520525 if _compare_as_fixed (self , other ):
521526 return self .frame_num > other .frame_num
522- if self ._timecode or self . _seconds is not None :
527+ if isinstance ( self ._time , ( Timecode , _Seconds )) :
523528 return self .seconds > self ._get_other_as_seconds (other )
524529 return self .frame_num > self ._get_other_as_frames (other )
525530
526531 def __ge__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
527532 if _compare_as_fixed (self , other ):
528533 return self .frame_num >= other .frame_num
529- if self ._timecode or self . _seconds is not None :
534+ if isinstance ( self ._time , ( Timecode , _Seconds )) :
530535 return self .seconds >= self ._get_other_as_seconds (other )
531536 return self .frame_num >= self ._get_other_as_frames (other )
532537
533538 def __iadd__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> "FrameTimecode" :
534- other_has_timecode = isinstance (other , FrameTimecode ) and other ._timecode
539+ other_is_timecode = isinstance (other , FrameTimecode ) and isinstance ( other ._time , Timecode )
535540
536- if self ._timecode and other_has_timecode :
537- if self ._timecode .time_base != other ._timecode .time_base :
541+ if isinstance ( self ._time , Timecode ) and other_is_timecode :
542+ if self ._time .time_base != other ._time .time_base :
538543 raise ValueError ("timecodes have different time bases" )
539- self ._timecode = Timecode (
540- pts = max (0 , self ._timecode .pts + other ._timecode .pts ),
541- time_base = self ._timecode .time_base ,
544+ self ._time = Timecode (
545+ pts = max (0 , self ._time .pts + other ._time .pts ),
546+ time_base = self ._time .time_base ,
542547 )
543548 return self
544549
545550 # If either input is a timecode, the output shall also be one. The input which isn't a
546551 # timecode is converted into seconds, after which the equivalent timecode is computed.
547- if self ._timecode or other_has_timecode :
548- timecode : Timecode = self ._timecode if self ._timecode else other ._timecode
549- seconds : float = self ._get_other_as_seconds (other ) if self ._timecode else self .seconds
550- self ._timecode = Timecode (
552+ if isinstance (self ._time , Timecode ) or other_is_timecode :
553+ timecode : Timecode = self ._time if isinstance (self ._time , Timecode ) else other ._time
554+ seconds : float = (
555+ self ._get_other_as_seconds (other )
556+ if isinstance (self ._time , Timecode )
557+ else self .seconds
558+ )
559+ self ._time = Timecode (
551560 pts = max (0 , timecode .pts + round (seconds / timecode .time_base )),
552561 time_base = timecode .time_base ,
553562 )
554- self ._seconds = None
555563 self ._rate = None
556- self ._frame_num = None
557564 return self
558565
559- other_has_seconds = isinstance (other , FrameTimecode ) and other ._seconds
560- if self ._seconds is not None and other_has_seconds :
561- self ._seconds = max (0 , self ._seconds + other ._seconds )
566+ other_is_seconds = isinstance (other , FrameTimecode ) and isinstance ( other ._time , _Seconds )
567+ if isinstance ( self ._time , _Seconds ) and other_is_seconds :
568+ self ._time = _Seconds ( max (0 , self ._time . value + other ._time . value ) )
562569 return self
563570
564- if self ._seconds is not None :
565- self ._seconds = max (0.0 , self ._seconds + self ._get_other_as_seconds (other ))
571+ if isinstance ( self ._time , _Seconds ) :
572+ self ._time = _Seconds ( max (0.0 , self ._time . value + self ._get_other_as_seconds (other ) ))
566573 return self
567574
568- self ._frame_num = max (0 , self ._frame_num + self ._get_other_as_frames (other ))
575+ self ._time = _FrameNumber ( max (0 , self ._time . value + self ._get_other_as_frames (other ) ))
569576 return self
570577
571578 def __add__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> "FrameTimecode" :
@@ -574,41 +581,43 @@ def __add__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameTi
574581 return to_return
575582
576583 def __isub__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> "FrameTimecode" :
577- other_has_timecode = isinstance (other , FrameTimecode ) and other ._timecode
584+ other_is_timecode = isinstance (other , FrameTimecode ) and isinstance ( other ._time , Timecode )
578585
579- if self ._timecode and other_has_timecode :
580- if self ._timecode .time_base != other ._timecode .time_base :
586+ if isinstance ( self ._time , Timecode ) and other_is_timecode :
587+ if self ._time .time_base != other ._time .time_base :
581588 raise ValueError ("timecodes have different time bases" )
582- self ._timecode = Timecode (
583- pts = max (0 , self ._timecode .pts - other ._timecode .pts ),
584- time_base = self ._timecode .time_base ,
589+ self ._time = Timecode (
590+ pts = max (0 , self ._time .pts - other ._time .pts ),
591+ time_base = self ._time .time_base ,
585592 )
586593 return self
587594
588595 # If either input is a timecode, the output shall also be one. The input which isn't a
589596 # timecode is converted into seconds, after which the equivalent timecode is computed.
590- if self ._timecode or other_has_timecode :
591- timecode : Timecode = self ._timecode if self ._timecode else other ._timecode
592- seconds : float = self ._get_other_as_seconds (other ) if self ._timecode else self .seconds
593- self ._timecode = Timecode (
597+ if isinstance (self ._time , Timecode ) or other_is_timecode :
598+ timecode : Timecode = self ._time if isinstance (self ._time , Timecode ) else other ._time
599+ seconds : float = (
600+ self ._get_other_as_seconds (other )
601+ if isinstance (self ._time , Timecode )
602+ else self .seconds
603+ )
604+ self ._time = Timecode (
594605 pts = max (0 , timecode .pts - round (seconds / timecode .time_base )),
595606 time_base = timecode .time_base ,
596607 )
597- self ._seconds = None
598608 self ._rate = None
599- self ._frame_num = None
600609 return self
601610
602- other_has_seconds = isinstance (other , FrameTimecode ) and other ._seconds
603- if self ._seconds is not None and other_has_seconds :
604- self ._seconds = max (0 , self ._seconds - other ._seconds )
611+ other_is_seconds = isinstance (other , FrameTimecode ) and isinstance ( other ._time , _Seconds )
612+ if isinstance ( self ._time , _Seconds ) and other_is_seconds :
613+ self ._time = _Seconds ( max (0 , self ._time . value - other ._time . value ) )
605614 return self
606615
607- if self ._seconds is not None :
608- self ._seconds = max (0.0 , self ._seconds - self ._get_other_as_seconds (other ))
616+ if isinstance ( self ._time , _Seconds ) :
617+ self ._time = _Seconds ( max (0.0 , self ._time . value - self ._get_other_as_seconds (other ) ))
609618 return self
610619
611- self ._frame_num = max (0 , self ._frame_num - self ._get_other_as_frames (other ))
620+ self ._time = _FrameNumber ( max (0 , self ._time . value - self ._get_other_as_frames (other ) ))
612621 return self
613622
614623 def __sub__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> "FrameTimecode" :
@@ -620,7 +629,9 @@ def __sub__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameTi
620629 # need to use relevant property instead.
621630
622631 def __int__ (self ) -> int :
623- return self ._frame_num
632+ if isinstance (self ._time , _FrameNumber ):
633+ return self ._time .value
634+ return self .frame_num
624635
625636 def __float__ (self ) -> float :
626637 return self .seconds
@@ -629,21 +640,21 @@ def __str__(self) -> str:
629640 return self .get_timecode ()
630641
631642 def __repr__ (self ) -> str :
632- if self ._timecode :
633- return f"{ self .get_timecode ()} [pts={ self ._timecode .pts } , time_base={ self ._timecode .time_base } ]"
634- if self ._seconds is not None :
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 } ]"
643+ if isinstance ( self ._time , Timecode ) :
644+ return f"{ self .get_timecode ()} [pts={ self ._time .pts } , time_base={ self ._time .time_base } ]"
645+ if isinstance ( self ._time , _Seconds ) :
646+ return f"{ self .get_timecode ()} [seconds={ self ._time . value } , fps={ self ._rate } ]"
647+ return f"{ self .get_timecode ()} [frame_num={ self ._time . value } , fps={ self ._rate } ]"
637648
638649 def __hash__ (self ) -> int :
639- if self ._timecode :
640- return hash (self ._timecode )
641- return self ._frame_num
650+ if isinstance ( self ._time , Timecode ) :
651+ return hash (self ._time )
652+ return self .frame_num
642653
643654 def _get_other_as_seconds (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> float :
644655 """Get the time in seconds from `other` for arithmetic operations."""
645656 if isinstance (other , int ):
646- if self ._timecode :
657+ if isinstance ( self ._time , Timecode ) :
647658 # TODO(https://scenedetect.com/issue/168): We need to convert every place that uses
648659 # frame numbers with timestamps to convert to a non-frame based way of temporal
649660 # logic and instead use seconds-based.
0 commit comments