Skip to content

Commit dc1e12e

Browse files
committed
[common] Add operators for Timecode
1 parent ad9d35d commit dc1e12e

File tree

5 files changed

+292
-33
lines changed

5 files changed

+292
-33
lines changed

scenedetect/_cli/commands.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,11 @@ def list_scenes(
158158
" | %5d | %11d | %s | %11d | %s |"
159159
% (
160160
i + 1,
161-
start_time.frame_num + 1,
161+
start_time.timecode.pts
162+
if start_time.timecode
163+
else start_time.frame_num + 1,
162164
start_time.get_timecode(),
163-
end_time.frame_num,
165+
end_time.timecode.pts if end_time.timecode else end_time.frame_num,
164166
end_time.get_timecode(),
165167
)
166168
for i, (start_time, end_time) in enumerate(scenes)

scenedetect/common.py

Lines changed: 142 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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

148238
class 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":

scenedetect/output/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,13 @@ def write_scene_list(
9898
csv_writer.writerow(
9999
[
100100
"%d" % (i + 1),
101-
"%d" % (start.frame_num + 1),
101+
"%d" % (start.timecode.pts if start.timecode else start.frame_num + 1),
102102
start.get_timecode(),
103103
"%.3f" % start.seconds,
104-
"%d" % end.frame_num,
104+
"%d" % (end.timecode.pts if end.timecode else end.frame_num + 1),
105105
end.get_timecode(),
106106
"%.3f" % end.seconds,
107-
"%d" % duration.frame_num,
107+
"%d" % (duration.timecode.pts if duration.timecode else duration.frame_num + 1),
108108
duration.get_timecode(),
109109
"%.3f" % duration.seconds,
110110
]

scenedetect/scene_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ def _process_frame(
398398
cuts = detector.process_frame(position, frame_im)
399399
self._cutting_list += cuts
400400
new_cuts = True if cuts else False
401-
# TODO: Support callbacks with PTS.
401+
# TODO(0.7): Support callbacks with PTS.
402402
if callback:
403403
if _USE_PTS_IN_DEVELOPMENT:
404404
raise NotImplementedError()

0 commit comments

Comments
 (0)