WORKING SOLUTION:
I finally managed to find a solution that supports seeking and that doesn't involve too much work. I basically created an HttpForwardRequest
component that delegates the request handling to the web server by issuing a new HTTP request to the specified media URL while preserving other initial servlet request details, such as HTTP headers. The web server's response will then be piped into the servlet's response output stream.
In our case, since the web server (ISS 7.0) already know how to do HTTP streaming, that's the only thing we have to do.
Note: I have tried with getRequestDispatcher('some_media_url').forward(...)
but it seems that it cannot serve media files with the correct headers.
HttpForwardRequest
code:
<cfcomponent output="no">
<cffunction name="init" access="public" returntype="HttpForwardRequest" output="no">
<cfargument name="url" type="string" required="yes" hint="The URL to which the request should be forwarded to.">
<cfargument name="requestHeaders" type="struct" required="yes" hint="The HTTP request headers.">
<cfargument name="response" type="any" required="yes" hint=" The servlet's response object.">
<cfargument name="responseHeaders" type="struct" required="no" default="#{}#" hint="Custom response headers to override the initial request response headers.">
<cfset variables.instance = {
url = arguments.url,
requestHeaders = arguments.requestHeaders,
response = arguments.response,
responseHeaders = arguments.responseHeaders
}>
<cfreturn this>
</cffunction>
<cffunction name="send" access="public" returntype="void" output="no">
<cfset var response = variables.instance.response>
<cfset var outputStream = response.getOutputStream()>
<cfset var buffer = createBuffer()>
<cftry>
<cfset var connection = createObject('java', 'java.net.URL')
.init(variables.instance.url)
.openConnection()>
<cfset setRequestHeaders(connection)>
<cfset setResponseHeaders(connection)>
<cfset var inputStream = connection.getInputStream()>
<cfset response.setStatus(connection.getResponseCode(), connection.getResponseMessage())>
<cfloop condition="true">
<cfset var bytesRead = inputStream.read(buffer, javaCast('int', 0), javaCast('int', arrayLen(buffer)))>
<cfif bytesRead eq -1>
<cfbreak>
</cfif>
<cftry>
<cfset outputStream.write(buffer, javaCast('int', 0), bytesRead)>
<cfset outputStream.flush()>
<!---
Connection reset by peer: socket write error
The above error occurs when users are seeking a video.
That is probably normal since I assume the client (e.g. Window Media Player)
closes the connection when seeking.
--->
<cfcatch type="java.net.SocketException">
<cfbreak>
</cfcatch>
</cftry>
</cfloop>
<cffinally>
<cfif not isNull(inputStream)>
<cfset inputStream.close()>
</cfif>
<cfif not isNull(connection)>
<cfset connection.disconnect()>
</cfif>
</cffinally>
</cftry>
</cffunction>
<cffunction name="setRequestHeaders" access="private" returntype="void" output="no">
<cfargument name="connection" type="any" required="yes">
<cfset var requestHeaders = variables.instance.requestHeaders>
<cfloop collection="#requestHeaders#" item="local.key">
<cfset arguments.connection.setRequestProperty(key, requestHeaders[key])>
</cfloop>
</cffunction>
<cffunction name="setResponseHeaders" access="private" returntype="void" output="no">
<cfargument name="connection" type="any" required="yes">
<cfset var response = variables.instance.response>
<cfset var responseHeaders = variables.instance.responseHeaders>
<cfset var i = -1>
<!--- Copy connection headers --->
<cfloop condition="true">
<cfset i = javaCast('int', i + 1)>
<cfset var key = arguments.connection.getHeaderFieldKey(i)>
<cfset var value = arguments.connection.getHeaderField(i)>
<cfif isNull(key)>
<cfif isNull(value)>
<!--- Both, key and value are null, break --->
<cfbreak>
</cfif>
<!--- Sometimes the key is null but the value is not, just ignore and keep iterating --->
<cfcontinue>
</cfif>
<cfset setResponseHeader(key, value)>
</cfloop>
<!--- Apply custom headers --->
<cfloop collection="#responseHeaders#" item="key">
<cfset setResponseHeader(key, responseHeaders[key])>
</cfloop>
</cffunction>
<cffunction name="setResponseHeader" access="private" returntype="void" output="no">
<cfargument name="key" type="string" required="yes">
<cfargument name="value" type="string" required="yes">
<cfset var response = variables.instance.response>
<cfif arguments.key eq 'Content-Type'>
<cfset response.setContentType(arguments.value)>
<cfelse>
<cfset response.setHeader(arguments.key, arguments.value)>
</cfif>
</cffunction>
<cffunction name="createBuffer" access="private" returntype="any" output="no">
<cfreturn repeatString("12345", 1024).getBytes()>
</cffunction>
</cfcomponent>
cf_streamurl
code:
<cfparam name="attributes.url" type="url">
<cfif thisTag.executionMode neq 'start'>
<cfexit>
</cfif>
<cfset pageContext = getPageContext()>
<cfset requestHeaders = {
'Authorization' = 'Anonymous'
}>
<cfset structAppend(requestHeaders, getHTTPRequestData().headers, false)>
<cfset pageContext.setFlushOutput(false)>
<!--- Forward the request to IIS --->
<cfset new references.cfc.servlet.HttpForwardRequest(
attributes.url,
requestHeaders,
pageContext.getResponse().getResponse()
).send()>
You can then use the cf_streamurl
custom tag like:
<cf_streamurl url="http://sh34lprald94/media_stream/unprotected/trusts.mp4"/>
IMPORTANT: It only supports Anonymous authentication for now.
First half-working attempt (historical purpose only):
We found a solution (which was actually quite simple) that suits our needs by inspecting the HTTP headers of the response packet and looking at the mime type returned by IIS when letting it server the media file.
The issue was that when trying to serve the file content to the browser using ColdFusion, we had to use one of the Window Media Services mime types to force the browser to delegate the handling to Window Media Player directly (which is then able to stream the file).
File extension MIME type
.asf video/x-ms-asf
.asx video/x-ms-asf
.wma audio/x-ms-wma
.wax audio/x-ms-wax
.wmv audio/x-ms-wmv
.wvx video/x-ms-wvx
.wm video/x-ms-wm
.wmx video/x-ms-wmx
.wmz application/x-ms-wmz
.wmd application/x-ms-wmd
The first step for solving the issue was to write a function that would resolve the mime type correctly based on the file's extension. IIS has that knowledge already, however I haven't found a way of querying it's MIME registry yet.
Note: wmsMimeTypes
is a struct used as a map to lookup WMS mime types.
<cffunction name="getMimeType" access="public" returntype="string">
<cfargument name="fileName" type="string" required="yes">
<cfset var mimeType = 'application/x-unknown'>
<cfset var ext = this.getFileExtension(arguments.fileName)>
<cfif structKeyExists(this.wmsMimeTypes, ext)>
<cfreturn this.wmsMimeTypes[ext]>
</cfif>
<!--- TODO: Is there a way to read the IIS MIME registry? --->
<cfregistry action="get" branch="HKEY_CLASSES_ROOT\.#ext#" entry="Content Type" variable="mimeType">
<cfreturn mimeType>
</cffunction>
Then we implemented a stream
method like below that encapsulates the streaming process based on the implementation found in Using ColdFusion To Stream Files To The Client Without Loading The Entire File Into Memory
Note: It also works with cfcontent
, but I read that it was quite inefficient because it's consuming too much resources, especially because it loads the entire file in memory before flushing to the browser.
<cffunction name="stream" access="public" returntype="void">
<cfargument name="file" type="string" required="yes">
<cfargument name="mimeType" type="string" required="no">
<cfscript>
var fileName = getFileFromPath(arguments.file);
var resolvedMimeType = structKeyExists(arguments, 'mimeType')? arguments.mimeType : this.getMimeType(fileName);
var javaInt0 = javaCast('int', 0);
var response = getPageContext().getResponse().getResponse();
var binaryOutputStream = response.getOutputStream();
var bytesBuffer = repeatString('11111', 1024).getBytes();
var fileInputStream = createObject('java', 'java.io.FileInputStream').init(javaCast('string', getRootPath() & arguments.file));
getPageContext().setFlushOutput(javaCast('boolean', false));
response.resetBuffer();
response.setContentType(javaCast('string', resolvedMimeType));
try {
while (true) {
bytesRead = fileInputStream.read(bytesBuffer, javaInt0, javaCast('int', arrayLen(bytesBuffer)));
if (bytesRead eq -1) break;
binaryOutputStream.write(bytesBuffer, javaInt0, javaCast('int', bytesRead));
binaryOutputStream.flush();
}
response.reset();
} finally {
if (not isNull(fileInputStream)) fileInputStream.close();
if (not isNull(binaryOutputStream)) binaryOutputStream.close();
}
</cfscript>
</cffunction>
You must NOT set the Content-Disposition
header or the browser will download the file instead of delegating the control to WMP.
Note: Letting the web server to stream the file to the client (or the CF solution we used) will never be as efficient as using a media server, like stated in the article that @Miguel-F suggested.
MAJOR DOWNSIDE: The previous implementation will not support seeking which actually might make the solution almost unusable.
somedomain.com/somefile.mp3
, it will open WMP which will effectively stream the file without issues (not sure how IIS is serving the file). However I cannot let users open the file directly so I tried various ways to serve the file to the browser using ColdFusion. It works but the browser will NOT open WMP until the file has finished to download, unlike when the browser GETsomedomain.com/somefile.mp3
. – Nephologysomefile.mp3
directly from the browser it is handing off the processing to WMP. WMP fires up and is able to initiate communications over your network via UDP (it's preferred protocol, see my answer). Using UDP between WMP and IIS it "streams" the file. When you use ColdFusion to downloadsomefile.mp3
the browser is handling the communications so UDP is out. It must communicate using HTTP to IIS and is unable to "stream" the file. Note this is just how I believe it works, I am not an expert at this – Mote