Skip to content

Commit 3f97807

Browse files
authored
RV adapter fix: don't require having access to OTIO from python interpreter in rv (#941)
* refactor rv adapter to run in host process
1 parent cd2b80b commit 3f97807

File tree

2 files changed

+411
-299
lines changed

2 files changed

+411
-299
lines changed

contrib/opentimelineio_contrib/adapters/extern_rv.py

Lines changed: 59 additions & 290 deletions
Original file line numberDiff line numberDiff line change
@@ -36,314 +36,83 @@
3636
# python
3737
import sys
3838
import os
39-
40-
# otio
41-
import opentimelineio as otio
39+
import json
4240

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

4745

46+
_RV_TYPE_MAP = {
47+
"rvSession.gto.FLOAT": rvSession.gto.FLOAT,
48+
"rvSession.gto.STRING": rvSession.gto.STRING,
49+
}
50+
51+
52+
# because json.loads returns a unicode type
53+
_UNICODE_TYPE = type(u"")
54+
55+
4856
def main():
4957
""" entry point, should be called from the rv adapter in otio """
5058

5159
session_file = rvSession.Session()
5260

5361
output_fname = sys.argv[1]
5462

55-
# read the input OTIO off stdin
56-
input_otio = otio.adapters.read_from_string(sys.stdin.read(), 'otio_json')
63+
simplified_data = _remove_unicode(json.loads(sys.stdin.read()))
64+
65+
result = execute_rv_commands(simplified_data, session_file)
5766

58-
result = write_otio(input_otio, session_file)
5967
session_file.setViewNode(result)
6068
session_file.write(output_fname)
6169

6270

63-
# exception class @{
64-
class NoMappingForOtioTypeError(otio.exceptions.OTIOError):
65-
pass
66-
# @}
67-
68-
69-
def write_otio(otio_obj, to_session, track_kind=None):
70-
WRITE_TYPE_MAP = {
71-
otio.schema.Timeline: _write_timeline,
72-
otio.schema.Stack: _write_stack,
73-
otio.schema.Track: _write_track,
74-
otio.schema.Clip: _write_item,
75-
otio.schema.Gap: _write_item,
76-
otio.schema.Transition: _write_transition,
77-
otio.schema.SerializableCollection: _write_collection,
78-
}
79-
80-
if type(otio_obj) in WRITE_TYPE_MAP:
81-
return WRITE_TYPE_MAP[type(otio_obj)](otio_obj, to_session, track_kind)
82-
83-
raise NoMappingForOtioTypeError(
84-
str(type(otio_obj)) + " on object: {}".format(otio_obj)
85-
)
86-
87-
88-
def _write_dissolve(pre_item, in_dissolve, post_item, to_session, track_kind=None):
89-
rv_trx = to_session.newNode("CrossDissolve", str(in_dissolve.name))
90-
91-
rate = pre_item.trimmed_range().duration.rate
92-
rv_trx.setProperty(
93-
"CrossDissolve",
94-
"",
95-
"parameters",
96-
"startFrame",
97-
rvSession.gto.FLOAT,
98-
1.0
99-
)
100-
rv_trx.setProperty(
101-
"CrossDissolve",
102-
"",
103-
"parameters",
104-
"numFrames",
105-
rvSession.gto.FLOAT,
106-
int(
107-
(
108-
in_dissolve.in_offset
109-
+ in_dissolve.out_offset
110-
).rescaled_to(rate).value
111-
)
112-
)
113-
114-
rv_trx.setProperty(
115-
"CrossDissolve",
116-
"",
117-
"output",
118-
"fps",
119-
rvSession.gto.FLOAT,
120-
rate
121-
)
122-
123-
pre_item_rv = write_otio(pre_item, to_session, track_kind)
124-
rv_trx.addInput(pre_item_rv)
125-
126-
post_item_rv = write_otio(post_item, to_session, track_kind)
127-
128-
node_to_insert = post_item_rv
129-
130-
if (
131-
hasattr(pre_item, "media_reference")
132-
and pre_item.media_reference
133-
and pre_item.media_reference.available_range
134-
and hasattr(post_item, "media_reference")
135-
and post_item.media_reference
136-
and post_item.media_reference.available_range
137-
and (
138-
post_item.media_reference.available_range.start_time.rate !=
139-
pre_item.media_reference.available_range.start_time.rate
140-
)
141-
):
142-
# write a retime to make sure post_item is in the timebase of pre_item
143-
rt_node = to_session.newNode("Retime", "transition_retime")
144-
rt_node.setTargetFps(
145-
pre_item.media_reference.available_range.start_time.rate
146-
)
147-
148-
post_item_rv = write_otio(post_item, to_session, track_kind)
149-
150-
rt_node.addInput(post_item_rv)
151-
node_to_insert = rt_node
152-
153-
rv_trx.addInput(node_to_insert)
154-
155-
return rv_trx
156-
157-
158-
def _write_transition(
159-
pre_item,
160-
in_trx,
161-
post_item,
162-
to_session,
163-
track_kind=None
164-
):
165-
trx_map = {
166-
otio.schema.TransitionTypes.SMPTE_Dissolve: _write_dissolve,
167-
}
168-
169-
if in_trx.transition_type not in trx_map:
170-
return
171-
172-
return trx_map[in_trx.transition_type](
173-
pre_item,
174-
in_trx,
175-
post_item,
176-
to_session,
177-
track_kind
178-
)
179-
180-
181-
def _write_stack(in_stack, to_session, track_kind=None):
182-
new_stack = to_session.newNode("Stack", str(in_stack.name) or "tracks")
183-
184-
for seq in in_stack:
185-
result = write_otio(seq, to_session, track_kind)
186-
if result:
187-
new_stack.addInput(result)
188-
189-
return new_stack
190-
191-
192-
def _write_track(in_seq, to_session, _=None):
193-
new_seq = to_session.newNode("Sequence", str(in_seq.name) or "track")
194-
195-
items_to_serialize = otio.algorithms.track_with_expanded_transitions(
196-
in_seq
197-
)
198-
199-
track_kind = in_seq.kind
200-
201-
for thing in items_to_serialize:
202-
if isinstance(thing, tuple):
203-
result = _write_transition(*thing, to_session=to_session,
204-
track_kind=track_kind)
205-
elif thing.duration().value == 0:
206-
continue
207-
else:
208-
result = write_otio(thing, to_session, track_kind)
209-
210-
if result:
211-
new_seq.addInput(result)
212-
213-
return new_seq
214-
215-
216-
def _write_timeline(tl, to_session, _=None):
217-
result = write_otio(tl.tracks, to_session)
218-
return result
219-
220-
221-
def _write_collection(collection, to_session, track_kind=None):
222-
results = []
223-
for item in collection:
224-
result = write_otio(item, to_session, track_kind)
225-
if result:
226-
results.append(result)
227-
228-
if results:
229-
return results[0]
230-
231-
232-
def _create_media_reference(item, src, track_kind=None):
233-
if hasattr(item, "media_reference") and item.media_reference:
234-
if isinstance(item.media_reference, otio.schema.ExternalReference):
235-
media = [str(item.media_reference.target_url)]
236-
237-
if track_kind == otio.schema.TrackKind.Audio:
238-
# Create blank video media to accompany audio for valid source
239-
blank = "{},start={},end={},fps={}.movieproc".format(
240-
"blank",
241-
item.available_range().start_time.value,
242-
item.available_range().end_time_inclusive().value,
243-
item.available_range().duration.rate
244-
)
245-
# Inserting blank media here forces all content to only
246-
# produce audio. We do it twice in case we look at this in
247-
# stereo
248-
media = [blank, blank] + media
249-
250-
src.setMedia(media)
251-
return True
252-
253-
elif isinstance(item.media_reference, otio.schema.ImageSequenceReference):
254-
frame_sub = "%0{n}d".format(
255-
n=item.media_reference.frame_zero_padding
256-
)
257-
258-
media = [
259-
str(item.media_reference.abstract_target_url(symbol=frame_sub))
260-
]
261-
262-
src.setMedia(media)
263-
264-
return True
265-
266-
elif isinstance(item.media_reference, otio.schema.GeneratorReference):
267-
if item.media_reference.generator_kind == "SMPTEBars":
268-
kind = "smptebars"
269-
src.setMedia(
270-
[
271-
"{},start={},end={},fps={}.movieproc".format(
272-
kind,
273-
item.available_range().start_time.value,
274-
item.available_range().end_time_inclusive().value,
275-
item.available_range().duration.rate
276-
)
277-
]
278-
)
279-
return True
280-
281-
return False
282-
283-
284-
def _write_item(it, to_session, track_kind=None):
285-
src = to_session.newNode("Source", str(it.name) or "clip")
286-
287-
if it.metadata:
288-
src.setProperty(
289-
"RVSourceGroup",
290-
"source",
291-
"otio",
292-
"metadata",
293-
rvSession.gto.STRING,
294-
# Serialize to a string as it seems gto has issues with unicode
295-
str(otio.core.serialize_json_to_string(it.metadata, indent=-1))
296-
)
297-
298-
range_to_read = it.trimmed_range()
299-
300-
if not range_to_read:
301-
raise otio.exceptions.OTIOError(
302-
"No valid range on clip: {0}.".format(
303-
str(it)
304-
)
305-
)
306-
307-
in_frame = out_frame = None
308-
if hasattr(it, "media_reference") and it.media_reference:
309-
if isinstance(it.media_reference, otio.schema.ImageSequenceReference):
310-
in_frame, out_frame = it.media_reference.frame_range_for_time_range(
311-
range_to_read
312-
)
313-
314-
if not in_frame and not out_frame:
315-
# because OTIO has no global concept of FPS, the rate of the duration
316-
# is used as the rate for the range of the source.
317-
in_frame = otio.opentime.to_frames(
318-
range_to_read.start_time,
319-
rate=range_to_read.duration.rate
320-
)
321-
out_frame = otio.opentime.to_frames(
322-
range_to_read.end_time_inclusive(),
323-
rate=range_to_read.duration.rate
324-
)
325-
326-
src.setCutIn(in_frame)
327-
src.setCutOut(out_frame)
328-
src.setFPS(range_to_read.duration.rate)
329-
330-
# if the media reference is missing
331-
if not _create_media_reference(it, src, track_kind):
332-
kind = "smptebars"
333-
if isinstance(it, otio.schema.Gap):
334-
kind = "blank"
335-
src.setMedia(
336-
[
337-
"{},start={},end={},fps={}.movieproc".format(
338-
kind,
339-
range_to_read.start_time.value,
340-
range_to_read.end_time_inclusive().value,
341-
range_to_read.duration.rate
342-
)
343-
]
344-
)
345-
346-
return src
71+
def execute_rv_commands(simplified_data, to_session):
72+
rv_nodes = []
73+
for node in simplified_data["nodes"]:
74+
new_node = to_session.newNode(str(node["kind"]), str(node["name"]))
75+
rv_node_index = len(rv_nodes)
76+
77+
# make sure that node order lines up
78+
assert(rv_node_index == node["node_index"])
79+
80+
rv_nodes.append(new_node)
81+
node["rv_node"] = new_node
82+
83+
for prop in node["properties"]:
84+
args = prop
85+
# the fourth argument is the type
86+
args[4] = _RV_TYPE_MAP[args[4]]
87+
88+
new_node.setProperty(*args)
89+
90+
for (fn, args) in node["commands"]:
91+
getattr(new_node, fn)(args)
92+
93+
# inputs done as a second pass now that all nodes are created
94+
for node in simplified_data["nodes"]:
95+
for input in node["inputs"]:
96+
node["rv_node"].addInput(rv_nodes[input])
97+
98+
# return the first node created.
99+
return rv_nodes[0]
100+
101+
102+
def _remove_unicode(blob):
103+
if _UNICODE_TYPE == type(blob):
104+
return blob.encode('utf-8')
105+
106+
if isinstance(blob, dict):
107+
result = {}
108+
for key, val in blob.items():
109+
result[_remove_unicode(key)] = _remove_unicode(val)
110+
return result
111+
112+
if isinstance(blob, list):
113+
return [_remove_unicode(i) for i in blob]
114+
115+
return blob
347116

348117

349118
if __name__ == "__main__":

0 commit comments

Comments
 (0)