Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.net MVC Streaming MP4 to iDevice issue

Ive got a bit of a issue with a piece of code I've been working on to serve video. The code is below:

public ResumingFileStreamResult GetMP4Video(string videoID)
    {
        if (User.Identity.IsAuthenticated)
        {
            string clipLocation = string.Format("{0}\\Completed\\{1}.mp4", ConfigurationManager.AppSettings["VideoLocation"].ToString(), videoID);

            FileStream fs = new FileStream(clipLocation, FileMode.Open, FileAccess.Read);

            ResumingFileStreamResult fsr = new ResumingFileStreamResult(fs, "video/mp4");

            return fsr;
        }
        else
        {
            return null;
        }
    }

This is my HTML code:

<video controls preload poster="@Url.Content(string.Format("~/Videos/{0}_2.jpg", Model.VideoID))">
    <source src="@Url.Action("GetMP4Video", "Video", new { videoID = Model.VideoID })" type="video/mp4" />
    <source src="@Url.Action("GetWebMVideo", "Video", new { videoID = Model.VideoID })" type="video/webm" />

        <object id="flowplayer" data="@Url.Content("~/Scripts/FlowPlayer/flowplayer-3.2.14.swf")" type="application/x-shockwave-flash" width="640" height="360">
            <param name="movie" value="@Url.Content("~/Scripts/FlowPlayer/flowplayer-3.2.14.swf")" />
            <param name="allowfullscreen" value="true" />
            <param name="flashvars" value="config={'playlist':['@Url.Content(string.Format("~/Videos/{0}_2.jpg", Model.VideoID))',{'url':'@Url.Action("GetMP4Video", "Video", new { videoID = Model.VideoID })','autoPlay':false}]}" />
        </object>
</video>

My problem is this setup seems to work fine in all browsers from my desktop but when I try to load the page using my iPad or iPhone it just shows the play icon with a line through it indicating it can't play it. I tried changing the source for the mp4 video to a direct link to the mp4 video and straight away the iPad started playing it.

Is there something special I need to do that I've missed in ordered to make my method compatible for iDevices? Any help on this would be appreciated.

like image 473
Chris Sainty Avatar asked Dec 27 '22 19:12

Chris Sainty


2 Answers

To make your videos playable on iOS devices, you need to implement support for byte-range (or partial) requests. These type of requests allow to download not the whole content, but partially, chunk by chunk (typical streaming). And this is the only way how iOS devices get and play videos on the page.

Partial requests use Range header to tell the server next chunk position and size. Server on the other side responses with 206 Partial Content and requested chunk contents.

You can find several implementations for ASP.NET handlers which can handle partial requests in internet. I suggest to use StaticFileHandler for this: easy to install and also has caching capabilities out of box. It also can be delivered via Nuget, but the package is called Talifun.Web.

To configure StaticFileHandler, register the handler in web.config for mp4 files and configure it in separate configuration section:

<configuration>
  <configSections>
    <section name="StaticFileHandler" type="Talifun.Web.StaticFile.Config.StaticFileHandlerSection, Talifun.Web" requirePermission="false" allowDefinition="MachineToApplication"/>
  </configSections>

  <StaticFileHandler webServerType="NotSet">
    <!-- The defaults to use when an extension is found that does not have a specific rule  -->
    <fileExtensionDefault name="Default" serveFromMemory="true" maxMemorySize="100000" compress="true"/>
    <!-- Specific rules for extension types -->
    <fileExtensions>
        <fileExtension name="VideoStaticContent" extension="3gp, 3g2, asf, avi, dv, flv, mov, mp4, mpg, mpeg, wmv" serveFromMemory="true" maxMemorySize="100000" compress="false"/>
    </fileExtensions>
  </StaticFileHandler>

  <system.webServer>
    <handlers>
      <add name="StaticContentHandler" verb="GET,HEAD" path="*.mp4" type="Talifun.Web.StaticFile.StaticFileHandler, Talifun.Web"/>
    </handlers>
  </system.webServer>
</configuration>

If can also easily apply your custom logic, for example authorization or custom video file source, by creating your ASP.NET handler and calling StaticFileManager directly.

public class MyOwnVideoHandler : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        // Authorization or any other stuff.
        ...

        // Get file from your storage.
        FileInfo file = ...;

        // Serve the file with StaticFileHandler.
        StaticFileManager.Instance.ProcessRequest(new HttpContextWrapper(context), file);
    }
}

Also, you can take a look at Scott Mitchell's article about partial requests for details and use the handler written by its author: it worked for me, but it doesn't have caching capabilities.

like image 141
whyleee Avatar answered Dec 29 '22 07:12

whyleee


@whyleee is correct. I can't speak on how good StaticFileHandler is, but I had been facing this same issue myself and it was driving me crazy. A Range header must be included in the Request and Response headers for this to work properly. For example, a slight modification to your code, with some code from my own Handler, looks like this (keep in mind that this is using an .ashx handler):

//First, accept Range headers.
context.Response.AddHeader("Accept-Ranges", "bytes")

//Then, read all of the bytes from the file you are requesting.
Dim file_info As New System.IO.FileInfo(clipLocation)
Dim bytearr As Byte() = File.ReadAllBytes(file_info.FullName)

//Then, you will need to check for a range header, and then serve up a 206 Partial Content status code in your response.
Dim startbyte As Integer = 0
If Not context.Request.Headers("Range") Is Nothing Then
    //Get the actual byte range from the range header string, and set the starting byte.
    Dim range As String() = context.Request.Headers("Range").Split(New Char() {"="c, "-"c})
    startbyte = Convert.ToInt64(range(1))

    //Set the status code of the response to 206 (Partial Content) and add a content range header.
    context.Response.StatusCode = 206
    context.Response.AddHeader("Content-Range", String.Format(" bytes {0}-{1}/{2}", startbyte, bytearr.Length - 1, bytearr.Length))
End If

//Finally, write the video file to the output stream, starting from the specified byte position.
context.Response.OutputStream.Write(bytearr, startbyte, bytearr.Length - startbyte)

As I said though, this is code for an .ashx handler so I'm not sure how much it applies to your situation, but I hope it helps you out!

like image 23
Martin-Brennan Avatar answered Dec 29 '22 07:12

Martin-Brennan