Encoding an MP4 file to Smooth Streaming and Apple HLS in the cloud | Encodage en nuage d’un fichier MP4 vers du smooth streaming et de l’Apple HLS
Since June 2012, a preview of Windows Azure Media Services is available. This post provides sample code and execution screenshots of the following scenario: Encoding an MP4 file to Smooth Streaming and Apple HLS in the cloud. | Depuis juin 2012, une version bêta de Windows Azure Media Services est disponible. Ce billet fournit un exemple de code et des copies d’écrans pour le scénario suivant: Encodage en nuage d’un fichier MP4 vers du smooth streaming et de l’Apple HLS |
You may find a lot of good resources on how to start with Windows Azure at https://www.windowsazure.com, and more specifically at https://www.windowsazure.com/en-us/home/scenarios/media/ for Windows Azure Media Services. | Vous trouverez beaucoup de bonnes ressources sur le démarrage avec Windows Azure à https://www.windowsazure.com, et plus spécifiquement à https://www.windowsazure.com/fr-fr/home/scenarios/media/ pour Windows Azure Media Services. En français, il y a également ce billet pour bien démarrer. |
Windows Azure Media Services SDK documentation is available in the MSDN library. | La documentation du SDK de Windows Azure Media Services est disponible dans la librairie MSDN. |
After following the first steps of https://www.windowsazure.com/en-us/develop/net/how-to-guides/media-services/, we get a machine with Visual Studio 2010, Windows Azure Media Services SDK, and Windows Azure SDK 1.6 (Windows Azure Media Services preview does not work yet with Windows Azure SDK 1.7 yet). At this stage, we also have a Windows Azure Media Services account and the corresponding key. | Après avoir suivi les premières étapes de https://www.windowsazure.com/en-us/develop/net/how-to-guides/media-services/, on a une machine avec Visual Studio 2010, le SDK Windows Azure Media Services, et Windows Azure SDK 1.6 (cette version de Windows Azure Media Services ne fonctionne pas encore avec le SDK 1.7 de Windows Azure). A ce stade, on a également un compte Windows Azure Media Services et sa clef. |
Let’s start a console App running on .NET Framework 4… | Commençons une application console qui s’exécute sur le .NET Framework 4.0… |
with the following references: | avec les références suivantes: |
Add some code | Ajoutons du code |
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.WindowsAzure.MediaServices.Client;
namespace WindowsAzureMediaServicesSample
{
class Program
{
[STAThread]
static void Main(string[] args)
{
try
{
Console.WriteLine("retrieving context");
CloudMediaContext context = new CloudMediaContext(Configuration.AccountName, Configuration.AccountKey);
Console.WriteLine("context is retrieved");
//(new SampleCode2(context)).Show();
//(new SampleCode2(context)).Reset();
//(new SampleCode2(context)).Show();
(new SampleCode5(context)).Run();
}
catch (Exception ex)
{
Console.WriteLine("Oops: {0}", ex);
}
finally
{
Console.WriteLine("---");
Console.ReadLine();
}
}
}
}
SampleCode2:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.WindowsAzure.MediaServices.Client;
namespace WindowsAzureMediaServicesSample
{
public class SampleCode2
{
private CloudMediaContext context = null;
public SampleCode2(CloudMediaContext context)
{
this.context = context;
this.context.Assets.OnUploadProgress += Assets_OnUploadProgress;
}
public void Show()
{
Console.WriteLine("--- media processors ---");
foreach (var mp in context.MediaProcessors)
{
Console.WriteLine("name={0}, Vendor={1}, version={2}", mp.Name, mp.Vendor, mp.Version);
}
Console.WriteLine();
foreach (var job in context.Jobs)
{
Console.WriteLine("{0} {1} {2} {3} {4}", job.Name, job.State, job.Created, job.EndTime, job.RunningDuration);
foreach (var t in job.Tasks)
{
Console.WriteLine("{0} {1}", t.Name, t.State);
foreach (var d in t.ErrorDetails)
{
Console.WriteLine("\t{0}\t{1}", d.Code, d.Message);
}
}
Console.WriteLine();
}
foreach (var p in context.AccessPolicies)
{
Console.WriteLine("policy Id={0}, name={1}, modified={2}, permissions={3}, duration={4}"
, p.Id, p.Name, p.LastModified, p.Permissions, p.Duration);
}
foreach (var l in context.Locators)
{
Console.WriteLine("Id={0}, Path={1}, expires={2}", l.Id, l.Path, l.ExpirationDateTime);
var p = l.AccessPolicy;
Console.WriteLine("policy Id={0}, name={1}, modified={2}, permissions={3}, duration={4}"
, p.Id, p.Name, p.LastModified, p.Permissions, p.Duration);
}
}
public void Reset()
{
Console.WriteLine("will reset");
foreach (var job in context.Jobs)
{
job.Delete();
}
foreach (var l in context.Locators)
{
context.Locators.Revoke(l);
}
foreach (var p in context.AccessPolicies)
{
context.AccessPolicies.Delete(p);
}
foreach (var a in context.Assets)
{
context.Assets.Delete(a);
}
}
private void Assets_OnUploadProgress(object sender, UploadProgressEventArgs e)
{
Console.WriteLine("Assets_OnUploadProgress: {0:0.00} %, {1} / {2}, {3:0.00} MB / {4:0.00} MB",
e.Progress, e.CurrentFile, e.TotalFiles, e.BytesSent / 1024 ^ 2, e.TotalBytes / 1024 ^ 2);
}
}
}
HLSConfiguration.xml:
<?xml version="1.0" encoding="utf-8" ?>
<!-- cf https://msdn.microsoft.com/en-us/library/hh973636.aspx -->
<taskDefinition xmlns="https://schemas.microsoft.com/iis/media/v4/TM/TaskDefinition#">
<name>Smooth Streams to Apple HTTP Live Streams</name>
<id>A72D7A5D-3022-45f2-89B4-1DDC5457C111</id>
<description xml:lang="en">Converts on-demand Smooth Streams encoded with H.264 (AVC) video and AAC-LC audio codecs to Apple HTTP Live Streams (MPEG-2 TS) and creates an Apple HTTP Live Streaming playlist (.m3u8) file for the converted presentation.</description>
<inputDirectory></inputDirectory>
<outputFolder>TS_Out</outputFolder>
<properties namespace="https://schemas.microsoft.com/iis/media/AppleHTTP#" prefix="hls">
<property name="maxbitrate" required="true" value="1600000" helpText="The maximum bit rate, in bits per second (bps), to be converted to MPEG-2 TS. On-demand Smooth Streams at or below this value are converted to MPEG-2 TS segments. Smooth Streams above this value are not converted. Most Apple devices can play media encoded at bit rates up to 1,600 Kbps."/>
<property name="manifest" required="false" value="" helpText="The file name to use for the converted Apple HTTP Live Streaming playlist file (a file with an .m3u8 file name extension). If no value is specified, the following default value is used: <ISM_file_name>-m3u8-aapl.m3u8"/>
<property name="segment" required="false" value="10" helpText="The duration of each MPEG-2 TS segment, in seconds. 10 seconds is the Apple-recommended setting for most Apple mobile digital devices."/>
<property name="log" required="false" value="" helpText="The file name to use for a log file (with a .log file name extension) that records the conversion activity. If you specify a log file name, the file is stored in the task output folder." />
<property name="encrypt" required="false" value="false" helpText="Enables encryption of MPEG-2 TS segments by using the Advanced Encryption Standard (AES) with a 128-bit key (AES-128)." />
<property name="pid" required="false" value="" helpText="The program ID of the MPEG-2 TS presentation. Different encodings of MPEG-2 TS streams in the same presentation use the same program ID so that clients can easily switch between bit rates." />
<property name="codecs" required="false" value="false" helpText="Enables codec format identifiers, as defined by RFC 4281, to be included in the Apple HTTP Live Streaming playlist (.m3u8) file." />
<property name="backwardcompatible" required="false" value="false" helpText="Enables playback of the MPEG-2 TS presentation on devices that use the Apple iOS 3.0 mobile operating system." />
<property name="allowcaching" required="false" value="true" helpText="Enables the MPEG-2 TS segments to be cached on Apple devices for later playback." />
<property name="passphrase" required="false" value="" helpText="A passphrase that is used to generate the content key identifier." />
<property name="key" required="false" value="" helpText="The hexadecimal representation of the 16-octet content key value that is used for encryption." />
<property name="keyuri" required="false" value="" helpText="An alternate URI to be used by clients for downloading the key file. If no value is specified, it is assumed that the Live Smooth Streaming publishing point provides the key file." />
<property name="overwrite" required="false" value="true" helpText="Enables existing files in the output folder to be overwritten if converted output files have identical file names." />
</properties>
<taskCode>
<type>Microsoft.Web.Media.TransformManager.SmoothToHLS.SmoothToHLSTask, Microsoft.Web.Media.TransformManager.SmoothToHLS, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35</type>
</taskCode>
</taskDefinition>
SampleCode5:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.WindowsAzure.MediaServices.Client;
using System.IO;
using System.Windows.Forms;
namespace WindowsAzureMediaServicesSample
{
public class SampleCode5
{
private System.Timers.Timer jobTimer;
private string runningJobId = null;
private bool jobCompleted = false;
private IJob job;
private CloudMediaContext context = null;
public SampleCode5(CloudMediaContext context)
{
this.context = context;
this.context.Assets.OnUploadProgress += Assets_OnUploadProgress;
}
public void Run()
{
#region upload or find MP4 initial asset
IAsset asset = null;
if (!Configuration.SampleFile3AlreadyUploaded)
{
asset = context.Assets.Create(Configuration.SampleFile3LocalPath, AssetCreationOptions.None); // an MP4 file
}
else
{
foreach (var a in context.Assets)
{
foreach (var f in a.Files)
{
if (string.Compare(f.Name, Configuration.SampleFile3, false) == 0)
{
asset = a;
break;
}
}
if (asset != null) break;
}
}
#endregion
#region submit or find job 1: MP4 => Smooth Streaming
job = context.Jobs.Where(j => j.Name == Configuration.JobNameSampleCode5).SingleOrDefault<IJob>();
if (job == null)
{
Console.WriteLine("submitting job");
var q = from p in context.MediaProcessors
where p.Name == "Windows Azure Media Encoder"
select p;
IMediaProcessor processor = q.FirstOrDefault<IMediaProcessor>();
if (processor == null)
{
throw new ApplicationException("media processor for smooth streaming not found");
}
job = context.Jobs.Create(Configuration.JobNameSampleCode5);
ITask task = job.Tasks.AddNew("sample encoding task",
processor,
"H.264 IIS Smooth Streaming - HD 720p CBR" /* cf https://msdn.microsoft.com/en-us/library/jj129582.aspx */,
TaskCreationOptions.None);
task.InputMediaAssets.Add(asset);
IAsset task1OutputAsset = task.OutputMediaAssets.AddNew(
"Smooth Streaming Output", true, AssetCreationOptions.None);
var q2 = from p in context.MediaProcessors
where p.Name == "Smooth Streams to HLS Task"
select p;
IMediaProcessor processor2 = q2.FirstOrDefault<IMediaProcessor>();
if (processor2 == null)
{
throw new ApplicationException("media processor for HLS task not found");
}
string hlsConfiguration = File.ReadAllText("HLSConfiguration.xml");
ITask task2 = job.Tasks.AddNew("sample HLS task 2",
processor2, hlsConfiguration,
TaskCreationOptions.None);
task2.InputMediaAssets.Add(task1OutputAsset); // start from output assets of previous task
task2.OutputMediaAssets.AddNew("HLS Output", true, AssetCreationOptions.None);
job.Submit();
}
#endregion
WaitForJob();
// If the job completes, have the files available for smooth streaming
if (jobCompleted)
{
Console.WriteLine("publishing smooth streaming and HLS results");
IAccessPolicy streamingPolicy = context.AccessPolicies.Create("Streaming policy",
TimeSpan.FromDays(5), AccessPermissions.Read);
foreach (var t in job.Tasks)
{
foreach (var outputAsset in t.OutputMediaAssets)
{
Console.WriteLine("output asset: {0} has {1} file(s)", outputAsset.Name, outputAsset.Files.Count);
foreach (var f in outputAsset.Files.Where(x => x.Name.EndsWith(".ism")))
{
if (f.Name.Contains("m3u8"))
{
#region publish HLS to a WAMS origin
Console.WriteLine("will create a new locator for HLS");
IFileInfo manifestFile = f;
ILocator originLocator = context.Locators.CreateOriginLocator(
outputAsset, streamingPolicy, DateTime.UtcNow.AddMinutes(-5));
string urlForClientStreaming = originLocator.Path + manifestFile.Name
+ "/manifest(format=m3u8-aapl)";
Console.WriteLine("URL to manifest for client HSL streaming: ");
Console.WriteLine(urlForClientStreaming);
Clipboard.SetText(urlForClientStreaming);
Console.WriteLine("---");
Console.ReadLine();
#endregion
}
else
{
#region publish smooth streaming to a WAMS origin
IFileInfo manifestFile = f;
ILocator originLocator = context.Locators.CreateOriginLocator(
outputAsset, streamingPolicy, DateTime.UtcNow.AddMinutes(-5));
string urlForClientStreaming = originLocator.Path + manifestFile.Name + "/manifest";
Console.WriteLine("URL to manifest for client smooth streaming: ");
Console.WriteLine(urlForClientStreaming);
Clipboard.SetText(urlForClientStreaming);
Console.WriteLine("---");
Console.ReadLine();
#endregion
#region download smooth streaming files locally
//string localFileName = Path.Combine(Configuration.OutputFolder, f.Name);
//Console.WriteLine("Asset {0}, downloading to {1}", outputAsset.Id, localFileName);
//f.OnDownloadProgress += new EventHandler<DownloadProgressEventArgs>(f_OnDownloadProgress);
//f.DownloadToFile(localFileName);
#endregion
}
}
}
}
}
else
{
Console.WriteLine("Please check job again later.");
}
}
private void WaitForJob()
{
runningJobId = job.Id;
if (job.State == JobState.Finished)
{
jobCompleted = true;
Console.WriteLine("");
Console.WriteLine("********************");
Console.WriteLine("Job state is: " + job.State + ".");
foreach (var t in job.Tasks)
{
Console.WriteLine("task {0} state={1} duration={2}, PerfMessage={3}",
t.Name, t.State, t.RunningDuration, t.PerfMessage);
}
Console.WriteLine("Job completed successfully.");
return;
}
// Expected polling interval in milliseconds. Adjust this
// interval as needed based on estimated job completion times.
const int JobProgressInterval = 10000;
// Create a timer with the specified interval, and an event
// to check job progress. This is an optional workaround
// because job progress checking is not currently implemented.
this.jobTimer = new System.Timers.Timer(JobProgressInterval);
// Hook up an event handler.
jobTimer.Elapsed += new System.Timers.ElapsedEventHandler(jobTimer_Elapsed);
jobTimer.Start();
Console.WriteLine("Please wait, checking job status...");
// Wait for timer to elapse.
Console.ReadLine();
// After the job progress event, stop timer.
jobTimer.Stop();
// Refresh the reference to the job object.
context.Detach(job);
job = context.Jobs.Where(j => j.Id == runningJobId).SingleOrDefault();
}
void f_OnDownloadProgress(object sender, DownloadProgressEventArgs e)
{
Console.WriteLine("Download progress: {0:0.00} %, {1:0.00} MB / {2:0.00} MB",
e.Progress, Convert.ToDouble(e.BytesDownloaded) / (1024 * 1024), Convert.ToDouble(e.TotalBytes) / (1024 * 1024));
}
void jobTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
//Get a refreshed job reference each time the event fires.
IJob theJob = context.Jobs.Where(j => j.Id ==
runningJobId).SingleOrDefault();
// Check and handle various possible job states.
switch (theJob.State)
{
case JobState.Finished:
jobCompleted = true;
Console.WriteLine("");
Console.WriteLine("********************");
Console.WriteLine("Job state is: " + theJob.State + ".");
foreach (var t in theJob.Tasks)
{
Console.WriteLine("task {0} state={1} duration={2}, PerfMessage={3}",
t.Name, t.State, t.RunningDuration, t.PerfMessage);
}
Console.WriteLine("Job completed successfully.");
Console.WriteLine("Press Enter to complete the job.");
jobTimer.Stop();
break;
case JobState.Queued:
case JobState.Scheduled:
case JobState.Processing:
Console.WriteLine("Job state is: " + theJob.State + ".");
Console.WriteLine("Continue waiting for the job to complete, or " +
"press Enter in the console to exit without waiting.");
break;
case JobState.Error:
Console.WriteLine("Error:");
foreach (var t in theJob.Tasks)
{
Console.WriteLine("task {0} state={1}", t.Name, t.State);
Console.WriteLine("\tdetails:");
foreach (var d in t.ErrorDetails)
{
Console.WriteLine("\t{0}\t{1}", d.Code, d.Message);
}
Console.WriteLine("---");
//Console.WriteLine("task body: '{0}'", t.TaskBody);
}
jobTimer.Stop();
break;
default:
Console.WriteLine(theJob.State.ToString());
break;
}
// Detach the job to prevent the reference going stale.
context.Detach(theJob);
}
void Assets_OnUploadProgress(object sender, UploadProgressEventArgs e)
{
Console.WriteLine("Assets_OnUploadProgress: {0:0.00} %, {1} / {2}, {3:0.00} MB / {4:0.00} MB",
e.Progress, e.CurrentFile, e.TotalFiles,
Convert.ToDouble(e.BytesSent) / (1024 * 1024), Convert.ToDouble(e.TotalBytes / (1024 * 1024)));
}
}
}
You can download the C# and XML files from https://sdrv.ms/NsNMYX | Vous pouvez télécharger le code C# et XML depuis https://sdrv.ms/NsNMYX |
Let’s execute it | Exécutons-le |
…
…
A few containers were created in the Windows Azure blob storage account. One of these is the source asset: | quelques conteneurs ont été créés dans le compte de stockage blobs Windows Azure. L’un d’eux est l’asset source: |
Note that most of them are intermediary assets that will be destroyed when the job is finished. | On notera que la plupart de ces conteneurs sont temporaires et seront détruits quand le job sera terminé. |
…
Blob storage now looks like this: | Le compte de stockage ressemble alors à cela: |
…
Paste the URL in a HTML file that contains the Smooth Streaming player | Collons l’URL dans le fichier HTML qui contient le player smooth streaming |
paste | collons |
From a browser, you get the video (which also happens to talk about Windows Azure by the way!) | Depuis un navigateur, on a la vidéo (qui se trouve être sur Windows Azure aussi!) |
I don’t have a screenshot of the same video on an iPhone from the smooth.html page, but I tested it also. | Je n’ai pas de copie d’écran de la même vidéo jouée depuis un iPhone à partir de la page smooth.html, mais je l’ai aussi testé. |
The Web site code and the XAP file can be downloaded from https://sdrv.ms/NbUyEe | Le code du site Web et le fichier XAP peuvent être téléchargés depuis https://sdrv.ms/NbUyEe |