Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ADR: Add Source edit by reference endpoints #90

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions .markdownlint-cli2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ ignores:
- 'LICENCE.md'
- 'ICLA.md'
- '.github/'
- 'examples/'

default: true

Expand Down
141 changes: 141 additions & 0 deletions api/TimeAddressableMediaStore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,147 @@ paths:
description: No content. The Source label property has been deleted.
"404":
description: The requested Source ID in the path is invalid.
/sources/{sourceId}/timeline:
parameters:
- name: sourceId
in: path
required: true
schema:
$ref: '#/components/schemas/uuid'
description: The Source identifier.
head:
summary: Source Timeline
description: Return Source Timeline path headers
operationId: HEAD_sources-sourceId-timeline
tags:
- Sources
responses:
"200":
$ref: '#/components/responses/trait_resource_info_head_200'
"404":
description: The requested Source does not exist, or does not have a label set.
get:
summary: Source Timeline
description: |
Returns the Source timeline

*Unsure: What happens if there's no timeline on file, because Flow lightweight copy was used instead? Should timeline be read-only?*
operationId: GET_sources-sourceId-timeline
tags:
- Sources
parameters:
- name: timerange
in: query
description: Return only the results in the timerange specified.
schema:
$ref: 'schemas/timerange.json'
- $ref: '#/components/parameters/trait_resource_paged_key'
- $ref: '#/components/parameters/trait_paged_limit'
responses:
"200":
description: ""
headers:
Link:
description: Provides references to cursors for paging. Only the 'rel' attribute with value 'next' is currently supported. If 'next' is not present then it is the last page.
schema:
type: string
X-Paging-Limit:
description: Identifies the current limit being used for paging. This may not match the requested value if the requested value was too high for the implementation
schema:
type: integer
X-Paging-Timerange:
description: Identifies the timerange for the returned data set.
schema:
$ref: 'schemas/timerange.json'
X-Paging-Count:
description: The number of items in the returned data set.
schema:
type: integer
X-Paging-Reverse-Order:
description: The items are returned in reverse order.
schema:
type: boolean
X-Paging-NextKey:
description: Opaque string that can be supplied to the `page` query parameter to get the next page of results.
schema:
type: string
content:
application/json:
schema:
type: array
items:
$ref: schemas/source-timeline.json
examples:
basic:
summary: Basic Example
value:
- source_id: 44f2fc4b-b461-4f70-8341-c820a4be14a2
timerange: "[0:0_9:0)"
- source_id: 6c77ab9f-bdef-407a-9b35-796005dacc28
timerange: "[11:0_20:0)"
original_timerange: "[1:0_10:0)"
"400":
description: Bad request. Invalid query options.
"404":
description: The Source ID in the path is invalid.

post:
summary: Create Source Timeline element
description: |
Register a new Source Timeline element, describing part of the the timeline with reference to another
Source.

Where that other Source is, in turn, a reference, implementations should "look through" the reference
and store the underlying location to which it points.

Timeranges specified as Source Timeline MUST NOT overlap with those specified for Flow Segments, for any
Flow of the Source *(but see option 2a in the associated draft ADR for an alternative)*.
operationId: POST_sources-sourceId-timeline
tags:
- Sources
requestBody:
content:
application/json:
example:
source_id: 6c77ab9f-bdef-407a-9b35-796005dacc28
timerange: "[11:0_20:0)"
original_timerange: "[1:0_10:0)"
schema:
$ref: schemas/source-timeline.json
required: true
responses:
"201":
description: created. The element has been created.
"400":
description: Bad request. Invalid JSON, or the given timeline is already covered by a Flow Segment
"403":
description: Forbidden. You do not have permission to modify this flow. It may be marked read-only.
"404":
description: The Source does not exist.
delete:
summary: Delete Source timeline element
description: |
*Note: This should probably have a delete request mechanism like Flows, but is omitted in this draft*
operationId: DELETE_sources-sourceId-timeline
tags:
- Sources
parameters:
- name: timerange
in: query
description: Only delete timeline elements that are completely covered by the given timerange.
schema:
default: _
$ref: 'schemas/timerange.json'
responses:
"204":
description: No content. The elements have been or will be deleted.
"400":
description: Bad request. Invalid query options.
"403":
description: Forbidden. You do not have permission to modify this flow. It may be marked read-only.
"404":
description: The requested flow ID in the path is invalid.

/flows:
head:
summary: List Flows
Expand Down
23 changes: 23 additions & 0 deletions api/schemas/source-timeline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"type": "object",
"description": "Provides references to other Sources in a reference Source timeline",
"title": "Source Timeline",
"required": [
"source_id",
"timerange"
],
"properties": {
"source_id" : {
"description": "The ID of the original Source being referenced.",
"type": "string"
},
"timerange": {
"description": "The timerange where this item should be placed on the current Source timeline, as described by the [TimeRange](../schemas/timerange#top) type. Note that where temporal re-ordering is used, this refers to the presentation timeline.",
"$ref": "timerange.json"
},
"original_timerange": {
"description": "The timerange from which the item should be drawn in the original Source, as described by the [TimeRange](../schemas/timerange#top) type. If omitted, assumed to match the `timerange`.",
"$ref": "timerange.json"
}
}
}
160 changes: 160 additions & 0 deletions docs/adr/0024-source-level-edit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
---
status: proposed
---
# Source-level Edit

## Context and Problem Statement

TAMS provides a way to perform limited lightweight copy edits of Flows, where segments are reused in multiple places (see [the README.md](https://github.com/bbc/tams/tree/main?tab=readme-ov-file#flow-and-media-timelines)).
This opens up some interesting workflow possibilities, but comes with several notable limitations.

Firstly it works entirely in Flows, however the TAMS data model is intended to work with Sources for most editorial operations.
As it is, if a Source exists as a lightweight copy of another, every desired Flow has to be copied separately to create it in the store.
This could be a fairly expensive operation, especially because there's no way to bulk copy segments or parts of timerange.

Secondly it is quite a blunt instrument: the only operation that can be performed is a cut.
However new Flows can be created and re-used objects can coexist with new objects in the same store, so a tool could upload new objects and register new segments covering transitions and effects, while reusing objects otherwise.

Thirdly there's no mechanism for handling ancestry.
It is possible to find all of the Flows for which a given object is used, but only by exhaustively querying all Flows in the store to find where that ID is used.
An additional endpoint may be added in future to directly query where an object ID is used, and may be the subject of a future ADR.
Regardless, there is no way to reason about how a given Flow came in to existence: was it the originator of these segments, a copy or a copy-of-a-copy - for some applications (e.g. rights management) this can be quite important.

This ADR discusses some options for improving on these limitations.

## Considered Options

* Option 1: Provide an edit API in the store that allows more complex operations to be specified on Sources
* Option 2: Provide a limited API for simple cut operations on Sources
* Option 2a: Provide a limited API as in (Option 2), that prevents mixing Flow and Source operations
* Option 3: Provide additional Flow Segment API capability for more direct by-reference operations
* Option 4: Use another EDL format, outside the TAMS API.

## Decision Outcome

Chosen option: *TBD*

### Implementation

{Once the proposal has been implemented, add a link to the relevant PRs here}

<!-- This is an optional element. Feel free to remove. -->
## Pros and Cons of the Options

### Option 1: Provide an edit API

Provide an API endpoint or set of endpoints that allow editorial operations to be described on Source timelines.
This could be thought of as a composition or edit decision API, allowing clients to write edits directly back to the store, describing how Sources get composed with transitions and effects.
Stores implementations or their clients could then render that composition on-the-fly, combining the underlying media while working by reference.

* Good, because it fully specifies complex compositions in a consistent way in the store.
* Good, because it allows for workflows that are fully edit-by-reference.
* Good, because it allows for referential workflows entirely using Sources.
* Bad, because it adds signficant additional complexity to store implementations or clients in order to implement the render process.
* Bad, because it creates *another* composition data format, when a large number already exist.

### Option 2: Provide a limited API for simple cut operations on Sources

Provide an API endpoint that allows Sources to use portions of the timerange of other Sources.
This would be equivalent to the existing object-reuse mechanism for Flows, while mitigating the shortcomings listed above.
The API would allow a client to specify that part of a Source timeline is drawn from another Source - see <https://github.com/bbc/tams/commit/cfa01b39c74f3f6e3b59062e3e0e2eb3d0d2f307> for a possible implementation and examples.
This would allow for a cuts-based edit without compositing, however the Flows that represent these Sources could have new segments added to cover transitions.

For example, given Sources A and B, a new Source C could exist containing `SourceA@[0:0_9:0)`, then a 2 second gap, then `SourceB@[1:0_10:0)`.
When a new Flow C is created, additional segments could be created covering `[9:0_11:0)` (the 2 second gap) containing a dissolve between the relevant Flows A and B.
It's assumed in the example that attempts to reference a Source that is already a reference will look through and reference against the underlying Source instead, providing a way to identify the original Source as well.

Where suitable Flows exist of those Sources, store implementations could "invent" the relevant segments themselves.
In the example above, given a Flow A and B, the store could respond a request for `GET /flows/<flowC>/segments?timerange=[5:0_6:0)` with the Flow Segments for Flow A at `[5:0_6:0)`.
However this is much more complex in cases where more than one Flow exists of each Source: how would a store identify the correct Flow in the original Source that should be mapped to the new Flow in the new Source.

* Good, because it allows for limited edit-by-reference workflows in the store.
* Good, because it provides a way to edit operations (albeit limited ones) using Sources.
* Good, because it allows for more efficient copies: a timerange can be copied into another Source without needing to create large numbers of new segment entries.
* Good, because it provides a way to handle more complex edit/composition operations by writing new segments.
* Good, because it's clear which Source a new Source originated from.
* Neutral, because it's not clear how Flows should come into existence from reference-based Sources (although some kind of profile mechanism could be introduced for matching compatible Flows).
* Bad, because some areas of a Flow timeline could originate due to a Source-level reference, and some due to new segments being created directly: care would need to be taken if the same point on a Source timeline is specified in two different ways.
* Bad, because it adds additional complexity to the API, and would likely introduce a breaking change.

### Option 2a: Provide a limited API as in (Option 2), that prevents mixing Flow and Source operations

As above, however if the Source-level edit endpoint is used on a given Source, Flow Segments cannot be created for the Flows.
Instead the Source, and all the Flows that represent it, must be fully described using references to other Sources.
In the case where new segments need to be written (e.g. to cover a transition) a new Flow and Source can be created as the "transition layer", which can then be composed into a new Source.

Building on the example above, instead we now have Flow/Source pairs A, B and D, where D contains only the rendered dissolve between A and B.
The resulting Flow/Source pair C contains `SourceA@[0:0_9:0)`, `SourceD@[9:0_11:0)`, `SourceB@[1:0_10:0)`.

This option has the same list of pros and cons above, expect the following item is mitigated and removed:

> Bad, because some areas of a Flow timeline could originate due to a Source-level reference, and some due to new segments being created directly: care would need to be taken if the same point on a Source timeline is specified in two different ways.

### Option 3: Provide additional Flow Segment API capability for more direct by-reference operations

To avoid the Flow mapping complexity introduced by Option 2/2a, another approach would be to continue working directly with Flows, but reduce the friction to creating copies of all existing Flows.
Instead of having to create a new Flow Segment for every copied segment in the original Flow, this option proposes an additional form of "reference" Flow Segment.

The references could take a form such as:

```json
[
{
"reference": {
"flow_id": "flow-a-id",
"timerange": "[0:0_9:0)"
},
"timerange": "[0:0_9:0)",
"ts_offset": "0:0",
},
{
"reference": {
"flow_id": "flow-d-id",
"timerange": "[0:0_2:0)"
},
"timerange": "[9:0_11:0)",
"ts_offset": "0:0",
},
{
"reference": {
"flow_id": "flow-c-id",
"timerange": "[1:0_10:0)"
},
"timerange": "[11:0_20:0]",
"ts_offset": "10:0",
}
]
```

While this doesn't allow working directly with Sources, it might make propagating a lightweight copy across all the Flows of a Source much more efficient (providing there are a relatively small number of edit points).

* Good, because it allows for limited edit-by-reference workflows in the store.
* Good, because it allows for more efficient copies: a timerange can be copied into another Flow without needing to create large numbers of new segment entries.
* Good, because it provides a way to handle more complex edit/composition operations by writing new segments as in Option 2.
* Good, because it's clear which Flow a new Flow originated from.
* Neutral, because it avoids a problem with which Flows come into existence, by moving it to the client's responsibility.
* Bad, because it still requires working with Flows rather than Sources.

### Option 4: Use another EDL format, outside the TAMS API

Instead of providing a more complete mechanism for lightweight edit in the TAMS API, another format could be recommended and used with references to material in a TAMS instance.
For example [OpenTimelineIO](https://github.com/AcademySoftwareFoundation/OpenTimelineIO) (or OTIO) is deliberately flexible to how the underlying media is referenced: intended to make relinking compositions as they move between systems easier, but the same approach could be applied to reference media in a TAMS store.

This would likely take the form of an Application Note, suggesting how OTIO might be used with TAMS to reference content either as a URL to a store, or a `MissingReference` with the ID in metadata.

Additional capabilities could be built on top of the combination of TAMS and OTIO, for example rendering an OTIO composition using lightweight Flow copies (and new objects for the transitions as in the examples above), or generating OTIO as part of a metadata-driven editorial workflow.

* Good, because it allows for complex compositions in a consistent way.
* Good, because it moves a significant amount of complexity into an existing technology.
* Good, because OTIO has growing support in other tools (e.g. NLEs).
* Good, because the flexible plugin model in OTIO (e.g. Media Linkers) could be used to bridge into other tools: for example fetching Flows as a file locally for an NLE without direct TAMS support.
* Neutral, because it requires incorporating an additional tech stack.
* Bad, because OTIO might be overkill for simple operations such as basic clipping.

<!-- This is an optional element. Feel free to remove. -->
## More Information

{You might want to provide additional evidence/confidence for the decision outcome here and/or
document the team agreement on the decision and/or
define when/how this decision the decision should be realized and if/when it should be re-visited.
Links to other decisions and resources might appear here as well.}
Loading