Issue with OCIO and RV Sync

Hi we are using RV network Sync and the Screening Room for running dailies with our artists when working from home. We have found that it doesn’t always setup OCIO correctly during these remote sessions. The primary user who has set up the session it all works ok, but when others connect via the network sync OCIO doesn’t seem to load corectly and its defaulting to RV’s own colour management and then treating all our sources incorrectly.

We use a custom ocio_source_setup.rvpkg to handle contexts for loading looks, and the OCIO config is loaded when the first source image is loaded into RV, it uses the path to determine the show and set the correct config.

Does RV Sync need to have some config set to follow the same behaviour? Or should it follow the same process. If the main user pushes his session to all connected parties then it seems to fix the current source being viewed but not all of them. If the user load in any source from a show this will set OCIO to be that show and then connecting to RV sync it works ok, but just connecting from a a fresh RV session seems to cause the issue.

We also see the same behaviour when using the new Live Review as well.

Any help would be appreciated.

our source setup example.

from rv import rvtypes, commands, extra_commands
import os, re, sys
import PyOpenColorIO as OCIO

#
#   Default implementations of helper methods
#
#

DEFAULT_PIPE = {}

OCIO_ROLES = {"OCIOFile" : "RVLinearizePipelineGroup",
              "OCIOLook" : "RVLookPipelineGroup"}

OCIO_DEFAULTS = {}

METHODS = [ 'ocio_config_from_media', 'ocio_node_from_media' ]

def ocio_config_from_media(media, attributes):

    if os.getenv("OCIO") == None:
        raise Exception("ERROR: $OCIO environment variable unset!")

    return OCIO.GetCurrentConfig()

def ocio_node_from_media(config, node, default, media=None, attributes={}):

    result = [{"nodeType" : d, "context" : {}, "properties" : {}} for d in default]

    nodeType = commands.nodeType(node)

    if (nodeType == "RVDisplayPipelineGroup"):
        display = config.getDefaultDisplay()
        result = [
            {"nodeType" : "OCIODisplay",
             "context" : {},
             "properties" : {
                 "ocio.function"        : "display",
                 "ocio.inColorSpace"    : OCIO.Constants.ROLE_SCENE_LINEAR,
                 "ocio_display.view"    : config.getDefaultView(display),
                 "ocio_display.display" : display}}]

    elif (nodeType == "RVLinearizePipelineGroup"):
        inspace = config.parseColorSpaceFromString(media)
        if inspace == "":
            inspace = attributes.get("default_setting", "")
        if inspace != "":
            result = [
                {"nodeType": "OCIOFile",
                 "context" : {},
                 "properties" : {
                     "ocio.function"            : "color",
                     "ocio.inColorSpace"        : inspace,
                     "ocio_color.outColorSpace" : OCIO.Constants.ROLE_SCENE_LINEAR}},
                {"nodeType" : "RVLensWarp", "context" : {}, "properties" : {}}]

    elif (nodeType == "RVLookPipelineGroup"):
        # If our config has a Look named "shot_specific_look" and uses the
        # environment/context variable "$SHOT" to locate any required files
        # on disk, then this is what that would likely look like:
        #
        # result = [
        #     {"nodeType"   : "OCIOLook",
        #      "context"    : {"SHOT" : os.environ.get("SHOT", "def123")}
        #      "properties" : {
        #          "ocio.function"     : "look",
        #          "ocio.inColorSpace" : OCIO.Constants.ROLE_SCENE_LINEAR,
        #          "ocio_look.look"    : "shot_specific_look"}}]

        look = attributes.get("default_setting","")
        if look != "":
            result = [
                 {"nodeType"   : "OCIOLook",
                  "context"    : {},
                  "properties" : {
                      "ocio.function"     : "look",
                      "ocio_look.look"    : look}}]

    return result

#
#   A couple of convenience functions
#

def isOCIOManaged(nodeType):
    def F():
        try:
            managed = commands.getIntProperty("#" + nodeType + ".ocio.active")[0] != 0
            return commands.CheckedMenuState if managed else commands.UncheckedMenuState
        except:
            return commands.UncheckedMenuState
    return F

def isOCIODisplayManaged(group):
    def F():
        try:
            groupName = "RVDisplayPipelineGroup"
            dpipeline = groupMemberOfType(group, groupName)
            dOCIO = groupMemberOfType(dpipeline, "OCIODisplay")
            managed = commands.getIntProperty(dOCIO + ".ocio.active")[0] != 0
            return commands.CheckedMenuState if managed else commands.UncheckedMenuState
        except:
            return commands.UncheckedMenuState
    return F

def ocioMenuCheck(nodeType, prop, value):
    def F():
        try:
            current = commands.getStringProperty("#" + nodeType + "." + prop)[0]
            managed = isOCIOManaged(nodeType)() == commands.CheckedMenuState
            checked = current == value and managed
            return commands.CheckedMenuState if checked else commands.NeutralMenuState
        except:
            return commands.DisabledMenuState
    return F

def ocioDisplayMenuCheck(group, display, view):
    def F():
        try:
            groupName = "RVDisplayPipelineGroup"
            dpipeline = groupMemberOfType(group, groupName)
            dOCIO = groupMemberOfType(dpipeline, "OCIODisplay")
            d = commands.getStringProperty(dOCIO + ".ocio_display.display")[0]
            v = commands.getStringProperty(dOCIO + ".ocio_display.view")[0]
            if d == display and v == view:
                return commands.CheckedMenuState
            return commands.UncheckedMenuState
        except:
            return commands.DisabledMenuState
    return F

def ocioEvent(nodeType, prop, value):
    "This function will apply its change on the current node of nodeType in the evaluation path"
    def F(event):
        commands.setStringProperty("#" + nodeType + "." + prop, [value], True)
        commands.redraw()
    return F

def ocioEventOnAllOfType(nodeType, prop, value):
    "This function will apply its change on all nodes of nodeType"
    def F(event):
        for node in commands.nodesOfType(nodeType):
            commands.setStringProperty(node + "." + prop, [value], True)
        commands.redraw()
    return F

def ocioDisplayEvent(group, display, view):
    def F(event):
        groupName = "RVDisplayPipelineGroup"
        dpipeline = groupMemberOfType(group, groupName)
        dOCIO = groupMemberOfType(dpipeline, "OCIODisplay")
        commands.setStringProperty(dOCIO + ".ocio_display.display", [display], True)
        commands.setStringProperty(dOCIO + ".ocio_display.view", [view], True)
        commands.redraw()
    return F

def groupMemberOfType(node, memberType):
    for n in commands.nodesInGroup(node):
        if commands.nodeType(n) == memberType:
            return n
    return None

def applyProps(node, contextProps, propertiesProps):
    for pprop,avalue in propertiesProps.items():
        commands.setStringProperty(node + "." + pprop, [avalue], True)
    for cprop,cvalue in contextProps.items():
        prop = node + ".ocio_context." + cprop
        if (not commands.propertyExists(prop)):
            commands.newProperty(prop, commands.StringType, 1)
        commands.setStringProperty(prop, [cvalue], True)

#
#   OCIOSourceSetupMode
#

class OCIOSourceSetupMode(rvtypes.MinorMode):
    '''
    This mode integrates both with the base RV source_setup package and
    OCIO. The idea is that incoming source media is first examined by the
    base setup package then when appropriate, this package will switch the
    source to use OCIO. If any source uses OCIO, the display is also
    switched over to OCIO control.

    There are many assumptions here. First and foremost is that your OCIO
    worflow uses parseColorSpaceFromString() to determine incoming color
    space. 

    ORDERING: this mode uses a sort key of "source_setup" with an
    ordering value of 10 which is after the default source_setup
    mode's ordering value of 0. This ensures that the default
    source_setup is run first and is then followed by the
    ocio_source_setup. If you are using this as an example for a
    different source_setup mode which you wish to have interoperate
    with the default and ocio modes use the same key of "source_setup"
    but with an ordering number that places it relative to the default
    and ocio modes. So for example if you want yours to come before
    the ocio mode, but after the default source_setup use 5 (since its
    between 0 and 10).
    '''

    def useSourceOCIO(self, source, nodeType, defaultSetting=""):
        '''
        This tells the source group to use OCIO instead of the RV
        linearize node. There is also ocio.look and ocio.preCache
        which can be activated in this way. For this code we're
        only assuming that OCIO is going to be used to linearize
        the source.
        '''
        
        medias = commands.getStringProperty("%s.media.movie" % source)
        media  = medias[0]

        try:
            srcAttrs  = commands.sourceAttributes(source, media)
            attrDict  = dict(zip([i[0] for i in srcAttrs],[j[1] for j in srcAttrs]))
            attrDict["source_node"] = source
            attrDict["default_setting"] = defaultSetting
        except:
            attrDict = {}

        if self.config == None:
            try:
                self.config = ocio_config_from_media(media, attrDict)
                OCIO.SetCurrentConfig(self.config)
                commands.defineModeMenu("OCIO Source Setup", self.buildOCIOMenu(), True)
            except Exception, inst:
                print("ERROR: Unable to retrieve OCIO context: %s" % inst)
                return

        #
        # If we already have this OCIO node and we are reading a session,
        # then use the one we have and return
        #

        pipeSlot = OCIO_ROLES[nodeType]
        srcPipeline = groupMemberOfType(commands.nodeGroup(source), pipeSlot)
        ocioNode = groupMemberOfType(srcPipeline, nodeType)
        if ocioNode != None and self.readingSession:
            for pNode in commands.nodesInGroup(srcPipeline):
                if commands.nodeType(pNode).startswith("OCIO"):
                    commands.ocioUpdateConfig(pNode)

            print("INFO: using %s node for %s %s" % (nodeType, source, pipeSlot))
            return

        #
        #   Anywhere in RV there is a pipeline "slot" (File, Linearize,
        #   Look, Display, View) you can use an OCIO node.  Each OCIO node
        #   can futher be configured to act in a manner similar to the nuke
        #   OCIO color, look, or display nodes. In this case we want it to
        #   act as an OCIO color node so we can transform from the incoming
        #   file space to the ROLE_SCENE_LINEAR space (the working space)
        #
        #   You can only get the OCIO node *after* the source group has
        #   been configured to use it. Otherwise the pipelines will not
        #   have been created yet.
        #
        #   Under the hood, an RV "color" OCIO node builds a ColorSpace for
        #   inspace and outspace and uses the processor which converts from
        #   one to the other.
        #

        try:
            if not DEFAULT_PIPE.has_key(pipeSlot):
                default = commands.getStringProperty(srcPipeline + ".pipeline.nodes")
                DEFAULT_PIPE[pipeSlot] = default
            pipelineList = ocio_node_from_media(self.config, srcPipeline, DEFAULT_PIPE[pipeSlot], media, attrDict)
        except Exception, inst:
            print("ERROR: Problem occurred while loading OCIO settings for %s: %s" % (nodeType, inst))
            return

        try:
            pipeline = [p["nodeType"] for p in pipelineList]
        except KeyError, inst:
            print("ERROR: Unable to make use of ocio_node_from_media return: %s" % inst)
        if pipeline == DEFAULT_PIPE[pipeSlot]:
            return

        print("INFO: using %s node for %s %s" % (nodeType, source, pipeSlot))

        commands.setStringProperty(srcPipeline + ".pipeline.nodes", pipeline, True)
        pipeNodes = commands.nodesInGroup(srcPipeline)
        pipeNodes.sort()
        for index,pNode in enumerate(pipelineList):
            stageOCIO = pipeNodes[index]
            try:
                applyProps(stageOCIO, pNode["context"], pNode["properties"])
            except KeyError, inst:
                print("ERROR: Unable to apply properties to %s: %s" % (stageOCIO, inst))

        commands.redraw()

    def disableSourceOCIO(self, source, nodeType):
        '''
        This reverts the source group's linearize node back to using
        a native RVLinearize node. 
        '''
        
        pipeSlot = OCIO_ROLES[nodeType]
        srcPipeline = groupMemberOfType(commands.nodeGroup(source), pipeSlot)
        nodesProp = srcPipeline + ".pipeline.nodes"
        current = commands.getStringProperty(nodesProp)

        if not DEFAULT_PIPE.has_key(pipeSlot) or current == DEFAULT_PIPE[pipeSlot]:
            return

        print("INFO: resetting %s for %s" % (pipeSlot, source))

        commands.setStringProperty(srcPipeline + ".pipeline.nodes", DEFAULT_PIPE[pipeSlot], True)
        commands.redraw()

    def useDisplayOCIO(self, group):
        '''
        This installs the OCIODisplay node in the DisplayGroup's display pipeline 
        in place of RV's RVDisplayColor node.

        NOTE: in RV4 all display devices are separate
        DisplayGroups. So each one can have a completely different
        view and display transform.
        '''

        if self.usingOCIOForDisplay.get(group, False) or self.config == None:
            return

        groupName = "RVDisplayPipelineGroup"
        try:
            dpipeline = groupMemberOfType(group, groupName)
            if not DEFAULT_PIPE.has_key(groupName):
                default = commands.getStringProperty(dpipeline + ".pipeline.nodes")
                DEFAULT_PIPE[groupName] = default
            pipelineList = ocio_node_from_media(self.config, dpipeline, DEFAULT_PIPE[groupName])
        except Exception, inst:
            print("ERROR: Problem occurred while loading OCIO settings for OCIODisplay: %s" % inst)
            return

        try:
            pipeline = [p["nodeType"] for p in pipelineList]
        except KeyError, inst:
            print("ERROR: Unable to make use of ocio_node_from_media return: %s" % inst)
        if pipeline == DEFAULT_PIPE[groupName]:
            return

        device = commands.getStringProperty(group + ".device.name")[0]
        print("INFO: using OCIODisplay for display: %s" % device)
        
        dpipeline = groupMemberOfType(group, groupName)
        commands.setStringProperty(dpipeline + ".pipeline.nodes", pipeline, True)

        pipeNodes = commands.nodesInGroup(dpipeline)
        pipeNodes.sort()
        for index,pNode in enumerate(pipelineList):
            stageOCIO = pipeNodes[index]
            try:
                applyProps(stageOCIO, pNode["context"], pNode["properties"])
            except KeyError, inst:
                print("ERROR: Unable to apply properties to %s: %s" % (stageOCIO, inst))

        self.usingOCIOForDisplay[group] = True
        commands.redraw()

    def disableDisplayOCIO(self, group):
        '''
        This reverts the DisplayGroup's display pipeline back to using
        RV's native RVDisplayColor node.
        '''
        
        groupName = "RVDisplayPipelineGroup"
        dpipeline = groupMemberOfType(group, groupName)
        nodesProp = dpipeline + ".pipeline.nodes"
        current = commands.getStringProperty(nodesProp)

        if not DEFAULT_PIPE.has_key(groupName) or current == DEFAULT_PIPE[groupName]:
            return

        commands.setStringProperty(dpipeline + ".pipeline.nodes", DEFAULT_PIPE[groupName], True)

        device = commands.getStringProperty(group + ".device.name")[0]
        print("INFO: using RVDisplayColor for display: %s" % device)

        self.usingOCIOForDisplay[group] = False
        commands.redraw()

    def sourceSetup (self, event):
        '''
        This function should be bound to the "source-group-complete" event. It
        will attempt to use OCIO to infer the incoming file space. If
        it succeeds, the OCIOFile node of the source group is
        activated and used to convert to the ROLE_SCENE_LINEAR space.
        '''

        event.reject() # don't eat this event -- allow others to get it too

        args        = event.contents().split(";;")
        group       = args[0]
        fileSource  = groupMemberOfType(group, "RVFileSource")
        imageSource = groupMemberOfType(group, "RVImageSource")
        source      = fileSource if imageSource == None else imageSource

        for nodeType in OCIO_ROLES.keys():
            self.useSourceOCIO(source, nodeType)

        #
        #   If this is the first OCIO color pipeline for a source assume
        #   that we also want to use OCIO for display. In this case we're
        #   just going to assume the defaults
        #

        if (len(commands.nodesOfType("OCIOFile")) == 1):
            for group in commands.nodesOfType("RVDisplayGroup"):
                if not self.usingOCIOForDisplay.get(group, False):
                    self.useDisplayOCIO(group)

    def beforeSessionRead(self, event):
        event.reject()
        self.readingSession = True

    def afterSessionRead(self, event):
        event.reject()
        self.readingSession = False
        if (len(commands.nodesOfType("OCIOFile")) > 1):
            for group in commands.nodesOfType("RVDisplayGroup"):
                if not self.usingOCIOForDisplay.get(group, False):
                    self.useDisplayOCIO(group)

    def ocioActiveEvent(self, nodeType):
        def F (event):
            if nodeType not in ["OCIOFile", "OCIOLook"]:
                if isOCIODisplayManaged(nodeType)() == commands.CheckedMenuState:
                    self.disableDisplayOCIO(nodeType)
                else:
                    self.useDisplayOCIO(nodeType)
                return

            evalInfo = commands.metaEvaluateClosestByType(commands.frame(), "RVFileSource", None)
            if len(evalInfo) == 0:
                evalInfo = commands.metaEvaluateClosestByType(commands.frame(), "RVImageSource", None)
            if len(evalInfo) == 0:
                return
            source = evalInfo[0]["node"]

            if isOCIOManaged(nodeType)() == commands.CheckedMenuState:
                self.disableSourceOCIO(source, nodeType)
            else:
                self.useSourceOCIO(source, nodeType, OCIO_DEFAULTS[nodeType])
        return F

    def checkForDisplayGroup (self, event):
        event.reject()
        try:
            node = event.contents()
            if (commands.nodeType(node) == "RVDisplayGroup"):
                self.usingOCIOForDisplay[node] = False
                commands.defineModeMenu("OCIO Source Setup", self.buildOCIOMenu(), True)
        except Exception,inst:
            print(str(inst),node)

    def maybeUpdateViews (self, event):
        event.reject()
        if event.contents().endswith("ocio_display.display"):
            commands.defineModeMenu("OCIO Source Setup", self.buildOCIOMenu(), True)

    def selectConfig(self, event):
        try:
            config = commands.openFileDialog(True, False, False, "ocio|OCIO Config", None)[0]
            self.config = OCIO.Config.CreateFromFile(config)
            OCIO.SetCurrentConfig(self.config)
            commands.defineModeMenu("OCIO Source Setup", self.buildOCIOMenu(), True)
        except Exception,inst:
            print(inst)

    def buildOCIOMenu(self):
        #
        #   Try to acquire OCIO config to populate the display menu
        #

        if self.config == None:
            try:
                self.config = ocio_config_from_media(None, None)
                OCIO.SetCurrentConfig(self.config)
            except Exception, inst:
                return [("OCIO", [("Choose Config...", self.selectConfig, "", None)])]

        #
        #   Make a unique entry for each device's display group
        #

        daList = []
        for display in commands.nodesOfType("RVDisplayGroup"):
            dList = [("Active", self.ocioActiveEvent(display), '', isOCIODisplayManaged(display)), ("_", None)]
            for d in self.config.getDisplays():
                vList = []
                for v in self.config.getViews(d):
                    vList.append( (v, ocioDisplayEvent(display, d, v),
                                   '', ocioDisplayMenuCheck(display, d, v)) )
                dList.append((d, vList))
            device = "  " + commands.getStringProperty(display + ".device.name")[0]
            daList.append((device, dList))

        #
        #   Apply file space changes only to the visible source
        #

        cssList = [("Active", self.ocioActiveEvent("OCIOFile"), '', isOCIOManaged("OCIOFile")), ("_", None)]
        csaList = []

        def addPath(family, tree):
            for f in family:
                for t in tree:
                    if f in t:
                        return addPath(family[1:],t)
                tree.append([f])
                return addPath(family, tree)

        families = [(cs.getFamily().split('/') + [cs.getName()]) for cs in self.config.getColorSpaces()]
        root = []
        for family in families:
            addPath(family, root)

        def addMenu(root, isSingle):
            if len(root) == 1:
                name = root[0]
                if isSingle:
                    OCIO_DEFAULTS.setdefault("OCIOFile", name)
                    return [(name, ocioEvent("OCIOFile", "ocio.inColorSpace", name),
                           '', ocioMenuCheck("OCIOFile", "ocio.inColorSpace", name))]
                else:
                    return [(name, ocioEventOnAllOfType("OCIOFile", "ocio.inColorSpace", name),
                           '', ocioMenuCheck("OCIOFile", "ocio.inColorSpace", name))]
            else:
                menu = []
                for r in root[1:]:
                    menu += addMenu(r, isSingle)
                return [(root[0], menu)]

        for r in (root):
            cssList += addMenu(r, True)
            csaList += addMenu(r, False)

        #
        #   Apply file look changes only to the visible source
        #

        lsList = [("Active", self.ocioActiveEvent("OCIOLook"), '', isOCIOManaged("OCIOLook")), ("_", None)]
        laList = []
        for l in self.config.getLooks():
            OCIO_DEFAULTS.setdefault("OCIOLook", l.getName())
            lsList.append( (l.getName(),
                            ocioEvent("OCIOLook", "ocio_look.look", l.getName()),
                            '',
                            ocioMenuCheck("OCIOLook", "ocio_look.look", l.getName())) )
            laList.append( (l.getName(),
                            ocioEventOnAllOfType("OCIOLook", "ocio_look.look", l.getName()),
                            '',
                            ocioMenuCheck("OCIOLook", "ocio_look.look", l.getName())) )
        
        final = [("Current Source", None, "", lambda: commands.DisabledMenuState),
                 ("  File Color Space", cssList)]
        if len(lsList) > 2:
            final += [("  Look", lsList)]
        final += [("All Sources", None, "", lambda: commands.DisabledMenuState),
                 ("  File Color Space", csaList)]
        if len(laList) > 0:
            final += [("  Look", laList)]
        final += [("_", None),
                ("Displays", None, "", lambda: commands.DisabledMenuState)]
        final += daList

        return [("OCIO", final)]

    def __init__(self): 
        rvtypes.MinorMode.__init__(self)

        self.usingOCIOForDisplay = {}
        self.readingSession = False
        self.config = None
        
        #
        #   Look for an implementation of the OCIOHelper on the PATH.
        #   Use the default if the import failed.
        #

        try:
            import rv_ocio_setup
            inherited = []
            for method in METHODS:
                try:
                    exec("global %s; %s = rv_ocio_setup.%s" % (method, method, method))
                    inherited.append(method)
                except AttributeError:
                    pass

            print("INFO: Using %s for OCIO setup methods: %s" %
                    (rv_ocio_setup.__file__, " ".join(inherited)))

        except ImportError:
            pass

        self.init("OCIO Source Setup", None,
                  [("source-group-complete", self.sourceSetup, "Color and Geometry Management"),
                   ("before-session-read", self.beforeSessionRead, ""),
                   ("after-session-read", self.afterSessionRead, ""),
                   ("graph-new-node", self.checkForDisplayGroup, ""),
                   ("graph-node-inputs-changed", self.checkForDisplayGroup, ""),
                   ("graph-state-change", self.maybeUpdateViews, "") ],
                  self.buildOCIOMenu(),
                  "source_setup", 10) # source_setup key used by source_setup and this mode

#
#   Dynamically looked up by the mode manager to create this mode. The name
#   matters
#

def createMode():
    return OCIOSourceSetupMode()

Hi,
I’m currently running into the same problem with our customized package.

I narrowed the problem down to the fact, that in the participants review session, the
graph-node-inputs-changed event happens before the source-group-complete, whereas it is usually the other way round.

I didn’t look all the way through your code, but this may give you a starting point.

You could have a look at the after-session-read event, that is triggered when everything else is done and update your config then.

Cheers and Good Luck
Henna

Thanks for the input Henna