@@ -144,6 +144,96 @@ class Timecode:
144144 def seconds (self ) -> float :
145145 return float (self .time_base * self .pts )
146146
147+ def _get_other_as_seconds (self , other : ty .Any ) -> float :
148+ if isinstance (other , Timecode ):
149+ return other .seconds
150+ if isinstance (other , float ):
151+ return float (other )
152+ raise TypeError (f"Unsupported type for comparison with Timecode: { type (other )} " )
153+
154+ def __add__ (self , other : ty .Union [float , "Timecode" ]) -> "Timecode" :
155+ if isinstance (other , Timecode ):
156+ if self .time_base != other .time_base :
157+ raise ValueError ("Timecode instances require equal time_base for arithmetic." )
158+ return Timecode (self .pts + other .pts , self .time_base )
159+ if isinstance (other , float ):
160+ # Assume other is in seconds. Convert to pts.
161+ pts_offset = round (other / self .time_base )
162+ return Timecode (self .pts + pts_offset , self .time_base )
163+ return NotImplemented
164+
165+ def __sub__ (self , other : ty .Union [float , "Timecode" ]) -> "Timecode" :
166+ if isinstance (other , Timecode ):
167+ if self .time_base != other .time_base :
168+ raise ValueError ("Timecode instances require equal time_base for arithmetic." )
169+ return Timecode (self .pts - other .pts , self .time_base )
170+ if isinstance (other , float ):
171+ # Assume other is in seconds. Convert to pts.
172+ pts_offset = round (other / self .time_base )
173+ return Timecode (self .pts - pts_offset , self .time_base )
174+ return NotImplemented
175+
176+ def __eq__ (self , other : ty .Any ) -> bool :
177+ if isinstance (other , Timecode ):
178+ if self .time_base == other .time_base :
179+ return self .pts == other .pts
180+ try :
181+ return math .isclose (self .seconds , self ._get_other_as_seconds (other ))
182+ except TypeError :
183+ return NotImplemented
184+
185+ def __ne__ (self , other : ty .Any ) -> bool :
186+ eq_result = self .__eq__ (other )
187+ return not eq_result if eq_result is not NotImplemented else NotImplemented
188+
189+ def __lt__ (self , other : ty .Any ) -> bool :
190+ if isinstance (other , Timecode ):
191+ if self .time_base == other .time_base :
192+ return self .pts < other .pts
193+ try :
194+ other_seconds = self ._get_other_as_seconds (other )
195+ if math .isclose (self .seconds , other_seconds ):
196+ return False
197+ return self .seconds < other_seconds
198+ except TypeError :
199+ return NotImplemented
200+
201+ def __le__ (self , other : ty .Any ) -> bool :
202+ if isinstance (other , Timecode ):
203+ if self .time_base == other .time_base :
204+ return self .pts <= other .pts
205+ try :
206+ other_seconds = self ._get_other_as_seconds (other )
207+ if math .isclose (self .seconds , other_seconds ):
208+ return True
209+ return self .seconds < other_seconds
210+ except TypeError :
211+ return NotImplemented
212+
213+ def __gt__ (self , other : ty .Any ) -> bool :
214+ if isinstance (other , Timecode ):
215+ if self .time_base == other .time_base :
216+ return self .pts > other .pts
217+ try :
218+ other_seconds = self ._get_other_as_seconds (other )
219+ if math .isclose (self .seconds , other_seconds ):
220+ return False
221+ return self .seconds > other_seconds
222+ except TypeError :
223+ return NotImplemented
224+
225+ def __ge__ (self , other : ty .Any ) -> bool :
226+ if isinstance (other , Timecode ):
227+ if self .time_base == other .time_base :
228+ return self .pts >= other .pts
229+ try :
230+ other_seconds = self ._get_other_as_seconds (other )
231+ if math .isclose (self .seconds , other_seconds ):
232+ return True
233+ return self .seconds > other_seconds
234+ except TypeError :
235+ return NotImplemented
236+
147237
148238class FrameTimecode :
149239 """Object for frame-based timecodes, using the video framerate to compute back and
@@ -211,9 +301,12 @@ def __init__(
211301 else :
212302 self ._frame_num = self ._parse_timecode_number (timecode )
213303
214- # TODO(v0.7): Add a PTS property as well and slowly transition over to that, since we don't
215- # always know the position as a "frame number". However, for the reverse case, we CAN state
216- # the presentation time if we know the frame number (for a fixed framerate video).
304+ @property
305+ def timecode (self ) -> ty .Optional [Timecode ]:
306+ return self ._timecode
307+
308+ # TODO(0.7): Can we make this return a Timecode if this is a VFR video? This value is used in
309+ # a *lot* of places, so a large increase will be signficiant for things like buffer sizes.
217310 @property
218311 def frame_num (self ) -> ty .Optional [int ]:
219312 return self ._frame_num
@@ -427,47 +520,69 @@ def __eq__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> bool:
427520 if other is None :
428521 return False
429522 if self ._timecode :
430- return self .seconds == self ._get_other_as_seconds (other )
523+ if isinstance (other , FrameTimecode ) and other ._timecode :
524+ return self ._timecode == other ._timecode
525+ return math .isclose (self .seconds , self ._get_other_as_seconds (other ))
431526 return self .frame_num == self ._get_other_as_frames (other )
432527
433528 def __ne__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
434529 if other is None :
435530 return True
436- if self ._timecode :
437- return self .seconds != self ._get_other_as_seconds (other )
438- return self .frame_num != self ._get_other_as_frames (other )
531+ return not self .__eq__ (other )
439532
440533 def __lt__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
441534 if self ._timecode :
442- return self .seconds < self ._get_other_as_seconds (other )
535+ if isinstance (other , FrameTimecode ) and other ._timecode :
536+ return self ._timecode < other ._timecode
537+ other_seconds = self ._get_other_as_seconds (other )
538+ if math .isclose (self .seconds , other_seconds ):
539+ return False
540+ return self .seconds < other_seconds
443541 return self .frame_num < self ._get_other_as_frames (other )
444542
445543 def __le__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
446544 if self ._timecode :
447- return self .seconds <= self ._get_other_as_seconds (other )
545+ if isinstance (other , FrameTimecode ) and other ._timecode :
546+ return self ._timecode <= other ._timecode
547+ other_seconds = self ._get_other_as_seconds (other )
548+ if math .isclose (self .seconds , other_seconds ):
549+ return True
550+ return self .seconds < other_seconds
448551 return self .frame_num <= self ._get_other_as_frames (other )
449552
450553 def __gt__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
451554 if self ._timecode :
452- return self .seconds > self ._get_other_as_seconds (other )
555+ if isinstance (other , FrameTimecode ) and other ._timecode :
556+ return self ._timecode > other ._timecode
557+ other_seconds = self ._get_other_as_seconds (other )
558+ if math .isclose (self .seconds , other_seconds ):
559+ return False
560+ return self .seconds > other_seconds
453561 return self .frame_num > self ._get_other_as_frames (other )
454562
455563 def __ge__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
456564 if self ._timecode :
457- return self .seconds >= self ._get_other_as_seconds (other )
565+ if isinstance (other , FrameTimecode ) and other ._timecode :
566+ return self ._timecode >= other ._timecode
567+ other_seconds = self ._get_other_as_seconds (other )
568+ if math .isclose (self .seconds , other_seconds ):
569+ return True
570+ return self .seconds > other_seconds
458571 return self .frame_num >= self ._get_other_as_frames (other )
459572
460573 def __iadd__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> "FrameTimecode" :
461574 if self ._timecode :
462- new_seconds = self .seconds + self ._get_other_as_seconds (other )
463- # TODO: This is incorrect for VFR, need a better way to handle this.
464- # For now, we convert back to a frame number.
465- self ._frame_num = self ._seconds_to_frames (new_seconds )
466- self ._timecode = None
575+ if isinstance (other , FrameTimecode ) and other ._timecode :
576+ self ._timecode = self ._timecode + other ._timecode
577+ else :
578+ other_as_seconds = self ._get_other_as_seconds (other )
579+ self ._timecode = self ._timecode + other_as_seconds
580+ if self ._timecode .pts < 0 :
581+ self ._timecode = Timecode (0 , self ._timecode .time_base )
467582 else :
468583 self ._frame_num += self ._get_other_as_frames (other )
469- if self ._frame_num < 0 : # Required to allow adding negative seconds/frames.
470- self ._frame_num = 0
584+ if self ._frame_num < 0 : # Required to allow adding negative seconds/frames.
585+ self ._frame_num = 0
471586 return self
472587
473588 def __add__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> "FrameTimecode" :
@@ -477,15 +592,17 @@ def __add__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameTi
477592
478593 def __isub__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> "FrameTimecode" :
479594 if self ._timecode :
480- new_seconds = self .seconds - self ._get_other_as_seconds (other )
481- # TODO: This is incorrect for VFR, need a better way to handle this.
482- # For now, we convert back to a frame number.
483- self ._frame_num = self ._seconds_to_frames (new_seconds )
484- self ._timecode = None
595+ if isinstance (other , FrameTimecode ) and other ._timecode :
596+ self ._timecode = self ._timecode - other ._timecode
597+ else :
598+ other_as_seconds = self ._get_other_as_seconds (other )
599+ self ._timecode = self ._timecode - other_as_seconds
600+ if self ._timecode .pts < 0 :
601+ self ._timecode = Timecode (0 , self ._timecode .time_base )
485602 else :
486603 self ._frame_num -= self ._get_other_as_frames (other )
487- if self ._frame_num < 0 :
488- self ._frame_num = 0
604+ if self ._frame_num < 0 :
605+ self ._frame_num = 0
489606 return self
490607
491608 def __sub__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> "FrameTimecode" :
0 commit comments