Director Plug Ins


Plug-Ins are small portions of custom code that can be loaded into and used by Ventuz Director to extend existing functionality. Instead of using some arcane scripting language, the Ventuz development team decided to use the standard Visual Studio tool chain. As a result:

Developing a plug-in requires knowledge of C# or Visual Basic, WPF (Windows Presentation Foundation) as well as the Director Plug-In API. However, for an end user, installing a plug-in is as easy as copying a DLL into the right folder.

Managing Plug-Ins

To enable a plug-in's functionality, open the show it should be used in and go to the Plug-ins Manager which can be found in the main menu under Show / Plug-Ins .... Set the check mark next to all plug-ins that should automatically be loaded whenever this show is opened.

In the image below, there have been two plug-ins installed but only the first is activated for the show. The button to the right of the name shows that this plug-in can be configured. By pressing it, the plug-in's configuration dialog opens.


The information which plug-ins should be enabled and their respective configuration settings are stored in the Director show file. To use a common set of settings for all shows based on a project, an operator can embed default settings into the Ventuz Project file via Show / Project / Show Defaults... in the main menu. Those will be used as defaults for each show that is newly created for this project.


Development Environment

The main requirements for developing a Director plug-in are:

The Visual Studio Extension file provides a project template for creating Director plug-ins that will automatically setup everything required to build and deploy a plug-in. Inside the Windows Explorer, double-click the vsix file to install the extension. Since most users will not be developing plug-ins, this is not automatically done by the Ventuz installer but has to be done manually. Open Visual Studio and under Tools/Extensions and Updates verify that the extension has correctly been installed.

Visual Studio Extensions are only supported by Visual Studio Professional or higher licenses, not Visual Studio Express! It is possible to develop Director plug-ins without the extension, only the task of setting up the project is then left to be done manually by the user.


A new version of the Ventuz Visual Studio Extension is shipped as part of the Ventuz installation whenever plug-in interfaces are added or changed. Make sure to remove any old versions and re-install the latest version when upgrading your Ventuz installation.


Creating a Project

To start a new plug-in, select File/New/Project... from the Visual Studio main menu. If the extension is correctly installed, a new entry Director Plug-In will be available in the Visual C# section and in the Visual Basic section. (This text will choose C# as programming language.) Select this entry, choose a name and location for your plug-in project and press OK. Next, the Ventuz Director Plugin Settings dialog will appear.


Display name and description of a plug-in are end-user information that will be used inside Director, for example in the plug-in manager. They have no relevance for the functionality of the plug-in but make it easier for an end-user to identify and use a plug-in. The set of controls below is for specifying what capabilities the plug-in will support. For this example, create a plug-in that provides one new window type. Finally, to test a plug-in, a Director instance is required. The extension will auto-detect the latest Ventuz installation and use that application path. If for some reason the auto-detection fails or the plug-in should be developed against a specific Director version, the appropriate Director Path can be entered in the first text box.

When pressing OK, Visual Studio will create a skeleton implementation of the plug-in. In our case, it contains the following files:

This is already a fully functional Director plug-in although it does not do anything besides showing an empty control. Before filling it with functionality, we will take a look at debugging a plug-in.

Debugging

Due to the Visual Studio Extension, debugging a Director plug-in is exactly the same as debugging any other c# program. Just hit F5 to launch the debugger. Behind the scenes, Visual Studio will launch the Director installation specified in the plug in settings dialog and inject the plug-in such that it can be used without copying it into your installation.

Set a breakpoint into the first line of

Initialize(IDirectorApplication application)

in the file Plugin.cs by setting the cursor on that line and pressing F9. Inside Director, either create a new or open an existing show. To activate the plug-in, go to the plug-in manager via Show / Plug-Ins... in the main menu and click on the check box next to the plug-ins name. Visual Studio will come to the front as the breakpoint will be hit. To actually create an instance of the new window type, go to the window manager in the main menu and add a new window. The list of available window types should now contain the plug-in window type.

Interfaces and Libraries

A Director plug-in is a DLL that contains a class which conforms to the IPlugin interface defined in Director.Shared.dll. When Director tries to allocate a plug-in instance, it calls the Initialize method on that interface. The application parameter of this method contains an instance of the IDirectorApplication interface which is the plug-ins entry point to access Director functionality. Any operation the plug-in wants to perform inside Director is done via this interface.

[Export(typeof(IPlugin))]
[ExportMetadata("Name", "DirectorWindowExample")]
[ExportMetadata("DisplayName", "DirectorWindowExample")]
[ExportMetadata("Description", "Ventuz Director Plugin for a new example window type.")]
public class Plugin : IPlugin
{
    private IDirectorApplication _application;
        
    public IDirectorApplication Application
    {
        get
        {
            return this._application;
        }
    }
 
    public IReadOnlyList<PluginWindowDescriptor> SupportedWindowTypes
    {
        get
        {
            List<PluginWindowDescriptor> result = new List<PluginWindowDescriptor>();
            result.Add(new PluginWindowDescriptor("windowType1", "Type 1", "Type 1 of DirectorWindowExample", typeof(WindowType1)));
            return result;
        }
    }

    public void Initialize(IDirectorApplication application)
    {
        this._application = application;
    }

    public void Dispose()
    {
    }
}

For Director to find the plug-in, the class can implement IPlugin or any derived interface. However, it always has to export the IPlugin interface, exporting just a derived interface will not work!


The project template stores a reference to that interface for later usage. Via this interface, various part of Director can be accessed such as the channels in the currently active show.

Configurable Plug-Ins

A plug-in that is configurable can implement IConfigurablePlugin. Director will call the plug-in's SaveConfiguration() method whenever the show is saved and call LoadConfiguration(...) when the show is re-opened. If the plug-in returns a non-null value for ConfigurationDialogContent, a configure button will be shown next to the plug-in entry in the plug-in manager as seen earlier. When it is pressed, a modal dialog will be presented with the content provided by IPluginConfigurationDialog. This allows the plug-in to show a customized configuration dialog and let Director take care of the reset.

public interface IConfigurablePlugin : IPlugin
{
    void LoadConfiguration(Dictionary<string, string> configuration);
    Dictionary<string, string> SaveConfiguration();
    IPluginConfigurationDialog ConfigurationDialogContent { get; }
}

Channel Rule Provider

The IChannelRuleProvider interface can be used to add new channel rule conditions and actions to Director. Channel rules are applied when the operator cues new content and used to block certain content or modify its data. A custom written action can for example be used to apply a data base lookup and fill in values whenever a page is cued.

Shot Box Extender

The IShotboxExtender interface is used to add new entries to the Shot Box. When such an item is selected by the operator, the plug-in can either return a URI and have Director use its own resolve mechanisms or return null and perform some custom actions by itself using the IDirectorApplication interface.

Window Provider

The IWindowProvider interface is probably the most commonly used interface. By implementing it, a plug-in can add one or more new window types to the list of available windows in Director.

The older IWindowPlugin interface is now obsolete and will be removed in a future version. It should no longer be used because it can only provide a single type of window and plug-ins of this type are loaded/unloaded every time the window layout changes (which makes managing resources like network ports, files or similar difficult at best).


Accessing Director Functionality

A major portion of a plug-in is of course the interaction of the plug-in with build-in Director functionality like cueing a template or performing a take. As shown in the previous code example, this is achieved via the IDirectorApplication interface that a plug-in is passed as part of the initialize method signature. IDirectorApplication represents the application as a whole and most importantly gives access to the active show in the form of IDirectorShow. The show in turn gives access to the channels in a show, a collection of IChannel instances. The way the interface hierarchy is build directly reflects Director's internal software architecture and should be familiar to anyone who has used Director, although currently only a subset of functionality is exposed.


Example: Cueing a Template

As an example for a simple, functional plug-in, let's extend the Director Window Plug-In created above and add two buttons to the window, one for cueing a specific template and one for taking the template.

User Interface

Director uses Microsoft's Windows Presentation Foundation (WPF) framework to build its user interface. The advantage of using such a standard framework as opposed to a customized scripting language is that there is a great abundance of tutorials, books and other learning material readily available on the net and the resulting large number of developers that are capable of writing WPF applications. For someone only interested in developing Director Plug-Ins though, the shear amount of possibilities can be a bit daunting. However, it's quite easy to achieve first results:

To create the user interface for our plug-in, open WindowType1.xaml and replace the Grid-tags with

<StackPanel>
    <Button Content="Cue" Click="Cue"/>
    <Button Content="Take" Click="Take"/>
</StackPanel>

Those are two WPF-buttons inside a stackpanel that will align the buttons vertically inside the plug-in window. The Content property specifies what is shown inside the button (i.e. the label) and the Click property associates each button with a method in the UserControl's code behind.

The typical WPF approach to add functionality to a button is by using commands. This is a slightly more complex approach and omitted here in order to keep this example as simple as possible. Commands also manage disabling a button if the command cannot be executed, for example disabling the take button when nothing is cued.


Inside WindowType1.cs, we'll find various method stubs. Since this plug-in does not need to store any window layout settings or perform initialization code, the generated method stubs can be left as is. We'll just add the two methods for our button functionality:

public void Cue(object sender, EventArgs args )
{

}

public void Take(object sender, EventArgs args )
{

}

We will also need access to Director functionality. Add a property to store the value that was passed to the window in the initialization:

Plugin Plugin { get; set; }
        
public void Initialize(IPlugin plugin)
{
    this.Plugin = plugin as Plugin; 
}

Retrieving a Template

In order to cue a template, we have to retrieve it from the set of available templates and then tell Director where to cue it. In this example, we'll use the Hockey demo project that ships with Director. It contains a template called NS1L that we want to cue whenever the operator clicks our cue button. As shown in the diagram above, the templates can be accessed by using the active show to get the ITemplates interface.

The quickest way find out what's accessible via the plug-in API is to double-click on the Ventuz.Director.Shared entry under References in the Solution Explorer. This opens the Object Browser window where one can navigate the various interfaces and their methods.


ITemplates implements a method that retrieves a template by its name (the internal name the author of the template gave the template state which may or may not differ from the end-user name shown in Director). To retrieve the template called "NS1L", we go through the plugin interface as follows:

Ventuz.Remoting4.Templates.Template template = this.Plugin.Application.Show.Templates.GetTemplate("NS1L");

Creating an Instance

Once we have the Template, we have to create an instance of that template and the corresponding data. This is done with the DirectorTemplateData class. Besides the template itself, it also contains a Data object. This is a special kind of object called a Dynamic Object as its Properties are generated only the fly during program execution. For example, to set the template parameter FirstLine, one simply writes:

DirectorTemplateData templateData = new DirectorTemplateData(template);
templateData.Data.FirstLine = "Test";

Visual Studio will not know at compile time whether or not the dynamic object actually has such a member as for that it would have to analyze the template. When accessing a member which is not part of the Template's DataModel, an exception will occur which might even crash the whole application! Make sure to use try/catch-blocks and watch out for unexpected exceptions.


Cue & Take

Now that the template instance is ready, we have to cue it to a channel. Again, we access the channel via the active show. In this example, we'll hard code to always use the first channel. The respective call is

this.Plugin.Application.Show.Channels[0].CueAsync(templateData);

One could also implement the window using IPluginWindow<IChannel> to make this window a channel-based window. In that case, Director would allow the end-user to configure the channel the window should use via its window configuration manager. The window implementation could then use DirectorDataContext to access the configured channel.


Implementing the take button is even simpler. Again we go via the IPlugin interface from the application level down to the channel and then tell it to do a take.

this.Plugin.Application.Show.Channels[0].TakeAsync();

Similar to CueAsync, the IChannel interface also contains the method UncueAsync which can be used to remove parts or the whole TemplateData from the currently cued item.

Asset URIs

If a template can use an image, movie or similar asset, the asset itself is not stored inside DirectorTemplateData. Instead, the template instance will only store a URI to reduce overhead when sending the template instance to remote machines during cueing. This becomes obvious when cueing a movie which happens almost instantaneously as the movie file itself is already present on the remote machine.

Director uses a set of pre-defined URI schemes that the Ventuz runtime will also understand. The most important ones in the context of plug-in development are:

Schema Description
ventuz:// A relative path that starts at the root of the Ventuz project folder structure.
assets:// A relative path that starts at the asset folder within the Ventuz project folder structure.

As an example, the A.Hilton.png headshot image from the Hockey demo project can be accessed by either ventuz://assets/headshots/A.Hilton.png or assets://headshots/A.Hilton.png. So if a template contains the template parameter Image, it would be set by the following code:

DirectorTemplateData templateData = new DirectorTemplateData(template);
templateData.Data.Image= new Uri("assets://headshots/A.Hilton.png");

Asynchronous Programming

Director is a multi-threaded application that places great emphasis on the fact that the application and its user interface should always be responsive, no matter what functionality is executed. But what happens when the user clicks one of the buttons in the plug-in? The main WPF UI thread processes the click and calls the plug-in method, preventing any UI updates until that method is done. This can easily freeze the user interface for a couple of seconds depending on how much work is done inside the method.

To prevent this, Director internally makes heavy use of C# Task objects and the Async framework. The latter allows the compiler to split a method into multiple parts and at certain points of the execution return to the caller (i.e. let the UI do other work) and resume at the same position later on.

Any method that wants to use that mechanism has to have the async keyword in its declaration. As a convention, one usually adds the suffix Async to the method name to visualize to the caller that this method may return prematurely. To prevent the user from clicking the button again once its operation has started, it is also a good idea to disable it.

public async void CueAsync(object sender, EventArgs args )
{
    this.IsEnabled = false;

    ...

    this.IsEnabled = true;
}

public async void TakeAsync(object sender, EventArgs args )
{
    this.IsEnabled = false;

    ...

    this.IsEnabled = true;
}

Just adding the async does not change the methods' behavior yet. The compiler needs to be told at what point it can safely jump out of the method with the await keyword. An await can be done on any Task object and the majority of potentially long running methods exposed in the Director SDK return a Task object. We want to await on the cueing and the taking as those will be the most time-consuming operations.

So the final implementation looks like this:

public async void CueAsync(object sender, EventArgs args )
{
    this.IsEnabled = false;
    
    Template template = plugin.Application.Show.Templates.GetTemplate("NS1L");
    DirectorTemplateData templateData = new DirectorTemplateData(template);
    templateData.Data.FirstLine = "Test";

    ITimelineItem timelineItem;
    try
    {
        timelineItem = await plugin.Application.Show.Channels[0].CueAsync(templateData);
    }
    finally
    {
        this.IsEnabled = true;
    }
}

public async void TakeAsync(object sender, EventArgs args)
{
    this.IsEnabled = false;

    try
    {
        await plugin.Application.Show.Channels[0].TakeAsync();
    }
    finally
    {
        this.IsEnabled = true;
    }
}

For the cue, retrieving the template and creating the template data should be reasonably fast so we do that before awaiting. Once the cueing is done and we have been returned the ITimelineItem instance, we might use it to present some further information to the user but that has been omitted for brevity. Finally, we re-enable the button. For the take, the plug-in control is disabled, the task is performed asynchronously and once it has finished, the control is re-enabled.

Modifying Project Data

Another common use case is developing a custom control panel that should modify project data (e.g. goals scored in a match). We'll use this to give an example on how to create WPF binding between a UI element and its respective data object. Let's assume we have a color each for the home and guests team that is part of the project data. How do we achieve something like shown in the following screenshot?


This example contains a number of standard WPF mechanisms like bindings, resources, converters, and using components from other DLLs. It is beyond the scope of this document to give the reader even a concise overview over WPF functionality and therefore this is only intended as something to follow through while using some other form to learn WPF!


Lets add a second window type to our plug-in. In the Visual Studio Solution Explorer, right-click and add a new WPF UserControl called ProjectDataWindow. In Plugin.cs, add another line in SupportedWindowTypes for the new window type:

public IReadOnlyList<PluginWindowDescriptor> SupportedWindowTypes
{
    get
    {
        List<PluginWindowDescriptor> result = new List<PluginWindowDescriptor>();
        result.Add(new PluginWindowDescriptor("windowType1", "Type 1", "Type 1 of DirectorWindowExample", typeof(WindowType1)));
        result.Add(new PluginWindowDescriptor("projectData", "Project data Window", "Access team colors", typeof(ProjectDataWindow)));
        return result;
    }
}

In ProjectDataWindow.xaml.cs, have the class implement the interface IPluginWindow:

using System.Collections.Generic;
using System.Windows.Controls;
using Ventuz.Director.Shared;

namespace DirectorWindowExample
{
    /// <summary>
    /// Interaction logic for ProjectData.xaml
    /// </summary>
    public partial class ProjectDataWindow : UserControl, IPluginWindow
    {
        public ProjectDataWindow()
        {
            InitializeComponent();
        }

        public System.Windows.FrameworkElement PopupContent
        {
            get
            {
                return null;
            }
        }
        
        public System.Windows.FrameworkElement WindowContent
        {
            get
            {
                return this;
            }
        }
        
        public void Dispose()
        {
        }
        
        public void Initialize(IPlugin plugin)
        {
            // Setup our plug-in as data source for WPF bindings.
            this.DataContext = plugin;
        }
        
        public void LoadLayoutSettings(Dictionary<string, string> settings)
        {
        }
        
        public Dictionary<string, string> SaveLayoutSettings()
        {
            return null;
        }
    }
}

The plug-in now provides two window types, the window containing the cue/take buttons and another that we will implement in the following. No code behind functionality will be needed as an alternative mechanism will be used to connect the user interface to the actual values: WPF bindings. For the following to work, we have to set the WPF control's DataContext to our plug-in which is done during initialization.

User Interface

Inside ProjectDataWindow.xaml, add:

The row and column definitions of the grid either specify a fixed height/wide (i.e. 75), auto (take exactly as much space as the content in that cell needs) or * (take as much space as possible). Each of the controls inside the Grid use the Grid.Row and Grid.Column Attached Properties to specify what goes into which cell of the Grid.

However, WPF does not come with a pre-build color picker, so we are going to use the exact same one as used in Director!

<UserControl x:Class="DirectorWindowExample.ProjectDataWindow"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:ventuzControls="clr-namespace:Ventuz.WPF.Controls;assembly=Ventuz.WPF"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="75"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="0" Text="Home" VerticalAlignment="Center"/>
        <ventuzControls:ColorPicker Grid.Row="0" Grid.Column="1" Height="30"/>
        <TextBlock Grid.Row="1" Grid.Column="0" Text="Guests" VerticalAlignment="Center"/>
        <ventuzControls:ColorPicker Grid.Row="1" Grid.Column="1" Height="30"/>
    </Grid>
</UserControl>

For this to work, we added a reference to the Ventuz.WPF.dll to the project by right-clicking on the References entry in the project's Solution Explorer (the DLL is part of your Director installation, just browse for it). Then we added an XML namespace so we can access the controls in the XAML.

xmlns:ventuzControls="clr-namespace:Ventuz.WPF.Controls;assembly=Ventuz.WPF"

Value Binding

Now we have two color pickers, but they do not do anything yet. We need to connect the value of the ColorPicker with the project data. First, we open Ventuz Designer and right-click on the icon for our project and choose Properties / Data / Project Data Edit / Project Data and look for where the colors are. In this case, their path is Home/HomeTeamColor and Visitor/VisitorTeamColor respectively.


We close the Ventuz Designer and continue inside Visual Studio. By double-clicking on the Ventuz.WPF reference in the Solution Explorer, the Object Browser opens up and we can see that the ColorPicker has a property named DisplayColor. To connect those two ends, we add a WPF Binding:

<ventuzControls:ColorPicker Grid.Row="0" Grid.Column="1" Height="30" DisplayColor="{Binding Application.Show.ProjectData.Data.Home.HomeTeamColor}"/>
<ventuzControls:ColorPicker Grid.Row="1" Grid.Column="1" Height="30" DisplayColor="{Binding Application.Show.ProjectData.Data.Visitor.VisitorTeamColor}"/>

For a binding to work, the DataContext of the control needs to be set properly and from there the path specified in the binding should lead to the desired property. By checking the code behind, we see that the DataContext is set to our plugin interface and from there we can again drill down to the property we want.

Converters

Unfortunately, this still does not work. When launching the debugger and opening our plug-in, the Output window will print the following lines:

System.Windows.Data Error: 1 : Cannot create default converter to perform 'two-way' conversions between types 'System.Drawing.Color' and 'System.Nullable`1[System.Windows.Media.Color]'. Consider using Converter property of Binding. BindingExpression:Path=Application.Show.ProjectData.Data.Home.HomeTeamColor; DataItem='Plugin' (HashCode=50714607); target element is 'ColorPicker' (Name=''); target property is 'DisplayColor' (type 'Nullable`1')
System.Windows.Data Error: 5 : Value produced by BindingExpression is not valid for target property.; Value='Color [A=255, R=25, G=25, B=254]' BindingExpression:Path=Application.Show.ProjectData.Data.Home.HomeTeamColor; DataItem='Plugin' (HashCode=50714607); target element is 'ColorPicker' (Name=''); target property is 'DisplayColor' (type 'Nullable`1')

In plain English, both ends don't match! We need a converter for this which luckily is also part of Ventuz.WPF.dll. We add another namespace:

xmlns:ventuzConverters="clr-namespace:Ventuz.WPF.Converters;assembly=Ventuz.WPF"

Then we add an instance of the RGBAToColorConverter as part of our UserControl's resources (usually added as the first thing right after the end of the opening UserControl tag).

<UserControl.Resources>
    <ventuzConverters:RGBAToColorConverter x:Key="colorConverter"/>
</UserControl.Resources>

And finally, we use the converter in our bindings:

<ventuzControls:ColorPicker Grid.Row="0" Grid.Column="1" Height="30" DisplayColor="{Binding Application.Show.ProjectData.Data.Home.HomeTeamColor, Converter={StaticResource colorConverter}}"/>
<ventuzControls:ColorPicker Grid.Row="1" Grid.Column="1" Height="30" DisplayColor="{Binding Application.Show.ProjectData.Data.Visitor.VisitorTeamColor, Converter={StaticResource colorConverter}}"/>

Now everything works as expected. When the plug-in window is opened, the color pickers will contain the current color set in the project data and when making any changes, they are reflected inside the presenter runtime.

Deployment

Installing a plug-in on a end-user machine is fairly straight forward. In the Ventuz installation directory, create a folder called Plugins. Copy the DLL output of the Visual Studio Director Plug-In project into that folder and restart Director. That's it!