Share via


Silverlight

Create a Custom Expression Encoder Publishing Plug-In

Laurence Moroney

This article discusses:
  • Expression Encoder plug-in basics
  • Building a boilerplate plug-in
  • Adding FTP upload functionality
  • Building, installing, and debugging
This article uses the following technologies:
WPF, Visual Studio, Expression Encoder

Contents

Expression Encoder and Plug-Ins
Creating a Plug-In
Building the FTP Upload Plug-In
Implementing the Publish Class
Saving Configuration Settings
Compile, Deploy, and Debug

A recently released tool, Microsoft® Expression® Encoder helps you encode, enhance, and publish rich media experiences that can be rendered using Microsoft SilverlightTM. Expression Encoder has an extensible plug-in architecture, giving you the ability to write plug-ins that allow for the publication of Expression Encoder output to different sources.

In this article, I will step through the building of a plug-in for Expression Encoder using Microsoft Visual Studio® 2008 Beta 2. This plug-in will fit within the standard workflow of an Expression Encoder session, and, at the end of an encoding session, it will upload the resulting output to an FTP server.

Expression Encoder and Plug-Ins

Figure 1 shows Expression Encoder. The left side of the workspace contains the video preview and a control bar that allows you to set the portion of the video to re-encode. On the right-hand side of the screen, there are three tabs: Settings, for configuring up the encoding session; Metadata, for applying new information to the media; and Output, for taking post-encode actions.

Figure 1 Expression Encoder

Figure 1** Expression Encoder **(Click the image for a larger view)

When a publishing plug-in is present in <Installation Directory>\Plugins and is loaded correctly, the Output tab will contain a Publish pane that lists the available publishing options. Each type requires a separate plug-in. Figure 2 shows that the FTP plug-in is available.

Figure 2 Selecting the FTP Plug-In

Figure 2** Selecting the FTP Plug-In **

The top part of the Publish pane is fixed for all plug-ins. The Publish To list is populated from the names of the valid plug-ins detected in the plug-ins directory. The Auto Start Publish checkbox is always available and, if checked, will kick off the publication automatically at the end of an encoding.

The Publish button is grayed out until encoding is complete, at which point it becomes available. By clicking the button, you can then publish all the files associated with the encoding at once. (This could be a single file if no template is used, or all files needed for the video player if a template is used.) The Settings section lets the user specify the details necessary for publishing the output. In the case of an FTP output, the server address, user name, and password are necessary. Plug-ins also provide the facility for an advanced settings area that is not initially visible but can be expanded using the pane expander button.

The Settings pane is free-format, so there's a lot of flexibility available to implement whatever your plug-in needs using XAML and Windows® Presentation Foundation (WPF).

Creating a Plug-In

The first step in constructing a plug-in is to create a new Microsoft .NET Framework 3.0 class library. Then you add the required references that allow you to compile and execute WPF and encoder applications. These are listed in Figure 3.

Figure 3 Required Encoder References

Reference Location
EncoderTypes C:\Program Files\Microsoft Expression\Encoder 1.0\EncoderTypes.dll
PresentationCore C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationCore.dll
PresentationFramework C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationFramework.dll
WindowsBase C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll

A plug-in is simply a standard .NET class that derives from the PublishPlugin base class and is decorated with the EncoderPluginAttribute attribute, which adds the metadata that describes your plug-in. Its constructor takes two strings, the plug-in name and a lengthier description.

Here's the required code:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Expression.Encoder.Plugins.
  Publishing;

namespace TestPlugin
{
    [EncoderPlugin("Boiler Plate", 
      "Boiler Plate App")]
    public class Class1 : PublishPlugin
    {
    }
}

To successfully compile the plug-in, you need to implement the three member functions of the PublishPlugin class. CreateStandardSettingsEditor creates a standard settings dialog and returns an object that describes the UI for it. CreateAdvancedSettingsEditor creates the advanced settings dialog and returns an object that describes the UI for the dialog. PerformPublish is called when the Publish button is pressed. It takes two parameters: root and filesToPublish. Root contains the directory of the files to be published. filesToPublish is an array of paths to the files that Expression Encoder will publish.

Figure 4 shows an example of a boiler plate class that implements these three functions. This class will compile into a DLL. If you copy this DLL to the Plugins directory, it will thereafter be available as a publish plug-in.

Figure 4 Boiler Plate Plug-In

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Expression.Encoder.Plugins.Publishing;

namespace TestPlugin
{
    [EncoderPlugin("Boiler Plate", "Boiler Plate App")]
    public class Class1 : PublishPlugin
    {
        public override void PerformPublish(string root, 
            string[] filesToPublish)
        {
        }
        #region IPublishPlugin Members
        public override object CreateStandardSettingsEditor()
        {
            return "Standard Settings";
        }
        public override object CreateAdvancedSettingsEditor()
        {
            return "Advanced Settings";
        }
        #endregion
    }
}

The attribute's constructor arguments define the list entry in the Publish To dropdown list, and the objects returned by the CreateStandardSettingsEditor and CreateAdvancedSettingsEditor functions define the UI for the Settings panes.

Building the FTP Upload Plug-In

Let's start with the simple boilerplate plug-in (which doesn't actually do anything) and expand it into a functional FTP upload plug-in. The standard and advanced parameters panes are implemented in WPF XAML, but they require their data to be implemented in a class that inherits from INotifyPropertyChanged in order for the plug-in to detect changes the user makes to the settings. A single class (in this case PublishData) is sufficient for both parameter panes.

Five properties are implemented for the FTP upload plug-in. UseProxy determines whether system proxy should be used or bypassed. ServerAddress provides the address of the FTP server to upload to. UserName is the login ID for the server, and UserPassword is, of course, the associated password for the login ID. Finally, ServerDirectory is the directory on the server that will receive your uploaded files. Figure 5 shows the complete class that implements these properties.

Figure 5 Properties for the FTP Plug-In

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Threading;
using System.Threading;
using System.ComponentModel;

namespace FTPPublishPlugin
{
    public class PublishData : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private bool _UseProxy = false;
        public bool UseProxy
        {
            get { return _UseProxy; }
            set
            {
                _UseProxy = value;
                PropertyChanged(this, 
                  new PropertyChangedEventArgs("UseProxy"));
            }
        }

        private string _ServerAddress = "";
        public string ServerAddress
        {
            get { return _ServerAddress; }
            set 
            { 
                _ServerAddress = value; 
                PropertyChanged(this, 
                  new PropertyChangedEventArgs("ServerAddress"));
            }
        }

        private string _UserName = "";
        public string UserName
        {
            get { return _UserName; }
            set
            {
                _UserName = value;
                PropertyChanged(this, 
                  new PropertyChangedEventArgs("UserName"));
            }
        }

        private string _UserPassword = "";
        public string UserPassword
        {
            get { return _UserPassword; }
            set
            {
                _UserPassword = value;
                PropertyChanged(this, 
                  new PropertyChangedEventArgs("UserPassword"));
            }
        }

        private string _ServerDirectory = "";
        public string ServerDirectory
        {
            get { return _ServerDirectory; }
            set
            {
                _ServerDirectory = value;
                PropertyChanged(this, 
                  new PropertyChangedEventArgs("ServerDirectory"));
            }
        }
    }

}

The structure of this class is straightforward. First, the class has to implement INotifyPropertyChanged to receive property notifications from the UI. It then implements a standard property handler design pattern whereby a private member variable stores the value of the property and is encapsulated by a getter and a setter accessor method. When the getter is called, the value of the private member variable is returned. When the setter is called, the private member is updated and the PropertyChanged event is raised. The PropertyChanged event is initialized with an argument containing the name of the property to be changed. The UI uses XAML data binding to hook into this code, causing user actions to call the getter and setter. An example of the XAML that is used in the dialog for the Server Address property is shown here:

<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Path=ServerAddress, Mode=TwoWay}" Margin="0,3,0,3" />

The parameter pane's UI controls are implemented using standard WPF XAML user controls. Note that the x:Class for these controls must be set to use the namespace you have been using thus far in your code; for example, they should be FTPPublishPlugin.StandardParams and FTPPublishPlugin.AdvancedParams for the standard and advanced panes, respectively.

These are databound to their associated properties within the PublishData class. This is achieved by setting the PublishData class to be the data context for the user control. When the plug-in instantiates the StandardParams user control, it passes an instance of the PublishData class to the control. The user control then uses this instance to set up the data context, as you see here:

public StandardParams(PublishData data)
{
    this.InitializeComponent();
    this.DataContext = data;

}

Thus, to implement the label and textbox for the Server Address, you simply use the following XAML in the user control:

<Label Grid.Column="0" Grid.Row="0" Margin="0,3,0,3">Server Address
</Label>
<TextBox Grid.Column="1" Grid.Row="0" Text=
"{Binding Path=ServerAddress, Mode=TwoWay}" Margin="0,3,0,3" />

As you can see, the TextBox's text property is bound to the ServerAddress property; the binding mode is two-way, allowing the property to be set and get automatically by WPF.

Handling a password entry field is a little more difficult, as you cannot databind to it for security reasons. In addition, the automatic styling of labels and textboxes that the encoder provides is not available to the password box, so you have to set the styling yourself. Thus, in order to implement a password box, you use the XAML shown in Figure 6 to implement the UI.

Figure 6 Password Box XAML

<PasswordBox PasswordChar="*" Grid.Column="1" Grid.Row="2" 
    x:Name="pass" PasswordChanged="HandlePW" Margin="0,3,0,3">
    <PasswordBox.Style>
        <Style TargetType="{x:Type PasswordBox}" >
            <Setter Property="BorderBrush" 
                Value="{DynamicResource BackgroundBrush}"/>
            <Setter Property="Foreground" 
                Value="{DynamicResource Text1Brush}"/>
            <Setter Property="Background" 
                Value="{DynamicResource BackgroundBrush}"/>
            <Setter Property="FontFamily" 
                Value="{DynamicResource 
                {x:Static SystemFonts.MessageFontFamilyKey}}"/>
            <Setter Property="FontSize" 
                Value="{DynamicResource 
                {x:Static SystemFonts.MessageFontSizeKey}}"/>
            <Setter Property="FontWeight" 
                Value="{DynamicResource 
                {x:Static SystemFonts.MessageFontWeightKey}}"/>
        </Style>
    </PasswordBox.Style>
</PasswordBox>

This specifies the event handler for the password change to be HandlePW. You use this event handler to determine the typed password and to store it in the PublishData class that is the current data context. Here's an example of this function:

public void HandlePW(object sender, RoutedEventArgs e)
{
    PublishData d = (PublishData) this.DataContext;
    d.UserPassword = pass.Password;
}

Implementing the Publish Class

Now that the foundations are laid, creating the publish class for an FTP server is straightforward. The first thing you'll need to do is to ensure that an instance of the PublishData class is created with the plug-in. The natural place to do this is in the constructor for the plug-in:

private StandardParams _standardParams;
private PublishData _publishData;
public Publish() : this(new PublishData())
{
}
public Publish(PublishData publishData)
{
    _publishData = publishData;
    _standardParams = new StandardParams(_publishData);
}

As you can see, this also initializes the StandardParams user control, so it will display when the plug-in is rendered. It passes the instance of the PublishData class (called _publishData) to the user control; this will be used to set the data context, as outlined in the previous step.

The next thing you do is to implement the required functions for creating the standard- and advanced-settings editor panes. If you recall the boilerplate example, these returned strings that were then rendered. Now you want to render the user controls for the respective settings panes:

public override object CreateStandardSettingsEditor()
{
    return _standardParams;   
}
public override object CreateAdvancedSettingsEditor()
{
    return new AdvancedParams(this, _publishData);  
}

The final step is to implement the Publish function, which gets called when the user requests publication of the output, either by pressing the Publish button or by selecting the Auto Publish option. In this case, you are going to publish to FTP, so the code for connecting to the server is implemented here. Figure 7 shows an example.

Figure 7 Connecting to the FTP Server

public override void PerformPublish(string root, string[] filesToPublish)
{
    try
    {
        int nFileCount = filesToPublish.Length;
        int nFileProgressSize = 100 / nFileCount;
        int nCurrentProgress = 0;
        WebClient wc = new WebClient();
        if (!_publishData.UseProxy)
            wc.Proxy = null;
        wc.BaseAddress = "ftp://" + _publishData.ServerAddress + "/";
        string strURI = "";
        wc.Credentials = 
        new NetworkCredential(_publishData.UserName, 
            _publishData.UserPassword);

        foreach (string theFile in filesToPublish)
        {
            if (!m_Cancelled)
            {
                strURI = theFile.Substring(theFile.LastIndexOf("\\") + 1);
                if (_publishData.ServerDirectory != String.Empty)
                    strURI = _
                        publishData.ServerDirectory + "\\" + strURI;
                nCurrentProgress += nFileProgressSize;
                OnProgress(strURI, nCurrentProgress);
                wc.UploadFile(strURI, theFile);
            }
        }

        if (m_Cancelled)
        {
            PublishingHost.ShowMessageBox("Upload Cancelled",   
                System.Windows.MessageBoxButton.OK, 
                System.Windows.MessageBoxImage.Error);
        } 
    }

    catch (Exception ex)
    {
        string strIE = ex.Message;
        if (ex.InnerException != null)
            strIE += "\n: " + ex.InnerException.Message;
                
        throw new Exception(strIE);
    }

    protected override void CancelPublish()
    {
        m_Cancelled = true;
        base.CancelPublish();
    }
}

As you can see, this is fairly straightforward C# code for managing the upload of a set of files to an FTP server. The required data for the FTP server is taken from the _publishData class, which in turn is set by the controls discussed earlier.

One function of note is OnProgress, which is part of the Plugins API and is not necessary to implement. You simply call this function with a string and a number from 0 to 100. It then renders the string and a bar representing the value as a percentage of progress. In this case, the progress is determined by the number of files uploaded, so if, for example, there are 10 files, each is considered 10 percent of the progress regardless of the file size.

Also take note of the CancelPublish function. The API provides this function for you to override. It will be fired when the user clicks the Cancel button that is automatically generated by the UI when you are publishing. In this case, it simply sets the m_Cancelled Boolean to true and cancels publication.

Note that as this code is just a sample application, it has a single exception handler that captures all exceptions and throws them up to the user. In a real application, you should provide more detailed error information for the various exceptions your application may encounter.

Saving Configuration Settings

In order to keep the user from constantly having to enter credentials for the FTP server being published to, you can override the SaveJobSettings and LoadJobSettings methods from the PublishPlugin base class. These methods will be called on the plug-in instance by Expression Encoder when the user calls Save Job from the file menu, and they allow you to serialize and restore your configuration properties. For example, to save your configuration properties, you would write the following:

public override void SaveJobSettings(XmlWriter writer)
{
  writer.WriteStartElement("FtpSample");
  writer.WriteElementString("UseProxy", 
      _publishData.UseProxy.ToString());
  writer.WriteElementString("ServerAddress", 
      _publishData.ServerAddress);
  writer.WriteElementString("UserName", _publishData.UserName);
  writer.WriteElementString("ServerDirectory", 
      _publishData.ServerDirectory);
  writer.WriteEndElement();
}

One thing to note here is that you must nest your configuration properties within a top level XML Element. This is because your properties are serialized into your encoding job's XML job file, which contains lots of additional data. You can restore your saved properties with the following:

public override void LoadJobSettings(XmlReader reader)
{
  reader.ReadStartElement("FtpSample");
  _publishData.UseProxy = bool.Parse(xr.ReadElementString("UseProxy"));
  _publishData.ServerAddress = xr.ReadElementString("ServerAddress");
  _publishData.UserName = xr.ReadElementString("UserName");
  _publishData.ServerDirectory = xr.ReadElementString("ServerDirectory");
  reader.ReadEndElement();
}

Compile, Deploy, and Debug

Should everything compile happily, you will have three files in your projects bin\debug directory: your plug-in DLL, a .pdb file for your DLL, and EncoderTypes.dll. Copy only the plug-in DLL file to the Plugins folder. Now, when you launch Expression Encoder, the plug-in should be loaded correctly.

After loading Expression Encoder, return to Visual Studio and select Debug | Attach to Process. The Attach to Process dialog will appear, and from there select MediaEncoderUI.exe.

If you want to inspect the publication process, you can set a breakpoint in the Publish function. Then, after completing an encoding job, if you press the Publish button in Expression Encoder, you will hit your breakpoint and will be able to step through and see what is happening as Expression Encoder publishes your files.

Laurence Moroney is a Senior Technology Evangelist with Microsoft, specializing in Silverlight. He is the author of many books on computing topics, including Silverlight, AJAX, interoperability, and security. His blog is at blogs.msdn.com/webnext.