工作解决方案:
我终于设法找到了一个支持寻求并且不涉及太多工作的解决方案。我基本上创建了一个HttpForwardRequest
组件,通过向指定的媒体 URL 发出新的 HTTP 请求,同时保留其他初始 servlet 请求详细信息(例如 HTTP 标头),将请求处理委托给 Web 服务器。然后,Web 服务器的响应将通过管道传输到 servlet 的响应输出流中。
在我们的例子中,由于 Web 服务器 (ISS 7.0) 已经知道如何进行 HTTP 流式传输,这是我们唯一需要做的事情。
注意:我已经尝试过,getRequestDispatcher('some_media_url').forward(...)
但它似乎无法提供具有正确标题的媒体文件。
HttpForwardRequest
代码:
<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
代码:
<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()>
然后,您可以使用cf_streamurl
自定义标签,例如:
<cf_streamurl url="http://sh34lprald94/media_stream/unprotected/trusts.mp4"/>
重要提示:它目前仅支持匿名身份验证。
第一半工作尝试(仅历史目的):
我们通过检查响应数据包的 HTTP 标头并查看 IIS 在让其为媒体文件提供服务时返回的 mime 类型,找到了适合我们需求的解决方案(实际上非常简单)。
问题是,当尝试使用 ColdFusion 将文件内容提供给浏览器时,我们必须使用一种Window Media Services mime 类型来强制浏览器直接将处理委托给 Window Media Player(然后它能够流式传输文件)。
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
解决该问题的第一步是编写一个函数,该函数将根据文件的扩展名正确解析 mime 类型。IIS 已经有了这些知识,但是我还没有找到查询它的 MIME 注册表的方法。
注意:wmsMimeTypes
是一个结构,用作查找 WMS mime 类型的映射。
<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>
然后我们实现了一个stream
类似下面的方法,该方法基于使用 ColdFusion 将文件流式传输到客户端而不将整个文件加载到内存中找到的实现来封装流式处理过程
注意:它也适用于cfcontent
,但我读到它效率很低,因为它消耗了太多资源,特别是因为它在刷新到浏览器之前将整个文件加载到内存中。
<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>
您不得设置Content-Disposition
标头,否则浏览器将下载文件而不是将控件委托给 WMP。
注意:让 Web 服务器将文件流式传输到客户端(或我们使用的 CF 解决方案)永远不会像使用媒体服务器那样高效,就像@Miguel-F 建议的文章中所述。
主要缺点:以前的实现不支持搜索,这实际上可能使解决方案几乎无法使用。