# Time ranges

> The five ranges an item has, which clock each one answers in, and why they differ.

A timeline is a one-dimensional coordinate system, and laying clips
end-to-end in a track is the easy case. Trimming, nesting, dissolves and
speed changes are where it stops being obvious, and the confusion is almost
always the same one: two different ranges were read as though they were
measured against the same clock.

They are not. Every range below belongs either to the **item's own clock** —
the media's frame numbering — or to its **parent's clock**, the track it sits
in. Getting this right is most of getting OTIO right.

Rust:

```rust
use opentime::{RationalTime, TimeRange};
use otio_core::schema::{
    Base, Clip, ExternalReference, Gap, ItemData, MediaReferenceData, Track,
};
use otio_core::{Document, Node};
use std::collections::BTreeMap;

fn frames(range: TimeRange) -> String {
    format!(
        "{} for {}",
        range.start_time().value(),
        range.duration().value()
    )
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut document = Document::new();

    // Ten seconds of rushes on disk. `available_range` belongs to the media,
    // not to the clip: it is what the file offers, whoever uses it.
    let media = document.insert(Node::ExternalReference(ExternalReference {
        media: MediaReferenceData {
            available_range: Some(TimeRange::new(
                RationalTime::new(0.0, 24.0),
                RationalTime::new(240.0, 24.0),
            )),
            ..MediaReferenceData::default()
        },
        target_url: "file:///A001.mov".into(),
    }));

    // Three seconds of it, starting two seconds in. A source range is in the
    // media's clock, which is why it starts at 48 rather than at 0.
    let clip = document.insert(Node::Clip(Clip {
        item: ItemData {
            base: Base {
                name: "shot".into(),
                ..Base::default()
            },
            source_range: Some(TimeRange::new(
                RationalTime::new(48.0, 24.0),
                RationalTime::new(72.0, 24.0),
            )),
            ..ItemData::new()
        },
        media_references: BTreeMap::from([("DEFAULT_MEDIA".to_string(), media)]),
        active_media_reference_key: "DEFAULT_MEDIA".into(),
    }));

    // A second of black in front of it, so the clip does not start the track.
    let head = document.insert(Node::Gap(Gap {
        item: ItemData {
            source_range: Some(TimeRange::new(
                RationalTime::new(0.0, 24.0),
                RationalTime::new(24.0, 24.0),
            )),
            ..ItemData::new()
        },
    }));

    let track = document.insert(Node::Track(Track {
        kind: "Video".into(),
        ..Track::default()
    }));
    document.append_child(track, head)?;
    document.append_child(track, clip)?;

    // The same clip, asked four questions. The first three answer in the
    // media's clock; the last answers in the track's.
    println!("available: {}", frames(document.available_range(clip)?));
    println!("trimmed:   {}", frames(document.trimmed_range(clip)?));
    println!("visible:   {}", frames(document.visible_range(clip)?));
    println!("in parent: {}", frames(document.range_in_parent(clip)?));

    Ok(())
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/ranges-of-a-clip/rust.rs</sub>

Python:

```python
import opentimelineio as otio

# Ten seconds of rushes on disk. `available_range` belongs to the media, not
# to the clip: it is what the file offers, whoever uses it.
media = otio.schema.ExternalReference(
    target_url="file:///A001.mov",
    available_range=otio.opentime.TimeRange(
        otio.opentime.RationalTime(0, 24), otio.opentime.RationalTime(240, 24)
    ),
)

# Three seconds of it, starting two seconds in. A source range is in the
# media's clock, which is why it starts at 48 rather than at 0.
clip = otio.schema.Clip(name="shot", media_reference=media)
clip.source_range = otio.opentime.TimeRange(
    otio.opentime.RationalTime(48, 24), otio.opentime.RationalTime(72, 24)
)

track = otio.schema.Track(name="V1", kind="Video")
# A second of black in front of it, so the clip does not start the track.
track.append(
    otio.schema.Gap(
        source_range=otio.opentime.TimeRange(
            otio.opentime.RationalTime(0, 24), otio.opentime.RationalTime(24, 24)
        )
    )
)
track.append(clip)


def frames(span):
    return f"{span.start_time.value} for {span.duration.value}"


# The same clip, asked four questions. The first three answer in the media's
# clock; the last answers in the track's.
print("available:", frames(clip.available_range()))
print("trimmed:  ", frames(clip.trimmed_range()))
print("visible:  ", frames(clip.visible_range()))
print("in parent:", frames(clip.range_in_parent()))
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/ranges-of-a-clip/python.py</sub>

TypeScript:

```typescript
import {
  init,
  Clip,
  ExternalReference,
  Gap,
  RationalTime,
  TimeRange,
  Track,
} from "@alchemist-edit/otio";

await init();

const frames = (span: TimeRange) =>
  `${span.startTime.value} for ${span.duration.value}`;

// Ten seconds of rushes on disk. `availableRange` belongs to the media, not
// to the clip: it is what the file offers, whoever uses it.
const media = new ExternalReference({ targetUrl: "file:///A001.mov" });
media.availableRange = new TimeRange(
  new RationalTime(0, 24),
  new RationalTime(240, 24),
);

// Three seconds of it, starting two seconds in. A source range is in the
// media's clock, which is why it starts at 48 rather than at 0.
const clip = new Clip({ name: "shot" });
clip.setMediaReference("DEFAULT_MEDIA", media);
clip.sourceRange = new TimeRange(
  new RationalTime(48, 24),
  new RationalTime(72, 24),
);

const track = new Track({ name: "V1", kind: "Video" });
// A second of black in front of it, so the clip does not start the track.
const head = new Gap({});
head.sourceRange = new TimeRange(
  new RationalTime(0, 24),
  new RationalTime(24, 24),
);
track.appendChild(head);
track.appendChild(clip);

// The same clip, asked four questions. The first three answer in the media's
// clock; the last answers in the track's.
console.log("available:", frames(clip.availableRange()));
console.log("trimmed:  ", frames(clip.trimmedRange()));
console.log("visible:  ", frames(clip.visibleRange()));
console.log("in parent:", frames(clip.rangeInParent()));
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/ranges-of-a-clip/typescript.ts</sub>

Go:

```go
package main

import (
	"fmt"
	"log"

	otio "github.com/alchemist-editor/otio-rust/sdk/go"
)

func frames(span otio.TimeRange) string {
	return fmt.Sprintf("%v for %v", span.StartTime.Value, span.Duration.Value)
}

func main() {
	// Ten seconds of rushes on disk. AvailableRange belongs to the media,
	// not to the clip: it is what the file offers, whoever uses it.
	media, err := otio.NewExternalReference("A001", "file:///A001.mov")
	if err != nil {
		log.Fatal(err)
	}
	if err := media.SetAvailableRange(otio.TimeRange{
		StartTime: otio.RationalTime{Value: 0, Rate: 24},
		Duration:  otio.RationalTime{Value: 240, Rate: 24},
	}); err != nil {
		log.Fatal(err)
	}

	// Three seconds of it, starting two seconds in. A source range is in the
	// media's clock, which is why it starts at 48 rather than at 0.
	clip, err := otio.NewClip("shot")
	if err != nil {
		log.Fatal(err)
	}
	if err := clip.SetMediaReference("DEFAULT_MEDIA", media.Node); err != nil {
		log.Fatal(err)
	}
	if err := clip.SetSourceRange(otio.TimeRange{
		StartTime: otio.RationalTime{Value: 48, Rate: 24},
		Duration:  otio.RationalTime{Value: 72, Rate: 24},
	}); err != nil {
		log.Fatal(err)
	}

	// A second of black in front of it, so the clip does not start the track.
	head, err := otio.NewGap("")
	if err != nil {
		log.Fatal(err)
	}
	if err := head.SetSourceRange(otio.TimeRange{
		StartTime: otio.RationalTime{Value: 0, Rate: 24},
		Duration:  otio.RationalTime{Value: 24, Rate: 24},
	}); err != nil {
		log.Fatal(err)
	}

	track, err := otio.NewTrack("V1", "Video")
	if err != nil {
		log.Fatal(err)
	}
	if err := track.AppendChild(head.Node); err != nil {
		log.Fatal(err)
	}
	if err := track.AppendChild(clip.Node); err != nil {
		log.Fatal(err)
	}

	// The same clip, asked four questions. The first three answer in the
	// media's clock; the last answers in the track's.
	available, _ := clip.AvailableRange()
	trimmed, _ := clip.TrimmedRange()
	visible, _ := clip.VisibleRange()
	inParent, _ := clip.RangeInParent()
	fmt.Println("available:", frames(available))
	fmt.Println("trimmed:  ", frames(trimmed))
	fmt.Println("visible:  ", frames(visible))
	fmt.Println("in parent:", frames(inParent))
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/ranges-of-a-clip/go.go</sub>

Swift:

```swift
import OpenTimelineIO

func frames(_ span: TimeRange) -> String {
    "\(span.startTime.value) for \(span.duration.value)"
}

// Ten seconds of rushes on disk. `availableRange` belongs to the media, not
// to the clip: it is what the file offers, whoever uses it.
let media = try ExternalReference(name: "A001", targetURL: "file:///A001.mov")
try media.setAvailableRange(
    TimeRange(
        startTime: RationalTime(value: 0, rate: 24),
        duration: RationalTime(value: 240, rate: 24)))

// Three seconds of it, starting two seconds in. A source range is in the
// media's clock, which is why it starts at 48 rather than at 0.
let clip = try Clip(name: "shot")
try clip.setMediaReference("DEFAULT_MEDIA", reference: media)
try clip.setSourceRange(
    TimeRange(
        startTime: RationalTime(value: 48, rate: 24),
        duration: RationalTime(value: 72, rate: 24)))

// A second of black in front of it, so the clip does not start the track.
let head = try Gap()
try head.setSourceRange(
    TimeRange(
        startTime: RationalTime(value: 0, rate: 24),
        duration: RationalTime(value: 24, rate: 24)))

let track = try Track(name: "V1", kind: "Video")
try track.appendChild(head)
try track.appendChild(clip)

// The same clip, asked four questions. The first three answer in the media's
// clock; the last answers in the track's.
print("available:", frames(try clip.availableRange()))
print("trimmed:  ", frames(try clip.trimmedRange()))
print("visible:  ", frames(try clip.visibleRange()))
print("in parent:", frames(try clip.rangeInParent()))
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/ranges-of-a-clip/swift.swift</sub>

C++:

```cpp
#include <iostream>

#include <opentimelineio/otio.hpp>

static std::string frames(const otio::TimeRange &span) {
    return std::to_string(span.start_time.value) + " for " +
           std::to_string(span.duration.value);
}

int main() {
    // Ten seconds of rushes on disk. `available_range` belongs to the media,
    // not to the clip: it is what the file offers, whoever uses it.
    otio::ExternalReference media =
        otio::ExternalReference::create("A001", "file:///A001.mov");
    media.set_available_range(otio::TimeRange(otio::RationalTime(0, 24),
                                              otio::RationalTime(240, 24)));

    // Three seconds of it, starting two seconds in. A source range is in the
    // media's clock, which is why it starts at 48 rather than at 0.
    otio::Clip clip = otio::Clip::create("shot");
    clip.set_media_reference("DEFAULT_MEDIA", media);
    clip.set_source_range(
        otio::TimeRange(otio::RationalTime(48, 24), otio::RationalTime(72, 24)));

    // A second of black in front of it, so the clip does not start the track.
    otio::Gap head = otio::Gap::create("");
    head.set_source_range(
        otio::TimeRange(otio::RationalTime(0, 24), otio::RationalTime(24, 24)));

    otio::Track track = otio::Track::create("V1", "Video");
    track.append_child(head);
    track.append_child(clip);

    // The same clip, asked four questions. The first three answer in the
    // media's clock; the last answers in the track's.
    std::cout << "available: " << frames(clip.available_range()) << "\n";
    std::cout << "trimmed:   " << frames(clip.trimmed_range()) << "\n";
    std::cout << "visible:   " << frames(clip.visible_range()) << "\n";
    std::cout << "in parent: " << frames(clip.range_in_parent()) << "\n";

    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/ranges-of-a-clip/cpp.cpp</sub>

Zig:

```zig
const std = @import("std");
const otio = @import("otio");

fn frames(label: []const u8, span: otio.TimeRange) void {
    std.debug.print("{s} {d} for {d}\n", .{ label, span.start_time.value, span.duration.value });
}

pub fn main() !void {
    const document = try otio.Document.init();
    defer document.deinit();

    // Ten seconds of rushes on disk. The available range belongs to the
    // media, not to the clip: it is what the file offers, whoever uses it.
    const media = try otio.ExternalReference.init(document, "A001", "file:///A001.mov");
    try media.setAvailableRange(.{
        .start_time = .{ .value = 0, .rate = 24 },
        .duration = .{ .value = 240, .rate = 24 },
    });

    // Three seconds of it, starting two seconds in. A source range is in the
    // media's clock, which is why it starts at 48 rather than at 0.
    const clip = try otio.Clip.init(document, "shot");
    try clip.setMediaReference("DEFAULT_MEDIA", media.asNode());
    try clip.setSourceRange(.{
        .start_time = .{ .value = 48, .rate = 24 },
        .duration = .{ .value = 72, .rate = 24 },
    });

    // A second of black in front of it, so the clip does not start the track.
    const head = try otio.Gap.init(document, null);
    try head.setSourceRange(.{
        .start_time = .{ .value = 0, .rate = 24 },
        .duration = .{ .value = 24, .rate = 24 },
    });

    const track = try otio.Track.init(document, "V1", "Video");
    try track.appendChild(head.asNode());
    try track.appendChild(clip.asNode());

    // The same clip, asked four questions. The first three answer in the
    // media's clock; the last answers in the track's.
    frames("available:", try clip.availableRange());
    frames("trimmed:  ", try clip.trimmedRange());
    frames("visible:  ", try clip.visibleRange());
    frames("in parent:", try clip.rangeInParent());
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/ranges-of-a-clip/zig.zig</sub>

C:

```c
#include <stdio.h>

#include "otio.h"

static void print_range(const char *label, OtioTimeRange span) {
    printf("%s %g for %g\n", label, span.start_time.value, span.duration.value);
}

int main(void) {
    OtioDocument *document = otio_document_new();

    /* Ten seconds of rushes on disk. The available range belongs to the
     * media, not to the clip: it is what the file offers, whoever uses it. */
    OtioNode media;
    otio_external_reference_new(document, "A001", "file:///A001.mov", &media, NULL);

    OtioTimeRange available;
    available.start_time.value = 0.0;
    available.start_time.rate = 24.0;
    available.duration.value = 240.0;
    available.duration.rate = 24.0;
    otio_media_reference_set_available_range(document, media, available, NULL);

    /* Three seconds of it, starting two seconds in. A source range is in the
     * media's clock, which is why it starts at 48 rather than at 0. */
    OtioNode clip;
    otio_clip_new(document, "shot", &clip, NULL);
    otio_clip_set_media_reference(document, clip, "DEFAULT_MEDIA", media, NULL);

    OtioTimeRange source;
    source.start_time.value = 48.0;
    source.start_time.rate = 24.0;
    source.duration.value = 72.0;
    source.duration.rate = 24.0;
    otio_item_set_source_range(document, clip, source, NULL);

    /* A second of black in front of it, so the clip does not start the
     * track. */
    OtioNode head;
    otio_gap_new(document, NULL, &head, NULL);

    OtioTimeRange black;
    black.start_time.value = 0.0;
    black.start_time.rate = 24.0;
    black.duration.value = 24.0;
    black.duration.rate = 24.0;
    otio_item_set_source_range(document, head, black, NULL);

    OtioNode track;
    otio_track_new(document, "V1", "Video", &track, NULL);
    otio_composition_append_child(document, track, head, NULL);
    otio_composition_append_child(document, track, clip, NULL);

    /* The same clip, asked four questions. The first three answer in the
     * media's clock; the last answers in the track's. */
    OtioTimeRange range;
    otio_item_available_range(document, clip, &range, NULL);
    print_range("available:", range);
    otio_item_trimmed_range(document, clip, &range, NULL);
    print_range("trimmed:  ", range);
    otio_item_visible_range(document, clip, &range, NULL);
    print_range("visible:  ", range);
    otio_item_range_in_parent(document, clip, &range, NULL);
    print_range("in parent:", range);

    otio_document_free(document);
    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/ranges-of-a-clip/c.c</sub>

C#:

```csharp
using System;
using OpenTimelineIO;

static string Frames(TimeRange span) => $"{span.StartTime.Value} for {span.Duration.Value}";

// Ten seconds of rushes on disk. The available range belongs to the media,
// not to the clip: it is what the file offers, whoever uses it.
var media = new ExternalReference("A001", "file:///A001.mov");
media.SetAvailableRange(new TimeRange(new RationalTime(0, 24), new RationalTime(240, 24)));

// Three seconds of it, starting two seconds in. A source range is in the
// media's clock, which is why it starts at 48 rather than at 0.
var clip = new Clip("shot");
clip.SetMediaReference("DEFAULT_MEDIA", media);
clip.SetSourceRange(new TimeRange(new RationalTime(48, 24), new RationalTime(72, 24)));

// A second of black in front of it, so the clip does not start the track.
var head = new Gap();
head.SetSourceRange(new TimeRange(new RationalTime(0, 24), new RationalTime(24, 24)));

var track = new Track("V1", "Video");
track.AppendChild(head);
track.AppendChild(clip);

// The same clip, asked four questions. The first three answer in the media's
// clock; the last answers in the track's.
Console.WriteLine("available: " + Frames(clip.AvailableRange()));
Console.WriteLine("trimmed:   " + Frames(clip.TrimmedRange()));
Console.WriteLine("visible:   " + Frames(clip.VisibleRange()));
Console.WriteLine("in parent: " + Frames(clip.RangeInParent()));
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/ranges-of-a-clip/csharp.cs</sub>

Objective-C:

```objectivec
#import <Foundation/Foundation.h>

#import <OpenTimelineIO/OpenTimelineIO.h>

static NSString *Frames(OTIOTimeRange span) {
    return [NSString stringWithFormat:@"%g for %g", span.startTime.value, span.duration.value];
}

int main(void) {
    @autoreleasepool {
        NSError *error = nil;

        // Ten seconds of rushes on disk. The available range belongs to the
        // media, not to the clip: it is what the file offers, whoever uses
        // it.
        OTIOExternalReference *media = [OTIOExternalReference externalReferenceWithName:@"A001"
                                                                              targetURL:@"file:///A001.mov"
                                                                                  error:&error];
        [media setAvailableRange:OTIOTimeRangeMake(OTIORationalTimeMake(0, 24),
                                                   OTIORationalTimeMake(240, 24))
                           error:&error];

        // Three seconds of it, starting two seconds in. A source range is in
        // the media's clock, which is why it starts at 48 rather than at 0.
        OTIOClip *clip = [OTIOClip clipWithName:@"shot" error:&error];
        [clip setMediaReference:@"DEFAULT_MEDIA" reference:media error:&error];
        [clip setSourceRange:OTIOTimeRangeMake(OTIORationalTimeMake(48, 24),
                                               OTIORationalTimeMake(72, 24))
                       error:&error];

        // A second of black in front of it, so the clip does not start the
        // track.
        OTIOGap *head = [OTIOGap gapWithName:nil error:&error];
        [head setSourceRange:OTIOTimeRangeMake(OTIORationalTimeMake(0, 24),
                                               OTIORationalTimeMake(24, 24))
                       error:&error];

        OTIOTrack *track = [OTIOTrack trackWithName:@"V1" kind:@"Video" error:&error];
        [track appendChild:head error:&error];
        [track appendChild:clip error:&error];

        // The same clip, asked four questions. The first three answer in the
        // media's clock; the last answers in the track's.
        OTIOTimeRange range;
        if ([clip getAvailableRange:&range error:&error]) NSLog(@"available: %@", Frames(range));
        if ([clip getTrimmedRange:&range error:&error]) NSLog(@"trimmed:   %@", Frames(range));
        if ([clip getVisibleRange:&range error:&error]) NSLog(@"visible:   %@", Frames(range));
        if ([clip getRangeInParent:&range error:&error]) NSLog(@"in parent: %@", Frames(range));
    }
    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/ranges-of-a-clip/objectivec.m</sub>

That sample prints four different answers about one clip, and the rest of
this page is why.

## The ranges in the item's own clock

### `available_range`

Everything behind the item, before anything trims it.

For a clip that is the active media reference's own `available_range`: if the
clip points at `wedding.mov`, this is how long that file is and what timecode
it starts at. For a track it is the sum of its children; for a stack, the
longest of them.

It can be unknown. Media goes missing, and a URL can be expensive enough to
query that nobody has. Upstream answers `None` in that case; here asking a
clip whose media says nothing about its length is an error rather than an
answer — `NoAvailableRange` in Rust, which reaches C as
`OTIO_STATUS_CORE_ERROR` and each SDK as whatever that SDK makes of a
failure. Note the difference from `source_range` below, where absence *is*
one of the answers and the status is `OTIO_STATUS_NO_VALUE`.

### `source_range`

The piece of the media this item uses — the trim, and the only one of these
ranges you set rather than ask for.

It may be absent, which means the item uses all of its media. An item with
neither a `source_range` nor an `available_range` is invalid: something has
to say how long it is.

It is usually shorter than what is available, but it does not have to be.
Asking for a range that runs past the end of the media is a real thing to
do — "this shot needs to be four seconds and only two have been rendered" —
and neither upstream nor this port will stop you or snap it back. What a
player does about it is the player's business.

### `trimmed_range`

The `source_range` if there is one, and the `available_range` if there is
not. This is how long the item is as far as its parent is concerned, and it
is what `duration` reports.

### `visible_range`

The `trimmed_range`, widened by any transition that reaches into the item.

A clip trimmed to end at frame 10, followed by a dissolve with an
`out_offset` of 5, is on screen until frame 15 — so its `visible_range` ends
at 15 while its `trimmed_range` ends at 10. With no transition touching it,
the two are equal, which is why the sample above prints the same numbers for
both.

## The ranges in the parent's clock

### `range_in_parent`

Where the item sits in the composition holding it.

In a **track** this is what you would expect, and one invariant follows from
it: each child begins exactly where the previous one ended, so for adjacent
clips A and B in a track,

```text
A.range_in_parent().end_time_exclusive() == B.range_in_parent().start_time()
```

In a **stack** it is duller, because a stack lays its children over the same
span: the start is always zero and the duration is the child's own. That is
what "these tracks are on top of each other" means. To offset something, put
it in a track and put a gap in front of it — or trim the track itself.

### `trimmed_range_in_parent`

The same, clipped to the parent's own `source_range`.

Most parents have no `source_range`, so most of the time this is
`range_in_parent` again. When the parent *is* trimmed, this is the honest
answer about where the child is relative to what survives the trim, and when
the parent's trim excludes the child entirely there is no answer at all:
`None` in Rust, `OTIO_STATUS_NO_VALUE` through the C ABI.

One inherited quirk: a *direct* child trimmed out of its composition raises
`InvalidTimeRange` rather than answering "nothing". That is upstream's
behaviour and it is deliberate here, because the alternative — handing back
a zero-length range — would quietly place the item at the head of the track.

## Compositions

A track or a stack is an item too, so it has all of the above. Two more are
worth naming:

- **`range_of_child_at_index`** — where the nth child sits, transitions
  included.
- **`trimmed_range_of_child_at_index`** — the same, relative to the
  composition's own trim.

Asking a track or a stack for its `duration` sums its children, which is why
nobody has to tell a track how long it is.

## Markers

A marker can hang off any item, and its `marked_range` is in **that item's**
clock — the same clock as the item's `source_range`. A marker on a clip is
positioned in the media's numbering; a marker on a track is positioned along
the track.

Its duration may be zero, which means an instant rather than a span. Both
are ordinary.

## Transitions

A transition has `in_offset` and `out_offset` rather than a range of its own:
how far it reaches back into the item before it and forward into the item
after. `range_in_parent` and `trimmed_range_in_parent` work on one, and both
defer to the parent.

A transition does not make its track any longer or shorter. A tool that
cannot render one can ignore it and still get the timeline's length right.

---

This page is adapted from
[Time Ranges](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/blob/main/docs/tutorials/time-ranges.md)
in upstream OpenTimelineIO's documentation, which is Apache-2.0 like this
project. The behaviour described has been checked against this
implementation rather than carried over on trust; where the two differ — the
shape of "there is no answer", chiefly — it says so above.

---

Page: https://otio-rust-docs.vercel.app/docs/concepts/time-ranges/
