-
Notifications
You must be signed in to change notification settings - Fork 3
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
Parser Rebuild #9
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
…from_file Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
… other than a comment Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
…ead of floats Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
… given before clip parsing. This better flags non-edl files Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
…a transition event Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
1ddd739
to
5e86a53
Compare
Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
Signed-off-by: Eric Reinecke <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this is a significant improvement over the previous implementation and probably get merged as-is, especially since you mentioned this is already being used in production at Netflix.
Most of my comments are questions on your approach or highlighting small fixes. However, a docstring pass would be greatly appreciated before being merged.
Admittedly the cmx_3600_reader
module is very dense. Like several operations are done in a single line or in a comprehension, which could be difficult to debug.
Also, you mentioned several times about m2
issues. Just to check, are you referring to m2
processors or something else? If it is the processors I'm surprised to here this is an issue to have to work around in the first place.
elif key == "media_reference": | ||
# These are the comments that will drive the media ref | ||
self.handled[key] = statement.data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why handle this if its identical to the else
clause?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a fair point. I think the idea is to explicitly call-out how we address media_reference
so that in future if more needs to happen it already has a place carved out to live.
def parsed_asc_sat(self, sat_data: str) -> float: | ||
return float(sat_data) | ||
|
||
def parsed_asc_sop(self, sop_data: str): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing return type annotation
) | ||
marker.marked_range = otio.opentime.TimeRange( | ||
start_time=marker_time, | ||
duration=opentime.RationalTime(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't know we could instantiate a RationalTime
without specifying any parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's a short-cut for zero time. Arguably this should be changed to:
RationalTime(rate=self.edl_rate)
for consistency-sake.
) -> Optional[otio.schema.Marker]: | ||
# An example EDL locator line looks like this: | ||
# * LOC: 01:00:01:14 RED ANIM FIX NEEDED | ||
m = re.match(r"(\d\d:\d\d:\d\d:\d\d)\s+(\w*)(\s+|$)(.*)", statement.data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Somewhat of a style preference to suggest using a more descriptive variable name than m
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Single-char variables are bad, this is good feedback. For some reason I have this nasty habit with re Match objects.
elif ( | ||
statement_identifier is NoteFormStatement.NoteFormIdentifiers.SPLIT | ||
): | ||
# SPLIT notes appy to the following event |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo
# SPLIT notes appy to the following event | |
# SPLIT notes apply to the following event |
class SourceMode(Enum): | ||
""" | ||
Which source type the edit applies to. | ||
""" | ||
|
||
VIDEO = "V" | ||
AUDIO = "A" | ||
BOTH = "B" | ||
AUDIO_2 = "A2" | ||
AUDIO_2_VIDEO = "A2/Y" | ||
AUDIO_1_AUDIO_2 = "AA" | ||
AUDIO_1_AUDIO_2_VIDEO = "AA/V" | ||
|
||
|
||
class EffectType(Enum): | ||
""" | ||
Type of effect. | ||
""" | ||
|
||
# Single source effects | ||
CUT = "C" | ||
SYNC_ROLL = "R" | ||
|
||
# Multiple source effects | ||
DISSOLVE = "D" | ||
WIPE = "W" | ||
KEY = "K" | ||
KEY_BACKGROUND = "KB" | ||
KEY_REMOVE = "KO" | ||
MATTE = "M" | ||
FOREGROUND_FILLER = "F" | ||
QUAD_SPLIT = "Q" | ||
NONADDITIVE_MIX = "N" | ||
AUDIO_MIX = "X" | ||
|
||
|
||
class SpecialSource(Enum): | ||
# BLACK/BL and BARS are called out as "Special Source Identifiers" in | ||
# the documents referenced here: | ||
# https://github.com/AcademySoftwareFoundation/OpenTimelineIO#cmx3600-edl | ||
AUX = "AX" | ||
BLACK = "BLACK" | ||
BARS = "BARS" | ||
|
||
@classmethod | ||
def _missing_(cls, value): | ||
if value == "BL": | ||
return cls.BLACK | ||
elif value == "SMPTEBars": | ||
return cls.BARS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fwiw, these could be benefit from being represented as StrEnum
s and get the benefits of strings and enums.
To account for older Python versions, this could be implemented as follows:
try:
# Python Built-Ins
from enum import StrEnum
except ImportError:
class StrEnum(str, enum.Enum):
"""
Mixin of ``str`` and ``Enum`` to make a compatible implementation of `StrEnum` for Py2.
Sourced from :
* https://www.cosmicpython.com/blog/2020-10-27-i-hate-enums.html
* https://stackoverflow.com/questions/75040733/is-there-a-way-to-use-strenum-in-earlier-python-versions
"""
def __str__(self):
# type: () -> str
return str.__str__(self)
This is used for all well-known note form statements. | ||
""" | ||
|
||
class NoteFormIdentifiers(Enum): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same suggestion of using StrEnum
for this.
line_number: Optional[int] = None | ||
event_number: Optional[str] = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, why are these defined in the class scope and populated in the instance scope?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been writing too much Java lately 😄 I should pull these "declarations"
# snap the metadata value back to what it originally was | ||
# We could consider overriding the value in the M2 speed with what | ||
# the TC says, but it's probably best to stay true to what the EDL | ||
# was signalling, who knows what TC rounding methods are used. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo
# was signalling, who knows what TC rounding methods are used. | |
# was signaling, who knows what TC rounding methods are used. |
def __init__( | ||
self, tracks, rate, style, global_start_time=None, reelname_len=8 | ||
): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we also add a parameter to specify the zfill
of the event number? If I wanted to do a round trip from a 32 EDL from AVID to OTIO back to EDL, the event number would be different between the 2 EDLs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this pass my goal was to not break the writer, but I didn't focus on addressing issues with it. It might be a good idea to collect some issues about things like this the writer could do a lot better (making "dialects" more of a first-class citizen in the implementation would fall in this category too).
""" | ||
Timeline timing: | ||
- for each clip in event, use rec start for timeline placement | ||
- If a clip has a rec end time different from the start, use it | ||
|
||
Clip Timing: | ||
- After the timeline has been resolved and all event statements have been | ||
processed, back calculate source duration through any time warps from the | ||
timeline timing | ||
- The source of truth for source timing is always the Timecode values | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like I left a stray "note to self" comment in here. This should be turned into #
comments so they aren't part of the docstring or removed if this is already captured piecemeal in the code below.
@douglascomet Thanks for the feedback! The A docstring pass is a good idea - I need to make sure I share some of the ideas I had behind this stuff to help future us. The code density stuff is good feedback - we can think about driving sections of this toward being more approachable. I'll try to carve some time for this to try and get it out in the coming weeks. |
Overview
EDL can be a tricky format to get right in the details - our original parser worked well in controlled situations, but also had issues like not adopting
global_start_time
support, not fully baked transition support, and brittleness when sources were in frame rates that didn't match the timeline rate.This refactor separates the reader code into two separate disciplines:
Changes and Enhancements
Adapter Args
The adapter has been updated with new controls on parsing behavior based on our more current knowledge of the format:
ignore_timecode_mismatch
is still present, but is always hard-wiredTrue
and raisesDeprecationWarning
when used. Will be removed in a future release.ignore_invalid_timecode_errors
- When a timecode can't be interpreted due to something likeFrame rate mismatch. Timecode '19:13:21:28' has frames beyond 23
, the start timecode in the source will be rounded up to zero frames in the next second. This helps address issues that can occur from doing things like using a 30 fps source in a 24 fps timeline without retimes.discard_unsupported_events
- enables a mode where events that can't be interpreted (likeKEY
edits) are skipped and the event is noted intimeline.metadata["cmx_3600"]["unsupported_events"]
.Note that setting
ignore_invalid_timecode_errors
anddiscard_unsupported_events
toFalse
(the default) will preserve the old behavior of erroring when correctness can't be trusted.Parsing
Timeline.global_start_time
rather thanTrack.available_range
to signal the timeline start time (cmx3600 adapter assumes tracks start at 00:00:00:00 #6) (EDL Adapter: should author and read timeline::global_start_time attribute rather than source_range.start_time of track AcademySoftwareFoundation/OpenTimelineIO#1385) (maybe EDL with offset record time breaks trimmed_range_in_parent. AcademySoftwareFoundation/OpenTimelineIO#346)EDLParseException
instances now include more contextual informationread_from_file
when the operating system default string encoding fails - this has anecdotally been successful in handling some EDLs that would otherwise failTODOs
ignore_invalid_timecode_errors
behavior