Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions tests/test_timecode.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,81 @@ def test_toggle_fractional_frame_3():
assert tc.__repr__() == "19:23:14:23"


def test_timestamp_realtime_1():
frames = 12345
ts = frames*1/24
assert Timecode(24, frames=frames).to_realtime(True) == ts


def test_timestamp_realtime_2():
tc = Timecode(50, start_seconds=1/50)
assert tc.to_realtime() == '00:00:00.020'


def test_timestamp_realtime_3():
#SMPTE 12-1 §5.2.2:
#- "When DF compensation is applied to NTSC TC, the deviation after one hour is approximately –3.6 ms"
tc = Timecode(29.97, '00:59:59;29')
assert tc.to_realtime() == str(Timecode(1000, '01:00:00.000') - int(round(3.6)))

#- "[...] The deviation accumulated over a 24-hour period is approximately –2.6 frames (–86 ms)"
tc = Timecode(59.94, '23:59:59;59')
assert tc.to_realtime() == str(Timecode(1000, '24:00:00.000') - 86)


def test_timestamp_realtime_4():
#SMPTE 12-1 §5.2.2
#- "Monotonically counting at int_framerate will yield a deviation of approx. +3.6 s in one hour of elapsed time."
tc = Timecode(59.94, '00:59:59:59', force_non_drop_frame=True)
assert tc.to_realtime() == str(Timecode(1000, '01:00:00.000') + 3600)


def test_timestamp_systemtime_1():
"""
TC with integer framerate always have system time equal to elapsed time.
"""
tc50 = Timecode(50, '00:59:59:49')
tc24 = Timecode(24, '00:59:59:23')
tcms = Timecode(1000, '01:00:00.000')
assert tc50.to_systemtime() == '01:00:00.000'
assert tc24.to_systemtime() == '01:00:00.000'
assert tcms.to_systemtime() == '01:00:00.000'


def test_timestamp_systemtime_2():
"""
TC with NTSC framerate always have system time different to realtime.
"""
tc = Timecode(23.98, '00:59:59:23')
assert tc.to_systemtime() == '01:00:00.000'
assert tc.to_systemtime() != tc.to_realtime()


def test_timestamp_systemtime_3():
"""
TC with DF NTSC framerate have system time roughly equal to real time.
with a -3.6 ms drift per hour (SMPTE 12-1 §5.2.2).
"""
tc = Timecode(29.97, '23:59:59;29')
assert tc.to_systemtime() == '24:00:00.000'
#Check if we have the expected drift at 24h
assert abs(tc.to_systemtime(True) - tc.to_realtime(True) - 24*3600e-6) < 1e-6


def test_add_const_dropframe_flag():
tc1 = Timecode(29.97, "00:00:00:00", force_non_drop_frame=True)
assert (tc1 + 1).drop_frame is False


def test_add_tc_dropframe_flag():
tc1 = Timecode(29.97, "00:00:00:00", force_non_drop_frame=True)
tc2 = Timecode(29.97, "00:00:00;00")

# Left operand drop_frame flag is preserved
assert (tc1 + tc2).drop_frame is False
assert (tc2 + tc1).drop_frame is True


def test_ge_overload():
tc1 = Timecode(24, "00:00:00:00")
tc2 = Timecode(24, "00:00:00:00")
Expand Down
86 changes: 68 additions & 18 deletions timecode/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ def framerate(self, framerate):
Fraction: If the current version of Python supports (which it should)
then Fraction is also accepted.
"""
# Convert rational frame rate to float
numerator = None
denominator = None
# Convert rational frame rate to float, defaults to None if not Fraction-like
numerator = getattr(framerate, 'numerator', None)
denominator = getattr(framerate, 'denominator', None)

try:
if "/" in framerate:
Expand All @@ -167,15 +167,6 @@ def framerate(self, framerate):
if isinstance(framerate, tuple):
numerator, denominator = framerate

try:
from fractions import Fraction

if isinstance(framerate, Fraction):
numerator = framerate.numerator
denominator = framerate.denominator
except ImportError:
pass

if numerator and denominator:
framerate = round(float(numerator) / float(denominator), 2)
if framerate.is_integer():
Expand All @@ -185,15 +176,20 @@ def framerate(self, framerate):
if isinstance(framerate, (int, float)):
framerate = str(framerate)

self._ntsc_framerate = False

# set the int_frame_rate
if framerate == "29.97":
self._int_framerate = 30
self.drop_frame = not self.force_non_drop_frame
self._ntsc_framerate = True
elif framerate == "59.94":
self._int_framerate = 60
self.drop_frame = not self.force_non_drop_frame
self._ntsc_framerate = True
elif any(map(lambda x: framerate.startswith(x), ["23.976", "23.98"])):
self._int_framerate = 24
self._ntsc_framerate = True
elif framerate in ["ms", "1000"]:
self._int_framerate = 1000
self.ms_frame = True
Expand Down Expand Up @@ -300,7 +296,7 @@ def tc_to_frames(self, timecode):

return frame_number + 1 # frames

def frames_to_tc(self, frames):
def frames_to_tc(self, frames, skip_rollover = False):
"""Convert frames back to timecode.

Args:
Expand Down Expand Up @@ -332,7 +328,8 @@ def frames_to_tc(self, frames):

# If frame_number is greater than 24 hrs, next operation will rollover
# clock
frame_number %= frames_per_24_hours
if not skip_rollover:
frame_number %= frames_per_24_hours

if self.drop_frame:
d = frame_number // frames_per_10_minutes
Expand Down Expand Up @@ -379,6 +376,56 @@ def tc_to_string(self, hrs, mins, secs, frs):
hrs, mins, secs, self.frame_delimiter, frs
)

def to_systemtime(self, as_float=False):
"""Convert a Timecode to the video system timestamp.
For NTSC rates, the video system time is not the wall-clock one.

Args:
as_float (bool): Return the time as a float number of seconds.

Returns:
str: The "system time" timestamp of the Timecode.
"""
if self.ms_frame:
return self.float-(1e-3) if as_float else str(self)

hh, mm, ss, ff = self.frames_to_tc(self.frames + 1, skip_rollover=True)
framerate = float(self.framerate) if self._ntsc_framerate else self._int_framerate
ms = ff/framerate
if as_float:
return (hh*3600 + mm*60 + ss + ms)
return "{:02d}:{:02d}:{:02d}.{:03d}".format(hh, mm, ss, round(ms*1000))

def to_realtime(self, as_float=False):
"""Convert a Timecode to a "real time" timestamp.
Reference: SMPTE 12-1 §5.1.2

Args:
as_float (bool): Return the time as a float number of seconds.

Returns:
str: The "real time" timestamp of the Timecode.
"""
#float property is in the video system time grid
ts_float = self.float

if self.ms_frame:
return ts_float-(1e-3) if as_float else str(self)

# "int_framerate" frames is one second in NTSC time
if self._ntsc_framerate:
ts_float *= 1.001
if as_float:
return ts_float

f_fmtdivmod = lambda x: (int(x[0]), x[1])
hh, ts_float = f_fmtdivmod(divmod(ts_float, 3600))
mm, ts_float = f_fmtdivmod(divmod(ts_float, 60))
ss, ts_float = f_fmtdivmod(divmod(ts_float, 1))
ms = round(ts_float*1000)

return "{:02d}:{:02d}:{:02d}.{:03d}".format(hh, mm, ss, ms)

@classmethod
def parse_timecode(cls, timecode):
"""Parse the given timecode string.
Expand Down Expand Up @@ -612,6 +659,7 @@ def __add__(self, other):
"""
# duplicate current one
tc = Timecode(self.framerate, frames=self.frames)
tc.drop_frame = self.drop_frame

if isinstance(other, Timecode):
tc.add_frames(other.frames)
Expand Down Expand Up @@ -645,8 +693,9 @@ def __sub__(self, other):
raise TimecodeError(
"Type {} not supported for arithmetic.".format(other.__class__.__name__)
)

return Timecode(self.framerate, frames=abs(subtracted_frames))
tc = Timecode(self.framerate, frames=abs(subtracted_frames))
tc.drop_frame = self.drop_frame
return tc

def __mul__(self, other):
"""Return a new Timecode instance with multiplied value.
Expand All @@ -669,8 +718,9 @@ def __mul__(self, other):
raise TimecodeError(
"Type {} not supported for arithmetic.".format(other.__class__.__name__)
)

return Timecode(self.framerate, frames=multiplied_frames)
tc = Timecode(self.framerate, frames=multiplied_frames)
tc.drop_frame = self.drop_frame
return tc

def __div__(self, other):
"""Return a new Timecode instance with divided value.
Expand Down