OpenTimelineIO / otio-cmx3600-adapter

OpenTimelineIO CMX 3600 EDL adapter
Apache License 2.0
4 stars 3 forks source link

cmx3600 adapter assumes tracks start at 00:00:00:00 #6

Open douglascomet opened 1 year ago

douglascomet commented 1 year ago

The editors I work with generate .edl(s) that typically start at or around 01:00:00:00. The reason they do this is because of the convention established by broadcast television to start at 1 hour. The editors also relayed that most, if not all, NLEs default their timelines to start at 01:00:00:00. Ideally, we would not have to conform how editors initialize their timelines and the adapters just react to what an editor choose to set up their timeline.

Here is a sample .edl

TITLE:   LA_previs_v0014 
FCM: NON-DROP FRAME
001  DARK_GRA V     C        01:00:00:00 01:00:01:14 00:59:58:00 00:59:59:14 
* FROM CLIP NAME:  DARK GRADIENT 2.TIF

When I run he following code, the cmx3600 adapter throws an error.

import opentimelineio as otio
path = r"Y:\dhalley\test.edl"
input_otio = otio.adapters.read_from_file(path)
track = input_otio.tracks[0]
print(f"clip: {track.clip_if()[0]}")
track_range = track.trimmed_range()
print(f"track_range: {track_range}")
start_time_code = track_range.start_time.to_timecode()
clip: Clip("DARK GRADIENT 2.TIF", MissingReference('', None, None, {}), TimeRange(RationalTime(86400, 24), RationalTime(38, 24)), {'cmx_3600': {'reel': 'DARK_GRA'}})
track_range: TimeRange(RationalTime(-86352, 24), RationalTime(3483, 24))
Traceback (most recent call last):
  File "C:\Users\doug.halley\AppData\Roaming\JetBrains\PyCharm2023.2\scratches\test_edl_parser.py", line 8, in <module>
    start_time_code = track_range.start_time.to_timecode()
ValueError: value cannot be negative here

I looked through the cmx3600 adapter code and stumbled across this snippet, where a RationalTime is constructed at 00:00:00:00. image

This code explains how the track in the above example becomes negative. This code is use to initialize the the track's source range and I think the source range should be based on the timing of the first clip found in the .edl instead of zero. I think this can be accomplished by doing the following:

            if track.source_range is None:
                zero = opentime.RationalTime(0, edl_rate)
                track.source_range = opentime.TimeRange(
                    start_time=record_in,
                    duration=zero
                )

This simple change has the start time of the track be the record_in determined from the clip and the duration be zero. From what I can tell, the duration of the track starts at zero because it gets extended later in the cmx3600 adapter based on the clips found during parsing of the .edl.

Another QOL of improvement that could be made to the cmx3600 adapter would be setting the timeline's global_start_time in this same code block. Currently the global_start_time is never set. Like the start_time of the track, the start_time of timeline should also be based on the first clip found in the .edl. With that in mind, the previous code suggestion would look like this:

            if track.source_range is None:
                zero = opentime.RationalTime(0, edl_rate)
                track.source_range = opentime.TimeRange(
                    start_time=record_in,
                    duration=zero
                )
                self.timeline.global_start_time = record_in
TrevorAyl commented 1 year ago

I had the same thought - but it seems that 'source_range' requires a negative number here or a lot of the test_cmx3600_adapter tests fail.

douglascomet commented 1 year ago

I had the same thought - but it seems that 'source_range' requires a negative number here or a lot of the test_cmx3600_adapter tests fail.

I've narrowed down the culprit for this to the use of transformed_time_range. Afaik, there aren't any docs for what this function is supposed to do but through debugging I found that it generates the negative offset.

        range_in_timeline = clip.transformed_time_range(
            clip.trimmed_range(),
            tracks
        )
TrevorAyl commented 1 year ago

Found a (stale?) explanation of how record-in + out should be calculated, using transformed_time_range, rather than stored.

I also found the definition in src/item.cpp but am too ignorant to figure out what's going on...

RationalTime
Item::transformed_time(
    RationalTime time,
    Item const*  to_item,
    ErrorStatus* error_status) const
{
    if (!to_item)
    {
        return time;
    }

    auto root   = _highest_ancestor();
    auto item   = this;
    auto result = time;

    while (item != root && item != to_item)
    {
        auto parent = item->parent();
        result -= item->trimmed_range(error_status).start_time();
        if (is_error(error_status))
        {
            return result;
        }

        result += parent->range_of_child(item, error_status).start_time();
        item = parent;
    }

    auto ancestor = item;
    item          = to_item;
    while (item != root && item != ancestor)
    {
        auto parent = item->parent();
        result += item->trimmed_range(error_status).start_time();
        if (is_error(error_status))
        {
            return result;
        }

        result -= parent->range_of_child(item, error_status).start_time();
        if (is_error(error_status))
        {
            return result;
        }

        item = parent;
    }

    assert(item == ancestor);
    return result;
}

TimeRange
Item::transformed_time_range(
    TimeRange    time_range,
    Item const*  to_item,
    ErrorStatus* error_status) const
{
    return TimeRange(
        transformed_time(time_range.start_time(), to_item, error_status),
        time_range.duration());
}

So I asked chatgpt:

The code you provided appears to be C++ code related to the OpenTimelineIO library, specifically dealing with the transformation of time and time ranges within a timeline. Let's break down what these functions do:

  1. RationalTime Item::transformed_time(RationalTime time, Item const* to_item, ErrorStatus* error_status) const: This function is a method of an Item in OpenTimelineIO. It's used to transform a given RationalTime (a type representing time with a numerator and denominator, often used for frame-based timecode) relative to another Item in the timeline. Here's a step-by-step explanation of how it works:

    • It takes three parameters: time (the time to transform), to_item (the target item relative to which the transformation is performed), and error_status (a pointer to an ErrorStatus object that can hold error information).
    • If to_item is not provided (null), it returns the original time as there's no need to perform any transformation.
    • It iterates through the items in the timeline, calculating the transformation of time to be relative to to_item. This is done by adjusting the time based on the trimmed ranges and parent-child relationships between items in the timeline.
    • It returns the transformed RationalTime.
  2. TimeRange Item::transformed_time_range(TimeRange time_range, Item const* to_item, ErrorStatus* error_status) const: This function also belongs to an Item in OpenTimelineIO. It's used to transform a given TimeRange relative to another Item in the timeline. Here's how it works:

    • It takes three parameters: time_range (the time range to transform, which includes a start time and duration), to_item (the target item relative to which the transformation is performed), and error_status (a pointer to an ErrorStatus object that can hold error information).
    • It calls the transformed_time function to transform the start time of the time_range relative to to_item.
    • It returns a new TimeRange with the transformed start time and the original duration.

These functions are crucial for manipulating timelines and managing time-related data in the OpenTimelineIO library, especially when you need to perform edits, effects, or other transformations within a timeline. They help ensure that timecode and time ranges are adjusted correctly as items are moved or transformed within the timeline structure.