Skip to main content

RV - STUDIO WIDE CUSTOMIZATION

Tweak software, Shotgun software and Autodesk. Lot of things have happened, but RV is still one of my favourite softwares, and it have been for the past 10+ years. When it comes to customization you have a lot of possibilities with Python and RV's Mu language. The Mu API documentation is only available inside RV and unfortionatly we don't have a Python API documentation (yet)... but it is pretty similar to Mu's :) Below are some notes by our pipeline TD; Emil Gunnarsson that hopefully can be usefull and save some time for someone. At the bottom he lists some useful links.

STUDIO WIDE CONFIGURATION

Environment Variables:
At Dupp we use RV_PREFS_CLOBBER_PATH and RV_SUPPORT_PATH and they are pointing to the same network directory.

  • RV_PREFS_CLOBBER_PATH
    Overrides all user preferenses and only uses studio wide preferences. (RV.ini on Windows)
  • RV_PREFS_OVERRIDE_PATH
    Used to set default preferences, but users can make local overrides. (RV.ini on Windows)
  • RV_SUPPORT_PATH
    Sets RV's script path. This is where you place studio wide stuff. Typical folder structure is shown on the right.
CREATING A SIMPLE PACKAGE
  • To import rv in python you should set the PYTONPATH=C:\Program Files\Shotgun\RV-2021.0.0\plugins\Python environment variable on your machine.
  • Start by creating a python file, for example myMenu.py
  • The following code will create a menu and 2 menuitems, and add some functionality to the buttons.
from rv.rvtypes import *

# These two are only available in the RV Runtime Environment
from rv.commands import *
from rv.extra_commands import *

class DuppMenuMode(MinorMode):
    def __init__(self):
        MinorMode.__init__(self)

        # Edit the existing list menu, and add a new dropdown to it, with 2 items
        self.init(
            "py-duppmenu-mode",
            None,
            None,
            [ 
                # Menu name 
                # NOTE: If it already exists it will merge with existing, and add submenus / menuitems to the existing one
                ("-= DUPP =-",
                    [   
                        # Menuitem name, actionHook (event), key, stateHook
                        ("Print to console", self.printToConsole, None, None),
                        ("Show console", self.showConsole, None, None)
                    ] 
                )
            ] 
        )

        # Open the console if it is not already open
        if not isConsoleVisible():
            showConsole()

    def printToConsole(self, event):
        print("Printing information to the console: ")
        print(getCurrentAttributes())

    def showConsole(self, event):
        print("Showing console!")

        if not isConsoleVisible():
            showConsole()

def createMode():
    # This function will automatically be called by RV
    # to initialize the Class
    return DuppMenuMode()
  • Next you have to create a PACKAGE file, this being a file named PACKAGE with no extension at all. Below is a basic template for a PACKAGE file. I specified 3.6 as the RV version because it's an older version and should work with most newer ones.
package: DuppMenu
author: Emil Gunnarsson
contact: This email address is being protected from spambots. You need JavaScript enabled to view it.
version: 1.0
rv: 3.6
requires: ''

modes:
  - file: myMenu
    load: immediate

description: A DuppMenu that allows you to communincate with the Ingrid Python API
  • Create the package, to do so you have to select both of the files you've created and zip them, name it something like duppmenu-1.0.rvpkg. It's important to add the version number -1.0 and the extension .rvpkg in order for it to show up when you want to add it to RV.
  • Now you can open RV and go to RV > Preferences > Packages > Add Packages..
    Open your .rvpkg file.
  • Restart RV and you should be able to see your menu.
FUNCTIONS TO KNOW ABOUT
  • commands
    • commands.getCurrentAttributes() Gives some generic information about the current input, filename & path, timecode, colorcode, etc. It can be helpful to convert this Tuple of Tuples into a Dictonary using something like attributes = dict(commands.getCurrentAttributes())
    • commands.readSetting() Reading settings
    • commands.writeSetting(string group, string name, SettingsValue value) Writing settings
    • commands.isConsoleVisible() Returns a boolean which lets you know if the console is visible or not
    • commands.showConsole() Shows the RV Python console
    • comands.addSource() Add a source to the timeline
    • commands.setSourceMedia() Add an array of sources to the timeline
    • commands.relocateSource() Switch the current source with a new one
    • commands.sources() Get a list of current sources in the timeline
    • commands.bind() Bind a function to an event
  • extra_commands
    • extra_commands.displayFeedback() Print some feedback in the upper left corner
  • rv.qtutils
    • rv.qtutils.sessionWindow() Gets the RV MainWindow frame as PySide object, so that you can change the window title, dock widgets, etc.
GETTING THE COLORSPACE FOR THE FIRST SOURCE
def getColorSpace(self):
        colorspace = "linear"

        # Return Nuke-style colorspaces
        # Property types: 1 = float, 2 = int, 8 = string
        # int, int, int, float
        # Gives a list of 1 element so we use [0] to get the first one (the value)
        # We are getting the values for logtype, sRGB2Linear, Rec709ToLinear & Gamma
        nLogtype = commands.getIntProperty("sourceGroup000000_tolinPipeline_0.color.logtype")[0]
        nRGB2Lin = commands.getIntProperty("sourceGroup000000_tolinPipeline_0.color.sRGB2linear")[0]
        nRec2Lin = commands.getIntProperty("sourceGroup000000_tolinPipeline_0.color.Rec709ToLinear")[0]
        fGamma22 = round(commands.getFloatProperty("sourceGroup000000_tolinPipeline_0.color.fileGamma")[0], 1)

        if fGamma22 == 2.2:
            colorspace = "Gamma2.2"
        elif nLogtype == 1:
            colorspace = "Cineon"
        elif nLogtype == 2:
            colorspace = "ViperLog"
        elif nLogtype == 3 or nLogtype == 4:
            colorspace = "AlexaV3LogC"
        elif nLogtype == 6 or nLogtype == 7:
            colorspace = "REDLog"
        elif nLogtype == 0 and nRGB2Lin == 1:
            colorspace = "sRGB"
        elif nLogtype == 0 and nRec2Lin == 1:
            colorspace = "rec709"

        return colorspace
SIMPLE MESSAGE BOX
def displayMessageBox(self, title, message, messageType = QMessageBox.Ok):

        # Create a simple message dialog
        msg = QMessageBox()
        msg.setIcon(QMessageBox.Information)
        msg.setText(message)
        msg.setWindowTitle(title)
        msg.setStandardButtons(messageType)
        msg.exec_()

        # # Additional useful methods
        # msg.setInformativeText("This is additional information")
        # msg.setDetailedText("The details are as follows:")
        # msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)

        # QMessageBox.Ok 0x00000400
        # QMessageBox.Open 0x00002000
        # QMessageBox.Save 0x00000800
        # QMessageBox.Cancel 0x00400000
        # QMessageBox.Close 0x00200000
        # QMessageBox.Yes 0x00004000
        # QMessageBox.No 0x00010000
        # QMessageBox.Abort 0x00040000
        # QMessageBox.Retry 0x00080000
        # QMessageBox.Ignore 0x00100000

        # # The method should take an index for the buttons
        # # EXAMPLE: def buttonClicked(self, index):
        # msg.buttonClicked.connect(self.buttonClicked)

        # # Get return value from the button clicked
        # retval = msg.exec_()
        # print("value of pressed message box button: ", retval)
BINDING FUNCTIONS TO EVENTS
# new-source, graph-state-change, after-progressive-loading, media-relocated
    commands.bind("default", "global", "media-relocated", self.mediaRelocated, "Doc string")
    commands.bind("default", "global", "graph-state-change", self.testEvent, "Doc string")
    # Events

    def mediaRelocated(self, event):
        self.hasBeenRelocated = True
        print("Media relocated...")

    def testEvent(self, event):
        print("another event")
PRINTING NODE INFORMATION
nodeTypes = ['RVSequence', 'RVStack', 'RVSwitch']

    for nodeType in nodeTypes:
        print("INFO: Checking nodes of type \""+nodeType+"\"")
        nodes = rv.commands.nodesOfType(nodeType)
        counter = 0

        for node in nodes:
            props = commands.properties(node)
            length = len(nodes)     

            for prop in props:
                propertyType = commands.propertyInfo(prop)['type']

                # I know there is some type called "halfproperty" but i havent come across it yet

                if propertyType == 1:
                    print("Float property value ("+ prop +"): ", commands.getFloatProperty(prop))
                elif propertyType == 2:
                    print("Integer property value: ("+ prop +")", commands.getIntProperty(prop))
                elif propertyType == 8: 
                    print("String property value: ("+ prop +")", commands.getStringProperty(prop))
                else:
                    print("PROPERTY TYPE: ", propertyType)

            if length > 1 and counter != (length - 1):
                print("------------------------------------------------------------------------------------------")
            counter += 1

        print("=============================================")
DOCKED WIDGET IN RV
import sys
sys.path.append("L:\\_DUPP_PIPE\\app_launcher\\DuppAppLauncher\\Code")

from rv.qtutils import *
from rv.rvtypes import *

import duppmenu

# These two are only available during the RV Runtime Environment
from rv.commands import *
from rv.extra_commands import *

from PySide.QtGui import *
from PySide.QtCore import *
from PySide.QtUiTools import QUiLoader

from Ingrid import IngridAPI

class DuppMenuMode(MinorMode):
    def __init__(self):
        MinorMode.__init__(self)      
        self.init("py-duppmenu-mode", None, None, [("-= DUPP =-",[("Publish", self.publish, None, None)])])

        # Variables
        self.comboBoxLayoutCounter = 0
        self.currentFolderIds = {'f_id': None, 'f_parent_id': None}
        self.isSpecialCase = False

        # Ingrid
        self.ingrid = IngridAPI()

        # GUI
        # Load custom Widget to place on top of the Docked Widget
        # This is simply a new .ui file which was created as a "Widget" (Not a MainWindow), then simply add a Frame to it and layout it
        # Add your desired widgets and find them using .findChild(QObject, "identifier")
        loader = QUiLoader()
        ui_file = QFile(os.path.join(self.supportPath(duppmenu, "duppmenu"), "CustomDockWidget.ui"))
        ui_file.open(QFile.ReadOnly)
        self.customDockWidget = loader.load(ui_file)
        ui_file.close()

        # Target custom widgets
        self.comboBox_0 = self.customDockWidget.findChild(QComboBox, "comboBox_0")
        self.comboBoxLayout = self.customDockWidget.findChild(QVBoxLayout, "comboBoxLayout")

        # Event handlers
        self.comboBox_0.currentIndexChanged.connect(lambda: self.onComboBoxIndexChanged(self.comboBox_0))

        # Gets the current RV session windows as a PySide QMainWindow.
        self.rvWindow = rv.qtutils.sessionWindow()
        
        # Create DockWidget and add the Custom Widget to it
        self.dockWidget = QDockWidget("Publish", self.rvWindow)
        self.dockWidget.setWidget(self.customDockWidget)

        # Dock widget to the RV MainWindow
        self.rvWindow.addDockWidget(Qt.RightDockWidgetArea, self.dockWidget)