Skip to content
Merged
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
349 changes: 59 additions & 290 deletions contrib/opentimelineio_contrib/adapters/extern_rv.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,314 +36,83 @@
# python
import sys
import os

# otio
import opentimelineio as otio
import json

# rv import
sys.path += [os.path.join(os.environ["OTIO_RV_PYTHON_LIB"], "rvSession")]
import rvSession # noqa


_RV_TYPE_MAP = {
"rvSession.gto.FLOAT": rvSession.gto.FLOAT,
"rvSession.gto.STRING": rvSession.gto.STRING,
}


# because json.loads returns a unicode type
_UNICODE_TYPE = type(u"")


def main():
""" entry point, should be called from the rv adapter in otio """

session_file = rvSession.Session()

output_fname = sys.argv[1]

# read the input OTIO off stdin
input_otio = otio.adapters.read_from_string(sys.stdin.read(), 'otio_json')
simplified_data = _remove_unicode(json.loads(sys.stdin.read()))

result = execute_rv_commands(simplified_data, session_file)

result = write_otio(input_otio, session_file)
session_file.setViewNode(result)
session_file.write(output_fname)


# exception class @{
class NoMappingForOtioTypeError(otio.exceptions.OTIOError):
pass
# @}


def write_otio(otio_obj, to_session, track_kind=None):
WRITE_TYPE_MAP = {
otio.schema.Timeline: _write_timeline,
otio.schema.Stack: _write_stack,
otio.schema.Track: _write_track,
otio.schema.Clip: _write_item,
otio.schema.Gap: _write_item,
otio.schema.Transition: _write_transition,
otio.schema.SerializableCollection: _write_collection,
}

if type(otio_obj) in WRITE_TYPE_MAP:
return WRITE_TYPE_MAP[type(otio_obj)](otio_obj, to_session, track_kind)

raise NoMappingForOtioTypeError(
str(type(otio_obj)) + " on object: {}".format(otio_obj)
)


def _write_dissolve(pre_item, in_dissolve, post_item, to_session, track_kind=None):
rv_trx = to_session.newNode("CrossDissolve", str(in_dissolve.name))

rate = pre_item.trimmed_range().duration.rate
rv_trx.setProperty(
"CrossDissolve",
"",
"parameters",
"startFrame",
rvSession.gto.FLOAT,
1.0
)
rv_trx.setProperty(
"CrossDissolve",
"",
"parameters",
"numFrames",
rvSession.gto.FLOAT,
int(
(
in_dissolve.in_offset
+ in_dissolve.out_offset
).rescaled_to(rate).value
)
)

rv_trx.setProperty(
"CrossDissolve",
"",
"output",
"fps",
rvSession.gto.FLOAT,
rate
)

pre_item_rv = write_otio(pre_item, to_session, track_kind)
rv_trx.addInput(pre_item_rv)

post_item_rv = write_otio(post_item, to_session, track_kind)

node_to_insert = post_item_rv

if (
hasattr(pre_item, "media_reference")
and pre_item.media_reference
and pre_item.media_reference.available_range
and hasattr(post_item, "media_reference")
and post_item.media_reference
and post_item.media_reference.available_range
and (
post_item.media_reference.available_range.start_time.rate !=
pre_item.media_reference.available_range.start_time.rate
)
):
# write a retime to make sure post_item is in the timebase of pre_item
rt_node = to_session.newNode("Retime", "transition_retime")
rt_node.setTargetFps(
pre_item.media_reference.available_range.start_time.rate
)

post_item_rv = write_otio(post_item, to_session, track_kind)

rt_node.addInput(post_item_rv)
node_to_insert = rt_node

rv_trx.addInput(node_to_insert)

return rv_trx


def _write_transition(
pre_item,
in_trx,
post_item,
to_session,
track_kind=None
):
trx_map = {
otio.schema.TransitionTypes.SMPTE_Dissolve: _write_dissolve,
}

if in_trx.transition_type not in trx_map:
return

return trx_map[in_trx.transition_type](
pre_item,
in_trx,
post_item,
to_session,
track_kind
)


def _write_stack(in_stack, to_session, track_kind=None):
new_stack = to_session.newNode("Stack", str(in_stack.name) or "tracks")

for seq in in_stack:
result = write_otio(seq, to_session, track_kind)
if result:
new_stack.addInput(result)

return new_stack


def _write_track(in_seq, to_session, _=None):
new_seq = to_session.newNode("Sequence", str(in_seq.name) or "track")

items_to_serialize = otio.algorithms.track_with_expanded_transitions(
in_seq
)

track_kind = in_seq.kind

for thing in items_to_serialize:
if isinstance(thing, tuple):
result = _write_transition(*thing, to_session=to_session,
track_kind=track_kind)
elif thing.duration().value == 0:
continue
else:
result = write_otio(thing, to_session, track_kind)

if result:
new_seq.addInput(result)

return new_seq


def _write_timeline(tl, to_session, _=None):
result = write_otio(tl.tracks, to_session)
return result


def _write_collection(collection, to_session, track_kind=None):
results = []
for item in collection:
result = write_otio(item, to_session, track_kind)
if result:
results.append(result)

if results:
return results[0]


def _create_media_reference(item, src, track_kind=None):
if hasattr(item, "media_reference") and item.media_reference:
if isinstance(item.media_reference, otio.schema.ExternalReference):
media = [str(item.media_reference.target_url)]

if track_kind == otio.schema.TrackKind.Audio:
# Create blank video media to accompany audio for valid source
blank = "{},start={},end={},fps={}.movieproc".format(
"blank",
item.available_range().start_time.value,
item.available_range().end_time_inclusive().value,
item.available_range().duration.rate
)
# Inserting blank media here forces all content to only
# produce audio. We do it twice in case we look at this in
# stereo
media = [blank, blank] + media

src.setMedia(media)
return True

elif isinstance(item.media_reference, otio.schema.ImageSequenceReference):
frame_sub = "%0{n}d".format(
n=item.media_reference.frame_zero_padding
)

media = [
str(item.media_reference.abstract_target_url(symbol=frame_sub))
]

src.setMedia(media)

return True

elif isinstance(item.media_reference, otio.schema.GeneratorReference):
if item.media_reference.generator_kind == "SMPTEBars":
kind = "smptebars"
src.setMedia(
[
"{},start={},end={},fps={}.movieproc".format(
kind,
item.available_range().start_time.value,
item.available_range().end_time_inclusive().value,
item.available_range().duration.rate
)
]
)
return True

return False


def _write_item(it, to_session, track_kind=None):
src = to_session.newNode("Source", str(it.name) or "clip")

if it.metadata:
src.setProperty(
"RVSourceGroup",
"source",
"otio",
"metadata",
rvSession.gto.STRING,
# Serialize to a string as it seems gto has issues with unicode
str(otio.core.serialize_json_to_string(it.metadata, indent=-1))
)

range_to_read = it.trimmed_range()

if not range_to_read:
raise otio.exceptions.OTIOError(
"No valid range on clip: {0}.".format(
str(it)
)
)

in_frame = out_frame = None
if hasattr(it, "media_reference") and it.media_reference:
if isinstance(it.media_reference, otio.schema.ImageSequenceReference):
in_frame, out_frame = it.media_reference.frame_range_for_time_range(
range_to_read
)

if not in_frame and not out_frame:
# because OTIO has no global concept of FPS, the rate of the duration
# is used as the rate for the range of the source.
in_frame = otio.opentime.to_frames(
range_to_read.start_time,
rate=range_to_read.duration.rate
)
out_frame = otio.opentime.to_frames(
range_to_read.end_time_inclusive(),
rate=range_to_read.duration.rate
)

src.setCutIn(in_frame)
src.setCutOut(out_frame)
src.setFPS(range_to_read.duration.rate)

# if the media reference is missing
if not _create_media_reference(it, src, track_kind):
kind = "smptebars"
if isinstance(it, otio.schema.Gap):
kind = "blank"
src.setMedia(
[
"{},start={},end={},fps={}.movieproc".format(
kind,
range_to_read.start_time.value,
range_to_read.end_time_inclusive().value,
range_to_read.duration.rate
)
]
)

return src
def execute_rv_commands(simplified_data, to_session):
rv_nodes = []
for node in simplified_data["nodes"]:
new_node = to_session.newNode(str(node["kind"]), str(node["name"]))
rv_node_index = len(rv_nodes)

# make sure that node order lines up
assert(rv_node_index == node["node_index"])

rv_nodes.append(new_node)
node["rv_node"] = new_node

for prop in node["properties"]:
args = prop
# the fourth argument is the type
args[4] = _RV_TYPE_MAP[args[4]]

new_node.setProperty(*args)

for (fn, args) in node["commands"]:
getattr(new_node, fn)(args)

# inputs done as a second pass now that all nodes are created
for node in simplified_data["nodes"]:
for input in node["inputs"]:
node["rv_node"].addInput(rv_nodes[input])

# return the first node created.
return rv_nodes[0]


def _remove_unicode(blob):
if _UNICODE_TYPE == type(blob):
return blob.encode('utf-8')

if isinstance(blob, dict):
result = {}
for key, val in blob.items():
result[_remove_unicode(key)] = _remove_unicode(val)
return result

if isinstance(blob, list):
return [_remove_unicode(i) for i in blob]

return blob


if __name__ == "__main__":
Expand Down
Loading