@@ -147,6 +147,20 @@ def seconds(self) -> float:
147
147
return float (self .time_base * self .pts )
148
148
149
149
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
+
150
164
class FrameTimecode :
151
165
"""Object for frame-based timecodes, using the video framerate to compute back and
152
166
forth between frame number and seconds/timecode.
@@ -172,24 +186,15 @@ def __init__(
172
186
TypeError: Thrown if either `timecode` or `fps` are unsupported types.
173
187
ValueError: Thrown when specifying a negative timecode or framerate.
174
188
"""
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."""
178
191
self ._rate : Fraction = None
179
192
"""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."""
186
193
187
194
# Copy constructor.
188
195
if isinstance (timecode , FrameTimecode ):
189
196
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
193
198
return
194
199
195
200
if not isinstance (fps , (float , Fraction , FrameTimecode )):
@@ -215,31 +220,31 @@ def __init__(
215
220
216
221
# Timecode with a time base.
217
222
if isinstance (timecode , Timecode ):
218
- self ._timecode = timecode
223
+ self ._time = timecode
219
224
return
220
225
221
226
# Process the timecode value, storing it as an exact number of frames only if required.
222
227
if isinstance (timecode , str ) and timecode .isdigit ():
223
228
timecode = int (timecode )
224
229
225
230
if isinstance (timecode , str ):
226
- self ._seconds = self ._timecode_to_seconds (timecode )
231
+ self ._time = _Seconds ( self ._timecode_to_seconds (timecode ) )
227
232
elif isinstance (timecode , float ):
228
233
if timecode < 0.0 :
229
234
raise ValueError ("Timecode frame number must be positive and greater than zero." )
230
- self ._seconds = timecode
235
+ self ._time = _Seconds ( timecode )
231
236
elif isinstance (timecode , int ):
232
237
if timecode < 0 :
233
238
raise ValueError ("Timecode frame number must be positive and greater than zero." )
234
- self ._frame_num = timecode
239
+ self ._time = _FrameNumber ( timecode )
235
240
else :
236
241
raise TypeError ("Timecode format/type unrecognized." )
237
242
238
243
@property
239
244
def frame_num (self ) -> ty .Optional [int ]:
240
245
"""The frame number. This value will be an estimate if the video is VFR. Prefer using the
241
246
`pts` property."""
242
- if self ._timecode :
247
+ if isinstance ( self ._time , Timecode ) :
243
248
# We need to audit anything currently using this property to guarantee temporal
244
249
# consistency when handling VFR videos (i.e. no assumptions on fixed frame rate).
245
250
warnings .warn (
@@ -249,11 +254,11 @@ def frame_num(self) -> ty.Optional[int]:
249
254
)
250
255
# We can calculate the approx. # of frames by taking the presentation time and the
251
256
# 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 ()
253
258
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
257
262
258
263
@property
259
264
def framerate (self ) -> float :
@@ -264,15 +269,15 @@ def framerate(self) -> float:
264
269
@property
265
270
def time_base (self ) -> Fraction :
266
271
"""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
269
274
return 1 / self ._rate
270
275
271
276
@property
272
277
def pts (self ) -> int :
273
278
"""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
276
281
return self .frame_num
277
282
278
283
def get_frames (self ) -> int :
@@ -321,11 +326,11 @@ def equal_framerate(self, fps) -> bool:
321
326
@property
322
327
def seconds (self ) -> float :
323
328
"""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 )
329
334
330
335
def get_seconds (self ) -> float :
331
336
"""[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"]
478
483
raise ValueError (
479
484
"FrameTimecode instances require equal framerate for frame-based arithmetic."
480
485
)
481
- if other ._frame_num is not None :
482
- return other ._frame_num
486
+ if isinstance ( other ._time , _FrameNumber ) :
487
+ return other ._time . value
483
488
# If other has no frame_num, it must have a timecode. Convert to frames.
484
489
return self ._seconds_to_frames (other .seconds )
485
490
raise TypeError ("Cannot obtain frame number for this timecode." )
@@ -489,7 +494,7 @@ def __eq__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> bool:
489
494
return False
490
495
if _compare_as_fixed (self , other ):
491
496
return self .frame_num == other .frame_num
492
- if self ._timecode or self . _seconds is not None :
497
+ if isinstance ( self ._time , ( Timecode , _Seconds )) :
493
498
return self .seconds == self ._get_other_as_seconds (other )
494
499
return self .frame_num == self ._get_other_as_frames (other )
495
500
@@ -498,74 +503,76 @@ def __ne__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> bool:
498
503
return True
499
504
if _compare_as_fixed (self , other ):
500
505
return self .frame_num != other .frame_num
501
- if self ._timecode or self . _seconds is not None :
506
+ if isinstance ( self ._time , ( Timecode , _Seconds )) :
502
507
return self .seconds != self ._get_other_as_seconds (other )
503
508
return self .frame_num != self ._get_other_as_frames (other )
504
509
505
510
def __lt__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
506
511
if _compare_as_fixed (self , other ):
507
512
return self .frame_num < other .frame_num
508
- if self ._timecode or self . _seconds is not None :
513
+ if isinstance ( self ._time , ( Timecode , _Seconds )) :
509
514
return self .seconds < self ._get_other_as_seconds (other )
510
515
return self .frame_num < self ._get_other_as_frames (other )
511
516
512
517
def __le__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
513
518
if _compare_as_fixed (self , other ):
514
519
return self .frame_num <= other .frame_num
515
- if self ._timecode or self . _seconds is not None :
520
+ if isinstance ( self ._time , ( Timecode , _Seconds )) :
516
521
return self .seconds <= self ._get_other_as_seconds (other )
517
522
return self .frame_num <= self ._get_other_as_frames (other )
518
523
519
524
def __gt__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
520
525
if _compare_as_fixed (self , other ):
521
526
return self .frame_num > other .frame_num
522
- if self ._timecode or self . _seconds is not None :
527
+ if isinstance ( self ._time , ( Timecode , _Seconds )) :
523
528
return self .seconds > self ._get_other_as_seconds (other )
524
529
return self .frame_num > self ._get_other_as_frames (other )
525
530
526
531
def __ge__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
527
532
if _compare_as_fixed (self , other ):
528
533
return self .frame_num >= other .frame_num
529
- if self ._timecode or self . _seconds is not None :
534
+ if isinstance ( self ._time , ( Timecode , _Seconds )) :
530
535
return self .seconds >= self ._get_other_as_seconds (other )
531
536
return self .frame_num >= self ._get_other_as_frames (other )
532
537
533
538
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 )
535
540
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 :
538
543
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 ,
542
547
)
543
548
return self
544
549
545
550
# If either input is a timecode, the output shall also be one. The input which isn't a
546
551
# 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 (
551
560
pts = max (0 , timecode .pts + round (seconds / timecode .time_base )),
552
561
time_base = timecode .time_base ,
553
562
)
554
- self ._seconds = None
555
563
self ._rate = None
556
- self ._frame_num = None
557
564
return self
558
565
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 ) )
562
569
return self
563
570
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 ) ))
566
573
return self
567
574
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 ) ))
569
576
return self
570
577
571
578
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
574
581
return to_return
575
582
576
583
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 )
578
585
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 :
581
588
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 ,
585
592
)
586
593
return self
587
594
588
595
# If either input is a timecode, the output shall also be one. The input which isn't a
589
596
# 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 (
594
605
pts = max (0 , timecode .pts - round (seconds / timecode .time_base )),
595
606
time_base = timecode .time_base ,
596
607
)
597
- self ._seconds = None
598
608
self ._rate = None
599
- self ._frame_num = None
600
609
return self
601
610
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 ) )
605
614
return self
606
615
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 ) ))
609
618
return self
610
619
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 ) ))
612
621
return self
613
622
614
623
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
620
629
# need to use relevant property instead.
621
630
622
631
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
624
635
625
636
def __float__ (self ) -> float :
626
637
return self .seconds
@@ -629,21 +640,21 @@ def __str__(self) -> str:
629
640
return self .get_timecode ()
630
641
631
642
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 } ]"
637
648
638
649
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
642
653
643
654
def _get_other_as_seconds (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> float :
644
655
"""Get the time in seconds from `other` for arithmetic operations."""
645
656
if isinstance (other , int ):
646
- if self ._timecode :
657
+ if isinstance ( self ._time , Timecode ) :
647
658
# TODO(https://scenedetect.com/issue/168): We need to convert every place that uses
648
659
# frame numbers with timestamps to convert to a non-frame based way of temporal
649
660
# logic and instead use seconds-based.
0 commit comments