DotNetCurry Logo

Encoding Media in WindowsAzure and Monitoring Progress using SignalR

Posted by: Sumit Maitra , on 8/29/2013, in Category Microsoft Azure
Views: 33393
Abstract: Explore how to Encode Media in Windows Azure from our previously built Azure Media Portal in ASP.NET MVC. We’ll also use SignalR to monitor progress.

In my previous article I showed how we could build a personal Media Portal using Windows Azure Media Services and ASP.NET MVC. Today we’ll add the ability to encode the uploaded media using Media Services. Since encoding is a long-running job, SignalR provides us a perfect way to ‘push’ out updates to the client as the encoding progresses.

Caveat: Mind you, encoding uses Azure App Fabric and will cost you, if you are on any of the paid plans. If you are on a 90 day trial, don’t go overboard trying to encode you entire media collection, keep an eye on the usage.

Getting Started with the ASP.NET MVC App

Since we are adding on to the Media Portal app, let’s download the existing code. You can get either the zip or clone it from our repo or fork it if want to.

 

Updating your Media Keys in Web.config

Once you have downloaded the code and built it, you need to update the following keys in the web.config

<add key="StorageConnectionString" value="[ ]" />
<add key="StorageAccountKey" value ="[ ]" />
<add key="StorageAccountName" value="[ ]" />
<add key="MediaAccountName" value="[ ]"/>
<add key="MediaAccountKey" value="[ ]" />

After the keys have been updated you should be able to upload media to your portal.

Adding SignalR to the project

We will add SignalR to the project so that we can update status of our Encoding process. Media services doesn’t return a percentage complete, rather the following statuses: Queued, Scheduled, Processing, Finished, Error, Canceled and Canceling. Even then it’s worthwhile intimating user as to the progress of the Job instead of sending the Job into a black hole.

To Add SignalR to the project we use Nuget to add it our Project

PM> install-package Microsoft.AspNet.SignalR

Configuring SignalR

Once installed we have to add the following line to the Application_Start event our Global.asax

RouteTable.Routes.MapHubs();

Next, we add a Folder called StatusHub and add a new Hub file called JobStatusHub

job-status-hub

In the JobStatusHub.cs file, we’ll rename the default Hello method to UpdateStatus and implement it as follows

public class JobStatusHub : Hub
{
    public void UpdateJobStatus(string assetId, string status)
    {
        Clients.All.updateJobStatus(assetId, status);
    }
}

As we can see in the code above, we are sending back an update to all client but with the assetId. The clients will verify against the assetId and if they initiated a job against that assetId, they will use the status to update UI else ignore the message.

We leave the SignalR implementation like this and come back to it once we are ready to send status updates.

Adding the Encode Functionality

Once you have the existing application going, let us work towards adding the new Encode functionality.

Updating the MediaElement

First up, we’ll update our MediaElement entity to add an ‘EncodingType’ property. Before we do that, let’s enable Entity Framework Code First Migrations. This enable us to apply changes in the Entities to the database.

Enabling Migrations

In the Package Manager Console, type in the following command

PM> enable-migrations -ContextTypeName AzureMediaPortal.Models.AzureMediaPortalContext

If you have only one DBContext class, simple enable-migrations works, but since we have two contexts, we need to specify the Type name explicitly.

Once Migrations are enabled, add the EncodingType property to the MediaElement class

public class MediaElement
{
public int Id { get; set; }
public string UserId { get; set; }
public string Title { get; set; }
public string FileUrl { get; set; }
public string AssetId { get; set; }
public bool IsPublic { get; set; }
public string EncodingType { get; set; }
}

Adding and Applying Migrations

Add the change to EF Migrations using the following command:

PM> add-migration UpdatedMediaElement

This takes a snapshot of the Entities and scaffolds a class that has the new property to be deployed. To update database using the following command:

PM> update-database

This updates our database, next we’ll scaffold the Encode view.

Encode View and updating the MediaController

We scaffold an Encode View using the MediaElement and select the Edit template.

add-encoding-view

Once the View is scaffolded, we add the Get Encode action method that takes the Id as a parameter and returns the View populated with the Media Element details.

[HttpGet]
public ActionResult Encode(int id)
{
    MediaElement mediaelement = db.MediaElements.Find(id);
    if (mediaelement == null)
    {
        return HttpNotFound();
    }
    if (string.IsNullOrEmpty(mediaelement.FileUrl))
    {
        mediaelement.FileUrl = GetStreamingUrl(mediaelement.AssetId);
        db.Entry(mediaelement).State = EntityState.Modified;
        db.SaveChanges();
    }
    return View(mediaelement);
}

Updating the Index View

The Encode page is launched from the Index view (and we could optionally add it to the Create/Edit views as well). We update the Index view to show the new property EncodingType by adding the following to the header and body respectively.

<th>
    @Html.DisplayNameFor(model => model.EncodingType)
</th>
<td>
    @Html.DisplayFor(modelItem => item.EncodingType)
</td>
We add an empty <td> in the header and the following markup for the Encode action link.
<td>
    @Html.ActionLink("Encode", "Encode", new { id=item.Id })
</td>

If we run the application now and navigate to the My Media page, we would see something like the following:

my-media-index

If we click on the Encode link, we’ll see the encode View

encode-view-default

Now as we can see, all the fields are editable where they need not be. The Encoding page should only let the user select the EncodingType (from a drop down of presets) and start Encoding (so the text Save is also incorrect in this context).

Updating the Encode View

We trim out the extra fields and leave only the Id, UserId, AssetId, Title and Encoding Type. Of this only Title and EncodingType are visible, rest are in hidden variables. We also remove the HtmlForm as we are going to post-back via AJAX.

Next we add a span for statusMessage and a statusPanel that contains a jQuery progress bar. This will be an indeterminate progress bar since we’ll not be getting progress % from the server.

The markup for encode ends up as follows:

@model AzureMediaPortal.Models.MediaElement
@Html.HiddenFor(model => model.Id)

<h1>Encode Media</h1>
<div class="editor-field">
@Html.HiddenFor(model => model.UserId)
</div>
<h3>
@Model.Title
</h3>
<div class="editor-field">
@Html.HiddenFor(model => model.AssetId)
</div>
<div>
<h3>Encode As </h3>
@Html.DropDownListFor(model => model.EncodingType, ViewBag.EncodingTypes as SelectList,
  "--Select Encoding Type--")
@Html.ValidationMessageFor(model => model.EncodingType)
</div>
<h3>Job Status:</h3>
<span id="statusMessage"></span>
<div id="statusPanel">
<div id="progress" style="width: 200px; height: 20px; background-color: grey"></div>
</div>
<p>
<input type="button" id="encode" value="Encode" />
</p>
<div>
@Html.ActionLink("Back to List", "Index")
</div>

JavaScript for handling SignalR, Postback and Status updates

With the Encode.cshtml all marked up, we need to handle the POST to the Encode action method as well as the status updates from SignalR. We separate out all the JavaScript and add a new JS file in the Scripts folder.

encode-manager

Handling SignalR Hub connections

In the script, first we instantiate the SignalR hub and attach two methods that the Server will be calling. The updateJobStatus is called as the status changes and the updateJobComplete is called once the job is complete. We update the ‘statusMessage’ text in both cases. Once the job is complete, we also hide the ‘statusPanel’ which contains our progress bar.

On script load we also hide the ‘statusPanel’ and initialize the jQuery UI progress bar.

$("#statusPanel").hide();
var progressbar = $("#progress").progressbar();

var hub = $.connection.jobStatusHub;
hub.client.updateJobStatus = function (assetId, statusMessage)
{
    var asset = $("#AssetId").val();
    if (asset == assetId)
    {
        $("#statusMessage").text(statusMessage);
    }
}
hub.client.updateJobCompleted = function (assetId, statusMessage)
{
    var asset = $("#AssetId").val();
    $("#encode").show();
    if (asset == assetId)
    {
        $("#statusMessage").text(statusMessage);
    }
    setTimeout(function ()
    {
        $("#statusPanel").fadeOut(1000);
    }, 2000);
}

Next we initialize the SignalR hub connection and let the server hub know that a client is ready. We use the start() function on the hub to do this. The start() function returns a promise and once the promise is completed, it executes the attached function.

The attached function hooks up the click handler of the Encode button. The click handler posts data to the /Media/Encode URL with partial data for the MediaElement entity. However we only need the AssetId and EncodingType, so it’s okay if the MediaElement does not have all its properties populated.

On success the statusMessage is set to “Job Started”. Remember the job status is updated asynchronously so the success method here is called right after the job has been kicked off. Thereafter as the job status changes, we’ll get ping-backs from the server and we’ve handled that using SignalR.

$.connection.hub.start().done(function ()
{
$(document).on("click", "#encode", null, function ()
{
  $("#statusMessage").text("");
  $("#statusPanel").fadeIn(1);
  $("#encode").hide();
  var jsonData = {
   Id: $("#Id").val(),
   AssetId: $("#AssetId").val(),
   EncodingType: $("#EncodingType").val()
  };
  $.ajax({
   type: "POST",
   contentType: "application/json",
   url: "/Media/Encode",
   data: JSON.stringify(jsonData)
  }).success(function ()
  {
   $("#statusMessage").text("Job Started");
  }).fail(function ()
  {
   ("#statusMessage").text("Failed to start encoding");
   $("#encode").show();
  });
});
});

The document’s ready function has only one thing to do that is setup the progressBar as indeterminate (setting value to false make the progressbar indeterminate).

$(document).ready(function ()
{
    progressbar.progressbar("option", "value", false);
});

Note: I had to upgrade jQuery.Ui.Common package to version 1.10.3 for the indeterminate progress bar to work. So there is a breaking change between the 1.8.x version that gets installed by default at the time of writing.

That sets up our client. Let’s see what the server side controller looks like.

Updating the Controller – Encode Get and Post actions

The Get Encode action is easy. It’s same as edit except that we need to setup a list of Encoding Type that we can use for selecting the Encoder. The complete list of Encoding types is provided here. Currently they are ‘magic’ strings, we can hope in future they’ll be an enumeration or something. So our final Get method looks like the following

[HttpGet]
public ActionResult Encode(int id)
{
    MediaElement mediaelement = db.MediaElements.Find(id);
    if (mediaelement == null)
    {
        return HttpNotFound();
    }
    if (string.IsNullOrEmpty(mediaelement.FileUrl))
    {
        mediaelement.FileUrl = GetStreamingUrl(mediaelement.AssetId);
        db.Entry(mediaelement).State = EntityState.Modified;
        db.SaveChanges();
    }
    ViewBag.EncodingTypes = new SelectList(new string[]
    {
  "H264 Broadband 720p",
  "H264 Broadband 1080p",
  "H264 Broadband SD 16x9"
});
return View(mediaelement);
}

The Post method is more involved as it does the following:

1. Creates the CloudMediaContext object (provided by the SDK) using the Account Name and Key. Next it extracts the Asset object that will be the encoding source.

2. It creates a new Job using the Context from 1 above.

3. Initializes the MediaProcessor. Note that we dig up the required processor from the list of available processors. Again we are using a ‘magic’ string “Windows Azure Media Encoder” to pick the right encoder.

4. Add a task to our job and provide it with the task name, the media processor, encoding preset and TaskOptions.

5. Add the source asset to the task as the Input Asset.

6. Create the destination Asset Name and add it to the OutPutAssets of the task.

7. Add a State Changed event handler (we’ll see the exact implementation of the handler in a bit)

8. Submit the Job and add a ContinueWith delegate to the ExecutionProgressTask. This happens asynchronously so execution returns to this point once the Job has completed.

9. Once the job is complete we Publish the asset.

10. Finally we save the data of the newly created asset to the Database.

The full code is as follows:

[HttpPost]
public JsonResult Encode(MediaElement element)
{
string serviceName = ConfigurationManager.AppSettings["MediaAccountName"];
string serviceKey = ConfigurationManager.AppSettings["MediaAccountKey"];
var context = new CloudMediaContext(serviceName, serviceKey); //#1
var encodeAssetId = element.AssetId;
// Preset reference documentation:
msdn.microsoft.com/en-us/library/windowsazure/jj129582.aspx
var encodingPreset = element.EncodingType;
var assetToEncode = context.Assets.Where(a => a.Id == encodeAssetId).FirstOrDefault();
if (assetToEncode == null)
{
  throw new ArgumentException("Could not find assetId: " + encodeAssetId);
}
IJob job = context.Jobs.Create("Encoding " + assetToEncode.Name + " to " +
  encodingPreset); //#2
IMediaProcessor latestWameMediaProcessor = (from p in context.MediaProcessors
  where p.Name == "Windows Azure Media Encoder"
  select p).ToList().OrderBy(wame =>
   new Version(wame.Version)).LastOrDefault(); //#3
ITask encodeTask = job.Tasks.AddNew("Encoding", latestWameMediaProcessor,
  encodingPreset, TaskOptions.None); //#4
encodeTask.InputAssets.Add(assetToEncode); //#5
string newAssetName = assetToEncode.Name + " as " + encodingPreset;
string userName = User.Identity.Name;
encodeTask.OutputAssets.AddNew(newAssetName, AssetCreationOptions.None); //#6
job.StateChanged += job_StateChanged; //#7
job.Submit(); //#8
job.GetExecutionProgressTask(CancellationToken.None).ContinueWith(t =>
{
  if (t.IsCompleted)
  {
   var preparedAsset = PublishAsset(job); //#9
   SaveNewAssetToDatabase(element, newAssetName, userName, preparedAsset); //#10
  }
});
return Json(new { status = "Starting" });
}

Update Client in the JobStatus_Changed event – Using SignalR

The Job Status Changed event is called as the Job progresses from one phase to the next.

void job_StateChanged(object sender, JobStateChangedEventArgs e)
{
var hub = GlobalHost.ConnectionManager.GetHubContext<JobStatusHub>();
IAsset element = ((IJob)sender).InputMediaAssets[0];
hub.Clients.All.updateJobStatus(element.Id, Enum.GetName(typeof(JobState),
  e.CurrentState));
}

We retrieve the hub instance from the GlobalHost and call the updateJobStatus method, we send the Asset ID and the Job Status. Since SignalR is broadcasting to all the clients, the clients need to filter out the asset that they are handling and use only the status that’s meant for them.

We could have used a more complex mechanism to keep a map of client and asset id and then sent to that client only. That’s left as an exercise.

Publish newly encoded Asset

Here we simply pick the .ism file and set it to Primary.

private static IAsset PublishAsset(IJob job)
{
var preparedAsset = job.OutputMediaAssets.FirstOrDefault();
var ismAssetFiles = preparedAsset.AssetFiles.ToList().
  Where(f => f.Name.EndsWith(".ism", StringComparison.OrdinalIgnoreCase))
  .ToArray();

  if (ismAssetFiles.Count() != 1)
   throw new ArgumentException("The asset should have only one, .ism file");
  ismAssetFiles.First().IsPrimary = true;
  ismAssetFiles.First().Update();
  return preparedAsset;
}

Save Asset to Database

We use the properties of the newly created asset to save a new MediaElement to the database. Along the way, we use the GetStreamingUrl method that we created in the previous article to generate a public URL for our newly encoded asset.

private void SaveNewAssetToDatabase(MediaElement element, string newAssetName, string
userName, IAsset preparedAsset)
{
var hub = GlobalHost.ConnectionManager.GetHubContext<JobStatusHub>();
hub.Clients.All.updateJobStatus(element.AssetId, "Done");
AzureMediaPortalContext dbContext = new AzureMediaPortalContext();
MediaElement newMedia = new MediaElement
{
  UserId = userName,
  EncodingType = element.EncodingType,
  Title = newAssetName,
  AssetId = preparedAsset.Id,
  FileUrl = GetStreamingUrl(preparedAsset.Id)
};
dbContext.MediaElements.Add(newMedia);
dbContext.SaveChanges();
hub.Clients.All.updateJobCompleted(element.AssetId, "Newly Encoded asset is now
  listed.");
}

Note once the Save is called, we raise the updateJobStatus once more with the status “Done” implying encoding task is done and once data is saved to the Database, the final call to the client calls the updateJobCompleted method.

That pretty much covers it all. Time to see this thing running.

Azure Encoding Demo

In the previous article I had already uploaded a video. From the Index page, we use the Encode action link to navigate to the Encode Page. As seen below, we’ve selected the “H264 Smoot Streaming 720p” option

encode-select

Hit Encode to begin the encoding process. In quick succession you’ll see the status go from Started, to Scheduled, to Processing.

encoding-started-scheduled-processing

Finally when the processing completes, it will update to Finished, before showing Done and “Newly Encoded asset is now listed.” If you go to the Edit Page, you should now see your freshly encoded video is up and running.

Some Caveats

1. I have deliberately skipped variable bit-rate smooth streaming since it needs a Silverlight plugin and the Playframework is a bit weird to configure in a hurry. More on that later.

2. Documentation here is a little out of date (as of this writing). Even though it emphatically states that “H264 Broadband 720p” generates only one mp4 file, it fails to mention that it also generates an ism and an xml file completely messing up my logic for generating public URLs. The GetStreamingUrl method earlier used to check if there is an ism file and used to point to the /manifest URL if it did. But now Azure apparently generates ism files for everything, so I’ve had to hack that logic and look for mp4 files only. I’ll revisit ism files when we do smooth streaming at a later date.

Conclusion

Today we saw how to encode media files uploaded to Azure. We also saw how to leverage SignalR to monitor the progress and update the status of the Encoding job.

Download the entire source code of this article (Github)

Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+
Further Reading - Articles You May Like!
Author
Sumit is a .NET consultant and has been working on Microsoft Technologies since his college days. He edits, he codes and he manages content when at work. C# is his first love, but he is often seen flirting with Java and Objective C. You can follow him on twitter at @sumitkm or email him at sumitkm [at] gmail


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!
Comment posted by klod on Thursday, May 1, 2014 11:17 AM
I'm trying to make an MVC5 Website and I want to creat a Live streaming using Windows Azure Media Services.
ther's any detailed or a sample project step by step on how create live streaming Video please ?