text: #
  # Copyright Contributors to the OpenTimelineIO project
  #
  # Licensed under the Apache License, Version 2.0 (the "Apache License")
  # with the following modification; you may not use this file except in
  # compliance with the Apache License and the following modification to it:
  # Section 6. Trademarks. is deleted and replaced with:
  #
  # 6. Trademarks. This License does not grant permission to use the trade
  #    names, trademarks, service marks, or product names of the Licensor
  #    and its affiliates, except as required to comply with Section 4(c) of
  #    the License and to reproduce the content of the NOTICE file.
  #
  # You may obtain a copy of the Apache License at
  #
  #     http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the Apache License with the above modification is
  # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  # KIND, either express or implied. See the Apache License for the specific
  # language governing permissions and limitations under the Apache License.
  #
  
  """RvSession Adapter harness"""
  
  import subprocess
  import os
  import copy
  import json
  
  import opentimelineio as otio
  
  
  # exception class @{
  class NoMappingForOtioTypeError(otio.exceptions.OTIOError):
      pass
  # @}
  
  
  def write_to_file(input_otio, filepath):
      if "OTIO_RV_PYTHON_BIN" not in os.environ:
          raise RuntimeError(
              "'OTIO_RV_PYTHON_BIN' not set, please set this to path to "
              "py-interp within the RV installation."
          )
  
      if "OTIO_RV_PYTHON_LIB" not in os.environ:
          raise RuntimeError(
              "'OTIO_RV_PYTHON_LIB' not set, please set this to path to python "
              "directory within the RV installation."
          )
  
      # the adapter generates a simple JSON blob that gets turned into calls into
      # the RV API.
      simplified_data = generate_simplified_json(input_otio)
  
      base_environment = copy.deepcopy(os.environ)
  
      base_environment['PYTHONPATH'] = (
          os.pathsep.join(
              [
                  # ensure that the rv adapter is on the pythonpath
                  os.path.dirname(__file__),
              ]
          )
      )
  
      proc = subprocess.Popen(
          [
              base_environment["OTIO_RV_PYTHON_BIN"],
              '-m',
              'extern_rv',
              filepath
          ],
          stdout=subprocess.PIPE,
          stderr=subprocess.PIPE,
          stdin=subprocess.PIPE,
          env=base_environment
      )
  
      # for debugging
      # with open("/var/tmp/test.json", 'w') as fo:
      #     fo.write(json.dumps(simplified_data, sort_keys=True, indent=4))
  
      # If the subprocess fails before writing to stdin is complete, python will
      # throw a IOError exception.  If it fails after writing to stdin, there
      # won't be an exception.  Either way, the return code will be non-0 so the
      # rest of the code should catch the error case and print the (presumably)
      # helpful message from the subprocess.
      try:
          proc.stdin.write(json.dumps(simplified_data))
      except IOError:
          pass
  
      out, err = proc.communicate()
  
      if out.strip():
          print("stdout: {}".format(out))
      if err.strip():
          print("stderr: {}".format(err))
  
      if proc.returncode:
          raise RuntimeError(
              "ERROR: extern_rv (called through the rv session file adapter) "
              "failed. stderr output: " + err
          )
  
  
  # real entry point for translator
  def generate_simplified_json(input_otio):
      session_json = {
          "nodes": [],
      }
  
      write_otio(input_otio, session_json)
  
      return session_json
  
  
  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 _add_node(to_session, kind, name=""):
      new_node = {
          "kind": kind,
          "name": name,
          "properties": [],
          "inputs": [],
          "commands": [],
          "node_index": len(to_session['nodes']),
      }
      to_session['nodes'].append(new_node)
      return new_node
  
  
  def _add_input(to_node, input_node):
      to_node["inputs"].append(input_node["node_index"])
  
  
  def _add_property(to_node, args):
      to_node['properties'].append(args)
  
  
  def _add_command(to_node, command_name, args):
      to_node["commands"].append((command_name, args))
  
  
  def _write_dissolve(pre_item, in_dissolve, post_item, to_session, track_kind=None):
      new_trx = _add_node(to_session, "CrossDissolve", str(in_dissolve.name))
  
      rate = pre_item.trimmed_range().duration.rate
      _add_property(
          new_trx, [
              "CrossDissolve",
              "",
              "parameters",
              "startFrame",
              "rvSession.gto.FLOAT",
              1.0
          ]
      )
      _add_property(
          new_trx,
          [
              "CrossDissolve",
              "",
              "parameters",
              "numFrames",
              "rvSession.gto.FLOAT",
              int(
                  (
                      in_dissolve.in_offset
                      + in_dissolve.out_offset
                  ).rescaled_to(rate).value
              )
          ]
      )
      _add_property(
          new_trx,
          [
              "CrossDissolve",
              "",
              "output",
              "fps",
              "rvSession.gto.FLOAT",
              rate
          ]
      )
  
      pre_item_rv = write_otio(pre_item, to_session, track_kind)
      _add_input(new_trx, 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 = _add_node(to_session, "Retime", "transition_retime")
          _add_command(
              rt_node,
              "setTargetFps",
              pre_item.media_reference.available_range.start_time.rate
          )
  
          post_item_rv = write_otio(post_item, to_session, track_kind)
  
          _add_input(rt_node, post_item_rv)
          node_to_insert = rt_node
  
      _add_input(new_trx, node_to_insert)
  
      return new_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 = _add_node(to_session, "Stack", str(in_stack.name) or "tracks")
  
      for seq in in_stack:
          result = write_otio(seq, to_session, track_kind)
          if result:
              _add_input(new_stack, result)
  
      return new_stack
  
  
  def _write_track(in_seq, to_session, _=None):
      new_seq = _add_node(to_session, "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:
              _add_input(new_seq, 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
  
              _add_command(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))
              ]
  
              _add_command(src, "setMedia", media)
  
              return True
  
          elif isinstance(item.media_reference, otio.schema.GeneratorReference):
              if item.media_reference.generator_kind == "SMPTEBars":
                  kind = "smptebars"
                  _add_command(
                      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):
      new_item = _add_node(to_session, "Source", str(it.name) or "clip")
  
      if it.metadata:
  
          _add_property(
              new_item,
  
              # arguments to property
              [
                  "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
          )
  
      _add_command(new_item, "setCutIn", in_frame)
      _add_command(new_item, "setCutOut", out_frame)
      _add_command(new_item, "setFPS", range_to_read.duration.rate)
  
      # if the media reference is missing
      if not _create_media_reference(it, new_item, track_kind):
          kind = "smptebars"
          if isinstance(it, otio.schema.Gap):
              kind = "blank"
          _add_command(
              new_item,
              "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 new_item
  