@@ -207,17 +207,27 @@ def __init__(
207207 ):
208208 raise ValueError ("Framerate must be positive and greater than zero." )
209209 self ._framerate = float (fps )
210- # Process the timecode value, storing it as an exact number of frames.
210+ # Process the timecode value, storing it as an exact number of frames only if required.
211+ if isinstance (timecode , str ) and timecode .isdigit ():
212+ timecode = int (timecode )
211213 if isinstance (timecode , str ):
212214 self ._seconds = self ._timecode_to_seconds (timecode )
213- self ._frame_num = self ._seconds_to_frames (self ._seconds )
215+ elif isinstance (timecode , float ):
216+ if timecode < 0.0 :
217+ raise ValueError ("Timecode frame number must be positive and greater than zero." )
218+ self ._seconds = timecode
219+ elif isinstance (timecode , int ):
220+ if timecode < 0 :
221+ raise ValueError ("Timecode frame number must be positive and greater than zero." )
222+ self ._frame_num = timecode
214223 else :
215- self ._frame_num = self ._parse_timecode_number (timecode )
216- self ._seconds = timecode if isinstance (timecode , float ) else None
224+ raise TypeError ("Timecode format/type unrecognized." )
217225
218226 @property
219227 def frame_num (self ) -> ty .Optional [int ]:
220228 if self ._timecode :
229+ # We need to audit anything currently using this property to guarantee temporal
230+ # consistency when handling VFR videos (i.e. no assumptions on fixed frame rate).
221231 warnings .warn (
222232 message = "TODO(https://scenedetect.com/issue/168): Update caller to handle VFR." ,
223233 stacklevel = 2 ,
@@ -227,6 +237,8 @@ def frame_num(self) -> ty.Optional[int]:
227237 # time base itself.
228238 (num , den ) = (self ._timecode .time_base * self ._timecode .pts ).as_integer_ratio ()
229239 return num / den
240+ if self ._seconds is not None :
241+ return self ._seconds_to_frames (self ._seconds )
230242 return self ._frame_num
231243
232244 @property
@@ -306,19 +318,26 @@ def get_seconds(self) -> float:
306318 )
307319 return self .seconds
308320
309- def get_timecode (self , precision : int = 3 , use_rounding : bool = True ) -> str :
321+ def get_timecode (
322+ self , precision : int = 3 , use_rounding : bool = True , nearest_frame : bool = True
323+ ) -> str :
310324 """Get a formatted timecode string of the form HH:MM:SS[.nnn].
311325
312326 Args:
313327 precision: The number of decimal places to include in the output ``[.nnn]``.
314328 use_rounding: Rounds the output to the desired precision. If False, the value
315329 will be truncated to the specified precision.
330+ nearest_frame: Ensures that the timecode is moved to the nearest frame boundary if this
331+ object has a defined framerate, otherwise has no effect.
316332
317333 Returns:
318334 str: The current time in the form ``"HH:MM:SS[.nnn]"``.
319335 """
320336 # Compute hours and minutes based off of seconds, and update seconds.
321- secs = self .seconds
337+ if nearest_frame and self .framerate :
338+ secs = self .frame_num / self .framerate
339+ else :
340+ secs = self .seconds
322341 hrs = int (secs / _SECONDS_PER_HOUR )
323342 secs -= hrs * _SECONDS_PER_HOUR
324343 mins = int (secs / _SECONDS_PER_MINUTE )
@@ -440,7 +459,7 @@ def __eq__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> bool:
440459 if other is None :
441460 return False
442461 if _compare_as_fixed (self , other ):
443- return self ._frame_num == other ._frame_num
462+ return self .frame_num == other .frame_num
444463 if self ._timecode or self ._seconds is not None :
445464 return self .seconds == self ._get_other_as_seconds (other )
446465 return self .frame_num == self ._get_other_as_frames (other )
@@ -449,35 +468,35 @@ def __ne__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> bool:
449468 if other is None :
450469 return True
451470 if _compare_as_fixed (self , other ):
452- return self ._frame_num != other ._frame_num
471+ return self .frame_num != other .frame_num
453472 if self ._timecode or self ._seconds is not None :
454473 return self .seconds != self ._get_other_as_seconds (other )
455474 return self .frame_num != self ._get_other_as_frames (other )
456475
457476 def __lt__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
458477 if _compare_as_fixed (self , other ):
459- return self ._frame_num < other ._frame_num
478+ return self .frame_num < other .frame_num
460479 if self ._timecode or self ._seconds is not None :
461480 return self .seconds < self ._get_other_as_seconds (other )
462481 return self .frame_num < self ._get_other_as_frames (other )
463482
464483 def __le__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
465484 if _compare_as_fixed (self , other ):
466- return self ._frame_num <= other ._frame_num
485+ return self .frame_num <= other .frame_num
467486 if self ._timecode or self ._seconds is not None :
468487 return self .seconds <= self ._get_other_as_seconds (other )
469488 return self .frame_num <= self ._get_other_as_frames (other )
470489
471490 def __gt__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
472491 if _compare_as_fixed (self , other ):
473- return self ._frame_num > other ._frame_num
492+ return self .frame_num > other .frame_num
474493 if self ._timecode or self ._seconds is not None :
475494 return self .seconds > self ._get_other_as_seconds (other )
476495 return self .frame_num > self ._get_other_as_frames (other )
477496
478497 def __ge__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
479498 if _compare_as_fixed (self , other ):
480- return self ._frame_num >= other ._frame_num
499+ return self .frame_num >= other .frame_num
481500 if self ._timecode or self ._seconds is not None :
482501 return self .seconds >= self ._get_other_as_seconds (other )
483502 return self .frame_num >= self ._get_other_as_frames (other )
@@ -513,9 +532,11 @@ def __iadd__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameT
513532 self ._seconds = max (0 , self ._seconds + other ._seconds )
514533 return self
515534
516- self ._frame_num = max (0 , self ._frame_num + self ._get_other_as_frames (other ))
517535 if self ._seconds is not None :
518536 self ._seconds = max (0.0 , self ._seconds + self ._get_other_as_seconds (other ))
537+ return self
538+
539+ self ._frame_num = max (0 , self ._frame_num + self ._get_other_as_frames (other ))
519540 return self
520541
521542 def __add__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> "FrameTimecode" :
@@ -554,9 +575,11 @@ def __isub__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameT
554575 self ._seconds = max (0 , self ._seconds - other ._seconds )
555576 return self
556577
557- self ._frame_num = max (0 , self ._frame_num - self ._get_other_as_frames (other ))
558578 if self ._seconds is not None :
559579 self ._seconds = max (0.0 , self ._seconds - self ._get_other_as_seconds (other ))
580+ return self
581+
582+ self ._frame_num = max (0 , self ._frame_num - self ._get_other_as_frames (other ))
560583 return self
561584
562585 def __sub__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> "FrameTimecode" :
@@ -579,7 +602,9 @@ def __str__(self) -> str:
579602 def __repr__ (self ) -> str :
580603 if self ._timecode :
581604 return f"{ self .get_timecode ()} [pts={ self ._timecode .pts } , time_base={ self ._timecode .time_base } ]"
582- return "%s [frame=%d, fps=%.3f]" % (self .get_timecode (), self ._frame_num , self ._framerate )
605+ if self ._seconds is not None :
606+ return f"{ self .get_timecode ()} [seconds={ self ._seconds } , fps={ self ._framerate } ]"
607+ return f"{ self .get_timecode ()} [frame_num={ self ._frame_num } , fps={ self ._framerate } ]"
583608
584609 def __hash__ (self ) -> int :
585610 if self ._timecode :
0 commit comments