Skip to content

Commit 42158eb

Browse files
authored
Second draft of file bundle adapter docs. (#1709)
* Second draft of file bundle adapter docs. * attempting to clarify documentation & docstrings in the code * add note about which files are compressed * clarify CWD constraint of relative input paths --------- Co-authored-by: ssteinbach <[email protected]>
1 parent ec2bc15 commit 42158eb

File tree

5 files changed

+170
-120
lines changed

5 files changed

+170
-120
lines changed

docs/tutorials/otio-filebundles.md

Lines changed: 86 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,91 +2,129 @@
22

33
## Overview
44

5-
This document describes OpenTimelineIO's file bundle formats, otiod and otioz. The intent is that they make it easier to package and send or archive OpenTimelineIO data and associated media.
5+
This document describes OpenTimelineIO's file bundle formats, `otiod` and `otioz`, as well as how to use the internal adapters that read and write them.
66

7-
## Source Timeline
7+
The OTIOZ/D File Bundle formats package OpenTimelineIO data and associated media into a single file. This can be useful for sending, archiving and interchange of a single unit that collects cut information and media together.
88

9-
For creating otio bundles, an OTIO file is used as input, whose media references are composed only of `ExternalReference` that have a target_url field pointing at a media file with a unique basename, because file bundles have a flat namespace for media. For example, if there are media references that point at:
9+
## OTIOZ/D File Bundle Format Details
1010

11-
`/project_a/academy_leader.mov`
11+
There are two encodings for OTIO file bundles, OTIOZ and OTIOD. OTIOD is an encoding in the file system that uses a directory hierarchy of files. OTIOZ is the identical structure packed into a single .zip file, currently using the python `zipfile` library. Both contain a content.otio entry at the top level which contains the cut information for the bundle.
1212

13-
and:
13+
### Structure
1414

15-
`/project_b/academy_leader.mov`
15+
File bundles have a consistent structure:
1616

17-
Because the basename of both files is `academy_leader.mov`, this will be an error. The adapters have different policies for how to handle media references. See below for more information.
17+
OTIOD:
18+
19+
```
20+
something.otiod (directory)
21+
├── content.otio (file)
22+
└── media (directory)
23+
├── media1 (file)
24+
   ├── media2 (file)
25+
   └── media3 (file)
26+
```
1827

19-
### URL Format
28+
OTIOZ (adds the version.txt file and is encoded in a zipfile):
2029

21-
The file bundle adapters expect the `target_url` field of the `media_reference` to be in one of two forms (as produced by python's urlparse library):
30+
```
31+
something.otioz (zipfile)
32+
├── content.otio (compressed file)
33+
├── version.txt (compressed file)
34+
└── media (directory)
35+
├── media1 (uncompressed file)
36+
   ├── media2 (uncompressed file)
37+
   ├── media3 (uncompressed file)
38+
   └── ... (uncompressed files)
39+
```
2240

23-
- absolute path: "file:///path/to/some/file" (encodes "/path/to/some/file")
24-
- relative path: "path/to/some/file" (assumes the path is relative to the current working directory when invoking the adapter).
41+
### content.otio file
42+
43+
This is an OpenTimelineIO file whose media references are either `MissingReference`s, or `ExternalReference`s with target_urls that are relative paths pointing into the `media` directory.
2544

26-
## Structure
45+
### version.txt file
2746

28-
File bundles, regardless of how they're encoded, have a consistent structure:
47+
This file encodes the otioz version of the file, with no other text, in the form:
2948

3049
```
31-
something.otioz
32-
├── content.otio
33-
├── version
34-
└── media
35-
├── media1
36-
   ├── media2
37-
   └── media3
50+
1.0.0
3851
```
3952

53+
### "media" Directory
4054

41-
### content.otio file
55+
The `media` directory contains all the media files that the `ExternalReference`s `target_url`s in the `content.otio` point at, in a flat structure. Each media file must have a unique basename, but can be encoded in whichever codec/container the user wishes (otio is unable to decode or encode the media files).
4256

43-
This is a normal OpenTimelineIO whose media references are either ExternalReferences with relative target_urls pointing into the `media` directory or `MissingReference`.
57+
## Adapter Usage
4458

45-
### version.txt file
59+
## Read Adapter Behavior
4660

47-
This file encodes the otioz version of the file, in the form 1.0.0.
61+
When a bundle is read from disk using the OpenTimelineIO Python API (using the adapters.read_from_* functions), only the `content.otio` file is read and parsed.
4862

49-
### Media Directory
63+
For example, to view the timeline (not the media) of an otioz file in `otioview`, you can run:
5064

51-
The media directory contains all the media files in a flat structure. They must have unique basenames, but can be encoded in whichever codec/container the user wishes (otio is unable to decode or encode the media files).
65+
`otioview sommething.otioz`
5266

53-
## Read Behavior
67+
Because this will _only_ read the `content.otio` from the bundle, it is usually a fast operation to run. None of the media is decoded or unzipped during this process.
5468

55-
When a bundle is read from disk, the `content.otio` file is extracted from the bundle and returned. For example, to view the timeline (not the media) of an otioz file in `otioview`, you can run:
69+
### extract_to_directory Optional Argument
5670

57-
`otioview sommething.otioz`
71+
extract_to_directory: if a value other than `None` is passed in, will extract the contents of the bundle into the directory at the path passed into the `extract_to_directory` argument. For the OTIOZ adapter, this will unzip the associated media.
5872

59-
This will _only_ read the `content.otio` from the bundle, so is usually a fast operation to run.
73+
### absolute_media_reference_paths Optional Argument
6074

61-
## MediaReferencePolicy
75+
The OTIOD adapter additionally has an argument `absolute_media_reference_paths` which will convert all the media references in the bundle to be absolute paths if `True` is passed. Default is `False`.
6276

63-
When building a file bundle using the OTIOZ/OTIOD adapters, you can set the 'media reference policy', which is described by an enum in the file_bundle_utils module. The policies can be:
77+
### Read Adapter Example
6478

65-
- (default) ErrorIfNotFile: will raise an exception if a media reference is found that is of type `ExternalReference` but that does not point at a `target_url`.
66-
- MissingIfNotFile: will replace any media references that meet the above condition with a `MissingReference`, preserving the original media reference in the metadata of the new `MissingReference`.
67-
- AllMissing: will replace all media references with `MissingReference`, preserving the original media reference in metadata on the new object.
79+
Extract the contents of the bundle and convert to an rv playlist:
6880

69-
When running in `AllMissing` mode, no media will be put into the bundle.
81+
`otioconvert -i /var/tmp/some_file.otioz -a extract_to_directory=/var/tmp/example_directory -o /var/tmp/example_directory/some_file.rv`
7082

71-
## OTIOD
83+
## Write Adapter
7284

73-
The OTIOD adapter will build a bundle in a directory stucture on disk. The adapter will gather up all the files it can and copy them to the destination directory, and then build the `.otio` file with local relative path references into that directory.
85+
### Source Timeline Constraints
7486

75-
## OTIOZ
87+
For creating otio bundles using the provided python adapter, an OTIO file is used as input. There are some constraints on the source timeline.
7688

77-
The OTIOZ adapter will build a bundle into a zipfile (using the zipfile library). The adapter will write media into the zip file uncompressed and the content.otio with compression.
89+
#### Unique Basenames
7890

79-
### Optional Arguments:
91+
Because file bundles have a flat namespace for media, and media will be copied into the bundle, the `ExternalReference` media references in the source OTIO must have a target_url fields pointing at media files with unique basenames.
8092

81-
- Read:
82-
- extract_to_directory: if a value other than `None` is passed in, will extract the contents of the bundle into the directory at the path passed into the `extract_to_directory` argument.
93+
For example, if there are media references that point at:
8394

84-
## Example usage in otioconvert
95+
`/project_a/academy_leader.mov`
8596

86-
### Convert an otio into a zip bundle
97+
and:
98+
99+
`/project_b/academy_leader.mov`
100+
101+
Because the basename of both files is `academy_leader.mov`, this will be an error. The adapters have different policies for how to handle media references. See below for more information.
102+
103+
#### Expected Source Timeline External Reference URL Format
104+
105+
The file bundle adapters expect the `target_url` field of any `media_reference`s in the source timeline to be in one of two forms (as produced by python's [urlparse](https://docs.python.org/3/library/urllib.parse.html) library):
106+
107+
- absolute path: "file:///path/to/some/file" (encodes "/path/to/some/file")
108+
- relative path: "path/to/some/file" (the path is relative to the current working directory of the command running the adapter on the source timeline).
109+
110+
### MediaReferencePolicy Option
111+
112+
When building a file bundle using the OTIOZ/OTIOD adapters, you can set the 'media reference policy', which is described by an enum in the file_bundle_utils module. The policies can be:
113+
114+
- (default) `ErrorIfNotFile`: will raise an exception if a media reference is found that is of type `ExternalReference` but that does not point at a `target_url`.
115+
- `MissingIfNotFile`: will replace any media references that meet the above condition with a `MissingReference`, preserving the original media reference in the metadata of the new `MissingReference`.
116+
- `AllMissing`: will replace all media references with `MissingReference`, preserving the original media reference in metadata on the new object.
117+
118+
When running in `AllMissing` mode, no media will be put into the bundle.
119+
120+
To use this argument with `otioconvert` from the commandline, you can use the `-A` flag with the argument name `media_policy`:
121+
122+
```
123+
otioconvert -i <some_file> -o path/to/output_file.otioz -A media_policy="AllMissing"
124+
```
87125

88-
`otioconvert -i somefile.otio -o /var/tmp/somefile.otioz`
126+
### Write Adapter Example
89127

90-
### Extract the contents of the bundle and convert to an rv playlist
128+
Convert an otio into a zip bundle:
91129

92-
`otioconvert -i /var/tmp/somefile.otioz -a extract_to_directory=/var/tmp/somefile -o /var/tmp/somefile/somefile.rv`
130+
`otioconvert -i some_file.otio -o /var/tmp/some_file.otioz`

src/py-opentimelineio/opentimelineio/adapters/file_bundle_utils.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@
66
import os
77
import copy
88

9-
109
from .. import (
1110
exceptions,
1211
schema,
1312
url_utils,
1413
)
1514

16-
import urllib.parse as urlparse
15+
import urllib
1716

1817

1918
# versioning
@@ -58,21 +57,17 @@ def _guarantee_unique_basenames(path_list, adapter_name):
5857
new_basename = os.path.basename(fn)
5958
if new_basename in basename_to_source_fn:
6059
raise exceptions.OTIOError(
61-
"Error: the {} adapter requires that the media files have "
62-
"unique basenames. File '{}' and '{}' have matching basenames"
63-
" of: '{}'".format(
64-
adapter_name,
65-
fn,
66-
basename_to_source_fn[new_basename],
67-
new_basename
68-
)
60+
f"Error: the {adapter_name} adapter requires that the media"
61+
f" files have unique basenames. File '{fn}' and"
62+
f" '{basename_to_source_fn[new_basename]}' have matching"
63+
f" basenames of: '{new_basename}'"
6964
)
7065
basename_to_source_fn[new_basename] = fn
7166

7267

7368
def _prepped_otio_for_bundle_and_manifest(
7469
input_otio, # otio to process
75-
media_policy, # what to do with media references
70+
media_policy, # how to handle media references (see: MediaReferencePolicy)
7671
adapter_name, # just for error messages
7772
):
7873
""" Create a new OTIO based on input_otio that has had media references
@@ -86,6 +81,8 @@ def _prepped_otio_for_bundle_and_manifest(
8681
their bundles.
8782
8883
This is considered an internal API.
84+
85+
media_policy is expected to be of type MediaReferencePolicy.
8986
"""
9087

9188
# make sure the incoming OTIO isn't edited
@@ -109,17 +106,18 @@ def _prepped_otio_for_bundle_and_manifest(
109106
# not an ExternalReference, ignoring it.
110107
continue
111108

112-
parsed_url = urlparse.urlparse(target_url)
109+
parsed_url = urllib.parse.urlparse(target_url)
113110

114-
# ensure that the urlscheme is either file or ""
111+
# ensure that the urlscheme is either "file" or ""
115112
# file means "absolute path"
116-
# none is interpreted as a relative path, relative to cwd
113+
# "" is interpreted as a relative path, relative to cwd of the python
114+
# process
117115
if parsed_url.scheme not in ("file", ""):
118116
if media_policy is MediaReferencePolicy.ErrorIfNotFile:
119117
raise NotAFileOnDisk(
120-
"The {} adapter only works with media reference"
121-
" target_url attributes that begin with 'file:'. Got a "
122-
"target_url of: '{}'".format(adapter_name, target_url)
118+
f"The {adapter_name} adapter only works with media"
119+
" reference target_url attributes that begin with 'file:'."
120+
f" Got a target_url of: '{target_url}'"
123121
)
124122
if media_policy is MediaReferencePolicy.MissingIfNotFile:
125123
cl.media_reference = reference_cloned_and_missing(

src/py-opentimelineio/opentimelineio/adapters/otiod.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626
import urllib.parse as urlparse
2727

2828

29-
def read_from_file(filepath, absolute_media_reference_paths=False):
29+
def read_from_file(
30+
filepath,
31+
# convert the media_reference paths to absolute paths
32+
absolute_media_reference_paths=False,
33+
):
3034
result = otio_json.read_from_file(
3135
os.path.join(filepath, utils.BUNDLE_PLAYLIST_PATH)
3236
)
@@ -53,6 +57,8 @@ def read_from_file(filepath, absolute_media_reference_paths=False):
5357
def write_to_file(
5458
input_otio,
5559
filepath,
60+
# see documentation in file_bundle_utils for more information on the
61+
# media_policy
5662
media_policy=utils.MediaReferencePolicy.ErrorIfNotFile,
5763
dryrun=False
5864
):
@@ -64,18 +70,14 @@ def write_to_file(
6470

6571
if not os.path.exists(os.path.dirname(filepath)):
6672
raise exceptions.OTIOError(
67-
"Directory '{}' does not exist, cannot create '{}'.".format(
68-
os.path.dirname(filepath),
69-
filepath
70-
)
73+
f"Directory '{os.path.dirname(filepath)}' does not exist, cannot"
74+
f" create '{filepath}'."
7175
)
7276

7377
if not os.path.isdir(os.path.dirname(filepath)):
7478
raise exceptions.OTIOError(
75-
"'{}' is not a directory, cannot create '{}'.".format(
76-
os.path.dirname(filepath),
77-
filepath
78-
)
79+
f"'{os.path.dirname(filepath)}' is not a directory, cannot create"
80+
f" '{filepath}'."
7981
)
8082

8183
# general algorithm for the file bundle adapters:
@@ -125,7 +127,6 @@ def write_to_file(
125127

126128
os.mkdir(filepath)
127129

128-
# write the otioz file to the temp directory
129130
otio_json.write_to_file(
130131
result_otio,
131132
os.path.join(filepath, utils.BUNDLE_PLAYLIST_PATH)

src/py-opentimelineio/opentimelineio/adapters/otioz.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@
2626

2727
from . import (
2828
file_bundle_utils as utils,
29-
otio_json
29+
otio_json,
3030
)
3131

3232
import pathlib
3333

3434

35-
def read_from_file(filepath, extract_to_directory=None):
35+
def read_from_file(
36+
filepath,
37+
# if provided, will extract contents of zip to this directory
38+
extract_to_directory=None,
39+
):
3640
if not zipfile.is_zipfile(filepath):
3741
raise exceptions.OTIOError(f"Not a zipfile: {filepath}")
3842

@@ -44,22 +48,20 @@ def read_from_file(filepath, extract_to_directory=None):
4448

4549
if not os.path.exists(extract_to_directory):
4650
raise exceptions.OTIOError(
47-
"Directory '{}' does not exist, cannot unpack otioz "
48-
"there.".format(extract_to_directory)
51+
f"Directory '{extract_to_directory()}' does not exist, cannot"
52+
" unpack otioz there."
4953
)
5054

5155
if os.path.exists(output_media_directory):
5256
raise exceptions.OTIOError(
53-
"Error: '{}' already exists on disk, cannot overwrite while "
54-
" unpacking OTIOZ file '{}'.".format(
55-
output_media_directory,
56-
filepath
57-
)
58-
57+
f"Error: '{output_media_directory}' already exists on disk, "
58+
f"cannot overwrite while unpacking OTIOZ file '{filepath}'."
5959
)
6060

6161
with zipfile.ZipFile(filepath, 'r') as zi:
62-
result = otio_json.read_from_string(zi.read(utils.BUNDLE_PLAYLIST_PATH))
62+
result = otio_json.read_from_string(
63+
zi.read(utils.BUNDLE_PLAYLIST_PATH)
64+
)
6365

6466
if extract_to_directory:
6567
zi.extractall(extract_to_directory)
@@ -70,10 +72,11 @@ def read_from_file(filepath, extract_to_directory=None):
7072
def write_to_file(
7173
input_otio,
7274
filepath,
75+
# see documentation in file_bundle_utils for more information on the
76+
# media_policy
7377
media_policy=utils.MediaReferencePolicy.ErrorIfNotFile,
7478
dryrun=False
7579
):
76-
7780
if os.path.exists(filepath):
7881
raise exceptions.OTIOError(
7982
f"'{filepath}' exists, will not overwrite."

0 commit comments

Comments
 (0)