Search code examples
ioscoldfusionjwplayermp4

Serve mp4 file through coldfusion and play with jwplayer


I have an web application in coldfusion which record videos and serve videos to user.

The video is working fine on android and desktop browsers but it is giving me error "Error loading media: File could not be played" in IOS.

Here is my JWPlayer code which is currently working.

jwplayer("element").setup({
  file: "/video.cfm?token=4514_129_9B2F727D-5056-A85D-6EBE3E48FC2AB9C6",
  image: "path/to/image",
  width: 450,
  height: 360,
  type: "mp4",
  logo: {
     file: 'path/to/logo',
     link: 'example.com',
     hide : true
  }
});

Here is my video.cfm to server mp4 after verification.

<cfset videoFile = 'path\to\file'>
<cfset fileInfo = GetFileInfo(videoFile)>
<cfset length = fileInfo.size>
<cfset start = 0>
<cfset end = fileInfo.size - 1>
<cfheader name="Content-type" value="video/mp4">
<cfheader name="Accept-Ranges" value="0-#length#">
<cfheader name="Content-Range" value="bytes #start#-#end#/#fileInfo.size#">
<cfheader name="Content-Length" value="#length#">
<cfcontent file="#videoFile#" type="video/mp4">

I have tried some solution by adding some header. But that doesn't work. Can anyone help me to sort out the problem.


Solution

  • I am able to resolve my problem. iOS uses a partial content header to run videos. Thanks to rickward for this lovely solution: Media Delivery to iPhones and iPads. I have made some little changes and it started working for me.

    Here is the final video.cfm file.

    <cfset videoPath = 'path\to\mp4\file'>
    <cfif FileExists(videoPath)>
        <cfset fileInfoVar = GetFileInfo(videoPath)>
        <cfheader name="Last-Modified" value="#fileInfoVar.Lastmodified#">
        <cfheader name="ETag" value="#hash(videoPath, 'MD5')#">
        <cfheader name="Content-Location" value="http://example.com/video.cfm">
    
        <cfif structKeyExists(GetHttpRequestData().headers, 'Range')>
            <cfset rangeDownload(videoPath)>
        <cfelse>
            <cffile action="readbinary" file="#videoPath#" variable="theData">
            <cfscript>
                context = getPageContext();
                context.setFlushOutput(false);
                response = context.getResponse().getResponse();
                response.setContentType("video/mp4");
                response.setContentLength(arrayLen(theData));
    
                out = response.getOutputStream();
                out.write(theData);
                out.flush();
                out.close();
            </cfscript>
        </cfif>
    </cfif>
    
    <cffunction name="rangeDownload" returnType="void" output="yes">
        <cfargument name="file" type="string" required="true" hint="path to file">
    
        <cfset var l = {}>
        <cfset l.request = GetHttpRequestData()>
    
        <cffile action="readbinary" file="#ARGUMENTS.file#" variable="l.theData">
    
        <cfset l.size = arrayLen(l.theData)>
        <cfset l.length = l.size>
        <cfset l.start  = 0>
        <cfset l.end = l.size - 1>
    
        <!--- Now that we've gotten so far without errors we send the accept range header
        /* At the moment we only support single ranges.
         * Multiple ranges requires some more work to ensure it works correctly
         * and comply with the spesifications: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2
         *
         * Multirange support annouces itself with:
         * header('Accept-Ranges: bytes');
         *
         * Multirange content must be sent with multipart/byteranges mediatype,
         * (mediatype = mimetype)
         * as well as a boundry header to indicate the various chunks of data.
         */
        --->
        <cfheader name="Accept-Ranges" value="0-#l.length#">
        <!---<cfheader name="Accept-Ranges" value="bytes"> --->
        <!---
          multipart/byteranges
          http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 --->
        <cfif structKeyExists(l.request.headers, 'Range')>
    
            <cfset l.c_start = l.start>
            <cfset l.c_end = l.end>
    
            <!--- Extract the range string --->
            <cfset l.range = ListGetAt(l.request.headers.range, 2, '=')>
            <!--- Make sure the client hasn't sent us a multibyte range --->
            <cflog file="rangeDownload" text="#l.range#" />
            <cfif l.range contains ','>
                <!--- (?) Should this be issued here, or should the first
                 range be used? Or should the header be ignored and
                 we output the whole content?
                --->
                <cfheader statusCode = "416" statusText = "Requested Range Not Satisfiable">
                <cfheader name="Content-Range" value="bytes #l.start#-#l.end#/#l.size#">
                <!--- (?) Echo some info to the client? --->
                <cfabort>
            </cfif>
            <!--- If the range starts with an '-' we start from the beginning
                If not, we forward the file pointer
                And make sure to get the end byte if specified --->
            <cfif Left(l.range, 1) eq '-'>
            <!--- The n-number of the last bytes is requested --->
                <cfset l.c_start = l.size - Mid(l.range, 2, Len(l.range))>
            <cfelse>
                <cfset l.rangeArray = ListToArray(l.range, '-')>
                <cfset l.c_start = l.rangeArray[1]>
                <cfif ArrayLen(l.rangeArray) eq 2 and val(l.rangeArray[2]) gt 0>
                    <cfset l.c_end = l.rangeArray[2]>
                <cfelse>
                    <cfset l.c_end = l.size>
                </cfif>
            </cfif>
            <!---
            /* Check the range and make sure it's treated according to the specs.
             * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
             */
            // End bytes can not be larger than l.end. --->
            <cfif l.c_end gt l.end>
                <cfset l.c_end = l.end>
            </cfif>
    
            <!--- Validate the requested range and return an error if it's not correct. --->
            <cfif l.c_start gt l.c_end || l.c_start gt (l.size - 1) || l.c_end gte l.size>
                <cfheader statusCode = "416" statusText = "Requested Range Not Satisfiable">
                <cfheader name="Content-Range" value="bytes #l.start#-#l.end#/#l.size#">
                <!--- (?) Echo some info to the client? --->
                <cfabort>
            </cfif>
    
            <cfset l.start = l.c_start>
            <cfset l.end = l.c_end>
            <cfset l.length = l.end - l.start + 1><!--- Calculate new content length --->
    
    
            <cfscript>
                context = getPageContext();
                context.setFlushOutput(false);
                response = context.getResponse().getResponse();
                response.setContentType("video/mp4");
                response.setContentLength(l.length);
            </cfscript>
            <cfheader statusCode = "206" statusText = "Partial Content">
    
        </cfif>
    
        <!--- Notify the client the byte range we'll be outputting --->
        <cfheader name="Content-Range" value="bytes #l.start#-#l.end#/#l.size#">
        <cfheader name="Content-Length" value="#l.length#">
    
        <cfscript>
            // Start buffered download
            out = response.getOutputStream();
            // write the portion requested
            out.write(l.theData, javacast('int', l.start), javacast('int', l.length));
            out.flush();
            out.close();
        </cfscript>
    </cffunction>