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
4 changes: 2 additions & 2 deletions contrib/adapters/extern_rv.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def _write_item(it, to_session):
)
)
src.setCutOut(
range_to_read.end_time().value_rescaled_to(
range_to_read.end_time_exclusive().value_rescaled_to(
range_to_read.duration
)
)
Expand All @@ -141,7 +141,7 @@ def _write_item(it, to_session):
"{},start={},end={},fps={}.movieproc".format(
kind,
range_to_read.start_time.value,
range_to_read.end_time().value,
range_to_read.end_time_exclusive().value,
range_to_read.duration.rate
)
]
Expand Down
6 changes: 3 additions & 3 deletions examples/shot_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def _timeline_with_breaks(name, full_path, dryrun=False):

# Note: if you wanted to snap to a particular frame rate, you would do
# it here.
end_time = otio.opentime.RationalTime(
end_time_exclusive = otio.opentime.RationalTime(
float(end_time_in_seconds),
1.0
).rescaled_to(fps)
Expand All @@ -127,7 +127,7 @@ def _timeline_with_breaks(name, full_path, dryrun=False):
clip.name = "shot {0}".format(shot_index)
clip.source_range = otio.opentime.range_from_start_end_time(
start_time,
end_time
end_time_exclusive
)

available_range = _media_start_end_of(full_path, fps)
Expand All @@ -138,7 +138,7 @@ def _timeline_with_breaks(name, full_path, dryrun=False):
)
track.append(clip)

playhead = end_time
playhead = end_time_exclusive
shot_index += 1

return timeline
Expand Down
4 changes: 2 additions & 2 deletions opentimelineio/adapters/cmx_3600.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,11 +350,11 @@ def write_to_string(input_otio):
track = input_otio.tracks[0]
for i, clip in enumerate(track):
source_tc_in = otio.opentime.to_timecode(clip.source_range.start_time)
source_tc_out = otio.opentime.to_timecode(clip.source_range.end_time())
source_tc_out = otio.opentime.to_timecode(clip.source_range.end_time_exclusive())

range_in_track = track.range_of_child_at_index(i)
record_tc_in = otio.opentime.to_timecode(range_in_track.start_time)
record_tc_out = otio.opentime.to_timecode(range_in_track.end_time())
record_tc_out = otio.opentime.to_timecode(range_in_track.end_time_exclusive())
reel = "AX"
name = None
url = None
Expand Down
4 changes: 2 additions & 2 deletions opentimelineio/adapters/fcp_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,14 +419,14 @@ def _build_item(item, timeline_range, br_map):
# media. But xml regards the source in point from the start of the media.
# So we subtract the media timecode.
source_start = item.source_range.start_time - timecode
source_end = item.source_range.end_time() - timecode
source_end = item.source_range.end_time_exclusive() - timecode

_insert_new_sub_element(item_e, 'duration',
text=str(int(item.source_range.duration.value)))
_insert_new_sub_element(item_e, 'start',
text=str(int(timeline_range.start_time.value)))
_insert_new_sub_element(item_e, 'end',
text=str(int(timeline_range.end_time().value)))
text=str(int(timeline_range.end_time_exclusive().value)))
_insert_new_sub_element(item_e, 'in',
text=str(int(source_start.value)))
_insert_new_sub_element(item_e, 'out',
Expand Down
6 changes: 3 additions & 3 deletions opentimelineio/core/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,13 +325,13 @@ def trimmed_range_of_child(self, child, reference_space=None):
)

# trimmed out
if new_start_time >= result_range.end_time():
if new_start_time >= result_range.end_time_exclusive():
return None

# compute duration
new_duration = min(
result_range.end_time(),
self.source_range.end_time()
result_range.end_time_exclusive(),
self.source_range.end_time_exclusive()
) - new_start_time

if new_duration.value < 0:
Expand Down
164 changes: 99 additions & 65 deletions opentimelineio/opentime.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@
simple.
"""

import math

class RationalTime(object):

""" Represents a point in time. Has a value and scale. """
class RationalTime(object):
"""
Represents an instantaneous point in time, value * (1/rate) seconds from
time 0seconds.
"""

def __init__(self, value=0, rate=1):
self.value = value
Expand Down Expand Up @@ -170,7 +174,6 @@ def __hash__(self):


class TimeTransform(object):

""" 1D Transform for RationalTime. Has offset and scale. """

def __init__(self, offset=RationalTime(), scale=1.0, rate=None):
Expand All @@ -182,7 +185,7 @@ def applied_to(self, other):
if isinstance(other, TimeRange):
return range_from_start_end_time(
start_time=self.applied_to(other.start_time),
end_time=self.applied_to(other.end_time())
end_time_exclusive=self.applied_to(other.end_time_exclusive())
)

target_rate = self.rate if self.rate is not None else other.rate
Expand Down Expand Up @@ -241,17 +244,18 @@ def __hash__(self):


class BoundStrategy(object):

""" Different bounding strategies for TimeRange """

Free = 1
Clamp = 2


class TimeRange(object):
""" Contains a range of time, starting (and including) start_time and
lasting duration.value * (1/duration.rate) seconds.

""" Contains a range of time, [start_time, end_time()).

end_time is computed, duration is stored.
A 0 duration TimeRange is the same as a RationalTime, and contains only the
start_time of the TimeRange.
"""

def __init__(self, start_time=RationalTime(), duration=RationalTime()):
Expand All @@ -271,37 +275,73 @@ def duration(self, val):
)
self._duration = val

def end_time(self):
"""" Compute and return the end point of the time range (inclusive).

The end point of a range of frames is the FINAL frame, not the frame
after. This means that it is self.start_time + self.duration - 1
def end_time_inclusive(self):
"""
The time of the last sample that contains data in the TimeRange.

return self.start_time + self.duration
If the TimeRange goes from (0, 24) w/ duration (10, 24), this will be
(9, 24)

def extended_by(self, other):
If the TimeRange goes from (0, 24) w/ duration (10.5, 24):
(10, 24)

In other words, the last frame with data (however fractional).
"""
Extend the bounds by another TimeRange or RationalTime (without
changing the start_bound or end_bound).

if (
self.end_time_exclusive()
- self.start_time.rescaled_to(self.duration)
).value > 1:

result = (
self.end_time_exclusive() - RationalTime(1, self.duration.rate)
)

# if the duration's value has a fractional component
if self.duration.value != math.floor(self.duration.value):
result = self.end_time_exclusive()
result.value = math.floor(result.value)

return result
else:
return self.start_time

def end_time_exclusive(self):
""""
Time of the first sample outside the time range.

If Start Frame is 10 and duration is 5, then end_time_exclusive is 15,
even though the last time with data in this range is 14.

If Start Frame is 10 and duration is 5.5, then end_time_exclusive is
15.5, even though the last time with data in this range is 15.
"""

return self.duration + self.start_time.rescaled_to(self.duration)

def extended_by(self, other):
""" Construct a new TimeRange that is this one extended by another. """

result = TimeRange(self.start_time, self.duration)
end = self.end_time()
if isinstance(other, RationalTime):
result.start_time = min(other, self.start_time)
end = max(other, end)
result.duration = duration_from_start_end_time(
result.start_time, end)
elif isinstance(other, TimeRange):
if isinstance(other, TimeRange):
result.start_time = min(self.start_time, other.start_time)
result.duration = max(self.duration, other.duration)
new_end_time = max(
self.end_time_exclusive(),
other.end_time_exclusive()
)
result.duration = duration_from_start_end_time(
self.start_time,
new_end_time
)
else:
raise TypeError(
"extended_by requires rtime be a RationalTime or TimeRange, "
"not a '{}'".format(type(other)))
"extended_by requires rtime be a TimeRange, not a '{}'".format(
type(other)
)
)
return result

# @TODO: remove?
def clamped(
self,
other,
Expand All @@ -319,15 +359,20 @@ def clamped(
if start_bound == BoundStrategy.Clamp:
test_point = max(other, self.start_time)
if end_bound == BoundStrategy.Clamp:
test_point = min(test_point, self.end_time())
# @TODO: this should probably be the end_time_inclusive,
# not exclusive
test_point = min(test_point, self.end_time_exclusive())
return test_point
elif isinstance(other, TimeRange):
test_range = other
end = test_range.end_time()
end = test_range.end_time_exclusive()
if start_bound == BoundStrategy.Clamp:
test_range.start_time = max(other.start_time, self.start_time)
if end_bound == BoundStrategy.Clamp:
end = min(test_range.end_time(), self.end_time())
end = min(
test_range.end_time_exclusive(),
self.end_time_exclusive()
)
test_range.duration = end - test_range.start_time
return test_range
else:
Expand All @@ -344,11 +389,14 @@ def contains(self, other):
"""

if isinstance(other, RationalTime):
return (self.start_time <= other and other < self.end_time())
return (
self.start_time <= other
and other < self.end_time_exclusive()
)
elif isinstance(other, TimeRange):
return (
self.start_time <= other.start_time and
self.end_time() >= other.end_time()
self.end_time_exclusive() >= other.end_time_exclusive()
)
raise TypeError(
"contains only accepts on otio.opentime.RationalTime or "
Expand All @@ -366,44 +414,24 @@ def overlaps(self, other):
elif isinstance(other, TimeRange):
return (
(
self.start_time < other.start_time and
self.end_time() > other.start_time) or
(
self.end_time() > other.end_time() and
self.start_time < other.end_time()
) or
(
self.start_time > other.start_time and
self.end_time() < other.end_time()
self.start_time < other.end_time_exclusive() and
other.start_time < self.end_time_exclusive()
)
)
raise TypeError(
"overlaps only accepts on otio.opentime.RationalTime or "
"otio.opentime.TimeRange, not {}".format(type(other))
)

def range_eq(self, other):
"""
Only compare the time boundaries, not the bound policies, unlike
__eq__.
"""
try:
return (
self.start_time == other.start_time and
self.duration == other.duration
)
except AttributeError:
return False

def __hash__(self):
return hash(
(self.start_time, self.duration)
)
return hash((self.start_time, self.duration))

def __eq__(self, rhs):
try:
return (self.start_time, self.duration) == (
rhs.start_time, rhs.duration)
return (
(self.start_time, self.duration) ==
(rhs.start_time, rhs.duration)
)
except AttributeError:
return False

Expand Down Expand Up @@ -506,29 +534,35 @@ def to_footage(time_obj):
raise NotImplementedError


def duration_from_start_end_time(start_time, end_time):
def duration_from_start_end_time(start_time, end_time_exclusive):
"""
Compute duration of samples from first to last. This is not the same as
distance. For example, the duration of a clip from frame 10 to frame 15
is 6 frames. Result in the rate of start_time.
"""

if start_time.rate == end_time.rate:
# @TODO: what to do when start_time > end_time_exclusive?

if start_time.rate == end_time_exclusive.rate:
return RationalTime(
end_time.value - start_time.value,
end_time_exclusive.value - start_time.value,
start_time.rate
)
else:
return RationalTime(
end_time.value_rescaled_to(start_time) - start_time.value,
(
end_time_exclusive.value_rescaled_to(start_time)
- start_time.value
),
start_time.rate
)


def range_from_start_end_time(start_time, end_time):
# @TODO: create range from start/end [in,ex]clusive
def range_from_start_end_time(start_time, end_time_exclusive):
"""Create a TimeRange from start and end RationalTimes."""

return TimeRange(
start_time,
duration=duration_from_start_end_time(start_time, end_time)
duration=duration_from_start_end_time(start_time, end_time_exclusive)
)
Loading