Friday, January 29, 2010

Programmatically Accessing the Live Smooth Streaming API

Live Smooth Streaming is a Microsoft technology that allows you to take a live, encoded, incoming video stream and rebroadcast it using Smooth Streaming technology. This technology multicasts the video stream in segments of varying bandwidths. This can then be played with a Silverlight-based client like the built-in MediaElement or more advanced player like the Silverlight Media Framework.

The incoming streams could be from your web cam or third party sources, or even from a file encoded on disk that is being webcast at a particular point in time. The live smooth streaming allows the client to adjust to the current network load and gracefully degrade quality when the network slows to avoid having to pause the playback and force the user to wait for a new buffer. The user can also rewind the "live content," and there are options to archive it to play on demand at a later date.

A content network with many live streams might have multiple servers and cabinets in their data centers to serve content. In that scenario, programmatic management of endpoints becomes essential to effectively scale the operation. If you find yourself needing to automate some aspects of starting, stopping, shutting down or creating publishing points for live streaming, you're in luck!

Let's start with a few entities to help us handle publishing points. A publishing point can have a stream state (the status of the encoding stream it is listening to), and archive state (the status of the stream it is archiving to) and a fragment stream that enables insertion of other content to the stream. These various nodes on the publishing point share a common set of states:


public enum State
{
    Disabled,
    Started,
    Stopped,
    Unknown
}

These are fairly self-explanatory. "Unknown" simply means the publishing point is in a status, such as stopped or shut down, when it doesn't make sense for the node to have a valid status.

The publishing point itself has a more involved list of states. For an excellent overview, read this article on creating and managing publishing points. Here are the states:


public enum PublishingPointState
{
    Idle,
    Starting,
    Started,
    Stopping,
    Stopped,
    Shuttingdown,
    Error,
    Unknown
}

Again, these are fairly self-explanatory. Idle happens when the publishing point has been created, but no action has been taken against it, or it is shut down. Starting indicates it is ready to receive a feed, and started means it is actively processing one.

Let's go ahead and build an entity to represent the publishing point:


public class PublishingPoint
{        
    public State StreamState { get; set; }

    public State ArchiveState { get; set; }

    public State FragmentState { get; set; }

    public PublishingPointState PubState { get; set; }

    public string SiteName { get; set; }

    public string Path { get; set; }

    public string Name { get; set; }
}

The first few properties are the various states of the nodes and the publishing point itself. The site name is the website it is configured on. This will usually be "Default Web Site." The Path is the virtual path (not the physical path) the publishing point resides in (also known as the Application), and the name is the actual name of the publishing point.

While there is not a strongly typed API to interface directly with the live smooth streaming configuration as of this writing, we can easily reach it through the new administration classes. With IIS 7.0 and smooth streaming installed, you should be able to find Microsoft.Web.Administration.dll in your %windir%\system32\inetsrv directory. Reference this DLL, and you can programmatically access web sites. You can also interact with configurations through a reflection-style interface that we'll cover here.

Get the List of Publishing Points

Our first task is to get a list of publishing points.


private const string LIVESTREAMINGSECTION = "system.webServer/media/liveStreaming";
private const string METHODGETPUBPOINTS = "GetPublishingPoints";
private const string ATTR_SITENAME = "siteName";
private const string ATTR_VIRTUALPATH = "virtualPath";
private const string ATTR_NAME = "name";
private const string ATTR_ARCHIVES = "archives";
private const string ATTR_FRAGMENTS = "fragments";
private const string ATTR_STREAMS = "streams";
private const string ATTR_STATE = "state";
        
public List<PublishingPoint> GetPublishingPoints()
{
    var retVal = new List<PublishingPoint>();

    using (var serverManager = new ServerManager())
    {
        Configuration appHost = serverManager.GetApplicationHostConfiguration();

        try
        {
            ConfigurationSection liveStreamingConfig = appHost.GetSection(LIVESTREAMINGSECTION);

            foreach (Site site in serverManager.Sites)
            {
                foreach (Application application in site.Applications)
                {
                    try
                    {
                        ConfigurationMethodInstance instance =
                            liveStreamingConfig.Methods[METHODGETPUBPOINTS].CreateInstance();

                        instance.Input[ATTR_SITENAME] = site.Name;
                        instance.Input[ATTR_VIRTUALPATH] = application.Path;

                        instance.Execute();

                        ConfigurationElement collection = instance.Output.GetCollection();

                        foreach (var item in collection.GetCollection())
                        {
                            retVal.Add(new PublishingPoint
                                           {
                                               SiteName = site.Name,
                                               Path = application.Path,
                                               Name = item.Attributes[ATTR_NAME].Value.ToString(),
                                               ArchiveState = (State) item.Attributes[ATTR_ARCHIVES].Value,
                                               FragmentState = (State) item.Attributes[ATTR_FRAGMENTS].Value,
                                               StreamState = (State) item.Attributes[ATTR_STREAMS].Value,
                                               PubState =
                                                   (PublishingPointState) item.Attributes[ATTR_STATE].Value
                                           });
                        }
                    }
                    catch (COMException ce)
                    {
                        Debug.Print(ce.Message);
                    }
                }
            }
        }
        catch (COMException ce)
        {
            Debug.Print(ce.Message);
        }
    }

    return retVal;
}

The ServerManager is our hook into administration. All of the configuration for the live smooth streaming is in the APPHOST section of the configuration, which we fetch at the beginning of the method by grabbing the configuration and then navigating to system.webServer/media/liveStreaming.

The method to fetch publishing points requires a web site and an application (virtual directory). We iterate through these and use the ConfigurationMethodInstance to ask for the this. The new IIS configuration extensions allow methods and API hook points to be defined in the configuration itself, with a set of inputs and outputs. You can read more about the ConfigurationMethod. We use this and pass in the web site name and application, to receive the list of publishing points. Notice how we get a generic collection of ConfigurationElement and reference the attributes to build our more strongly-typed PublishingPoint object.

Of course, there are multiple areas where exceptions may be thrown. In the example, I capture the COM errors and just print them to debug. You can interrogate the error code and provide more specific feedback as to why the call failed.

Starting, Stopping, and Shutting Down Publishing Points

Now that we have a publishing point, we can stop, start, and shut it down. This is also done through a configuration method. The method names for the action are:


public enum PublishingPointCommand
{
    Start,
    Stop,
    Shutdown
}

The method names are the actual text, so we can use the ToString() method on the enum to pass the command. Issuing a command against a specific publishing point now looks like this:


private static void _IssueCommand(PublishingPoint publishingPoint, PublishingPointCommand command)
{
    using (var serverManager = new ServerManager())
    {
        Configuration appHost = serverManager.GetApplicationHostConfiguration();

        ConfigurationSection liveStreamingConfig = appHost.GetSection(LIVESTREAMINGSECTION);

        if (liveStreamingConfig == null)
        {
            throw new Exception("Couldn't get to the live streaming section.");
        }

        ConfigurationMethodInstance instance =
            liveStreamingConfig.Methods[METHODGETPUBPOINTS].CreateInstance();

        instance.Input[ATTR_SITENAME] = publishingPoint.SiteName;
        instance.Input[ATTR_VIRTUALPATH] = publishingPoint.Path;

        instance.Execute();

        // Gets the PublishingPointCollection associated with the method output
        ConfigurationElement collection = instance.Output.GetCollection();

        foreach (var item in collection.GetCollection())
        {
            if (item.Attributes[ATTR_NAME].Value.ToString().Equals(publishingPoint.Name))
            {
                var method = item.Methods[command.ToString()];
                var methodInstance = method.CreateInstance();
                methodInstance.Execute();
                break;
            }
        }
    }
}

Of course, you can probably see a place where it makes sense to refactor some code, because we're spinning through the collection again. This time, instead of turning it into a concrete type, we are taking the specific item in the collection for the publishing point we're targeting, then creating an instance of the command method and firing it off. Simple as that!

Creating the Publishing Point

So now we can manipulate and gather information about a point, but what about creating it? If you are frustrated trying to find where in the API you can call a "Create" method, take a deep breath and step away from the configuration collection. It's not there. To create a publishing point, we simply drop a file on the system! That's right. If you create a new publishing point, you can navigate to the file system and set an .isml file. Open it in notepad and you'll find the format is straightforward:

<?xml version="1.0" encoding="utf-8"?>
   <smil xmlns="http://www.w3.org/2001/SMIL20/Language">
      <head>
         <meta name="title" content="Pub Point Title" />
         <meta name="module" content="liveSmoothStreaming" />
         <meta name="sourceType" content="Push" /><!-- or Pull -->
         <meta name="publishing" content="Fragments;Streams;Archives" />
         <meta name="estimatedTime" content="00:11:22" />
         <meta name="lookaheadChunks" content="2" />
         <meta name="manifestWindowLength" content="0" />
         <meta name="startOnFirstRequest" content="False" />
         <meta name="archiveSegmentLength" content="0" />
      </head>
   <body/>
</smil>

That's it ... very straightforward. Give it a title, make it push or pull, and estimate the time. Of course, you can manipulate some of the more advanced features as well, but for a basic interface, this is all we need. We'll store the XML with a constant that holds the template called ISML_TEMPLATE. When the user requests a new publishing point, we'll find the right virtual directory, get the physical path, and then write out the file:

public enum LiveSourceType
{
    Push,
    Pull
}

public bool CreatePublishingPoint(PublishingPoint publishingPoint, string title, string duration, LiveSourceType type)
{

    bool retVal = false;

    using (var manager = new ServerManager())
    {
        Site site = manager.Sites[publishingPoint.SiteName];

        Application application = site.Applications[publishingPoint.Path];

        string template =
            string.Format(ISML_TEMPLATE, title, type, duration ?? string.Empty);

        try
        {
            string path = string.Format(@"{0}\{1}", application.VirtualDirectories[0].PhysicalPath,
                                        publishingPoint.Name);

            File.WriteAllText(path, template);

            retVal = true;
        }
        catch (Exception ex)
        {
            Debug.Print(ex.Message);
        }
    }

    return retVal;
}

This is fairly straightforward to give you the point. In production, you'll need to test for nulls (i.e. if the site doesn't exist, a null template is passed in, etc) and may want to validate that it was created successfully by enumerating the list after creation. I'd also recommend creating an XmlDocument or XDocument for more advanced titles ... in this example, using string format, we can make it fast but must make sure the title is clean or escape it before making it part of the source XML.

That's it ... in a nutshell, you now have all of the bits and pieces needed to interface programmatically with the Live Smooth Streaming APIs. I have no sample project because this is intended to be a guideline for your own explorations and use of the system. I would also suggest the excellent blog by Ezequiel Jadib which contains loads of information about smooth streaming.

Jeremy Likness

2 comments:

  1. Hi
    i am using below code to get the list of all Publishing points.
    ConfigurationMethodInstance instance = liveStreamingConfig.Methods[METHODGETPUBPOINTS].CreateInstance();

    instance.Input[ATTR_SITENAME] = siteName;
    instance.Input[ATTR_VIRTUALPATH] = applicationPath;

    instance.Execute();

    ConfigurationElement collection = instance.Output.GetCollection();

    foreach (var item in collection.GetCollection())
    {
    retVal.Add(new PublishingPoint
    {
    SiteName = siteName,
    Path = applicationPath,
    Name = item.Attributes[ATTR_NAME].Value.ToString(),
    ArchiveState = (State)item.Attributes[ATTR_ARCHIVES].Value,
    FragmentState = (State)item.Attributes[ATTR_FRAGMENTS].Value,
    StreamState = (State)item.Attributes[ATTR_STREAMS].Value,
    PubState = (PublishingPointState)item.Attributes[ATTR_STATE].Value
    });
    LogDetails("----------------------------------------");
    LogDetails("siteName: " + siteName);
    LogDetails("applicationPath: " + applicationPath);
    LogDetails("Name: " + item.Attributes[ATTR_NAME].Value.ToString());
    LogDetails("ArchiveState: " + item.Attributes[ATTR_ARCHIVES].Value.ToString());
    LogDetails("FragmentState: " + item.Attributes[ATTR_FRAGMENTS].Value.ToString());
    LogDetails("StreamState: " + item.Attributes[ATTR_STREAMS].Value.ToString());
    LogDetails("PubState: " + item.Attributes[ATTR_STATE].Value.ToString());
    LogDetails("");
    }

    But i always get the item.Attributes[ATTR_STATE].Value as zero, whatever be the state.
    can you please help me why it is not showing the correct state?

    Additionally if I click the shutdown button I get an error:
    -------------------------
    Exception Details: System.Runtime.InteropServices.COMException: The data is invalid. (Exception from HRESULT: 0x8007000D)

    Source Error:


    Line 277: var method =
    item.Methods[command.ToString()];
    Line 278: var methodInstance =
    method.CreateInstance();
    Line 279: methodInstance.Execute();
    Line 280: break;
    Line 281: }


    Source File: c:\inetpub\wwwroot\MountPointHandler\Default.aspx.cs
    Line: 279
    -------------------------

    Please suggest
    Thanks in advance.

    Warm Regards,
    Parag Gupta

    ReplyDelete
  2. Thanks,

    This was very helpfull.

    Reuven Kadison.

    ReplyDelete