8

受控环境:IE8、IIS 7、ColdFusion

当从 IE 发出指向媒体文件(如 .mp3、.mpeg 等)的 GET 请求时,浏览器将启动相关的应用程序(Window Media Player),我猜 IIS 提供文件的方式允许应用程序流式传输它。

我们希望能够完全控制文件的流式传输过程,以便我们可以在特定时间只允许特定用户使用。出于这个原因,我们不能简单地让 IIS 直接提供文件,而是希望使用 ColdFusion 来提供文件。

我们尝试了几种不同的方法,但在每种情况下,浏览器都会在启动外部应用程序之前下载整个文件内容。这就是我们想要避免的。

请注意,我们不想要基于 NTFS 权限的解决方案。

看起来最有希望的解决方案是使用 ColdFusion 将文件流式传输到客户端而不将整个文件加载到内存中,但似乎提供的唯一优势是文件在提供给浏览器时不会完全加载到内存中,但是浏览器仍然等到传输结束,然后在 Winwodw Media Player 中打开文件。

有没有办法使用 ColdFusion 或 Java 将文件流式传输到浏览器并让浏览器将处理委托给关联的应用程序,就像我们让 IIS 直接提供文件一样?

4

3 回答 3

1

工作解决方案:

我终于设法找到了一个支持寻求并且不涉及太多工作的解决方案。我基本上创建了一个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 建议的文章中所述。

主要缺点:以前的实现不支持搜索,这实际上可能使解决方案几乎无法使用。

于 2013-10-15T18:28:28.730 回答
0

这对于评论来说太长了
我找到了另一个可能对您有帮助的参考资料 - TechNet Windows Media Encoder

摘自那篇文章:

了解 Windows Media 服务器和 Web 服务器之间的区别

您可以将基于 Windows Media 的内容从运行 Windows Media Services 的服务器或从 Web 服务器流式传输到播放器,例如 Windows Media Player。服务器和播放器既可以在 Internet 上使用,也可以在 Intranet 上使用,并且可以通过防火墙分隔。尽管 Windows Media 服务器专为流式传输基于 Windows Media 的内容而设计,但标准的 Web 服务器并非如此。如果您决定使用 Web 服务器,则需要注意内容交付方式的差异,这可能会影响播放质量。

Web 服务器和 Windows Media 服务器之间发送数据的方法不同。Web 服务器旨在尽可能快地发送尽可能多的数据。这是发送包含静态图像、文本和网页脚本的数据包的首选方法,但它不是发送包含流媒体的数据包的最佳方法。流媒体应该实时传送,而不是大爆发,并且播放器应该在渲染之前接收数据包。

Windows Media 服务器根据它在向播放器发送流时接收到的反馈信息来计量数据包的传递。当播放器以这种方式接收数据包时,演示更有可能是流畅的。由于带宽使用受到控制,更多用户可以同时连接到您的站点并接收无中断的流。

Web 服务器不支持多比特率视频。当文件从 Web 服务器流式传输时,不会监控传输质量,也无法调整比特率。Web 服务器不能使用首选的传输协议,即用户数据报协议 (UDP),因此流的传输更有可能在播放器缓冲数据时被静默期中断。使用 Web 服务器也无法实现实时流式传输和多播。

于 2013-10-15T18:02:07.460 回答
0

对工作解决方案的一点观察(也许对某人有帮助)......

就我而言,我使用 CF9(但在 CF11 上进行了测试,结果相同),并且文件是从 CouchDB 服务器流式传输的。

当流文件是文本文件时,文件的第一行在第一次尝试时被 0 值覆盖(损坏)并且在第二次尝试时是正确的(非常奇怪的 inded)但无论如何都不会下载文件。

我的解决方案(我没有真正的解释)是在 setResponseHeaders 函数中移动 cfbreak

我改变了这段代码:

        <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>

有了这个:

        <cfif isNull(key)>
            <cfbreak>
        </cfif>

感谢@plalx 的解决方案。帮了我很多!

稍后编辑一些细节

流式数据的结构如下:

  1. 标头
  2. 空行
  3. b9b(我认为它是文件内容开始的标志)
  4. 文件内容
  5. 0(零值 - 我认为它是文件内容结束的标志)

在我看来,getHeaderFieldKey 和 getHeaderField 函数认为“b9b”行是一个 {key: 'b9b', value:(content of the file) } 对并将其写入标题,然后读取 0 作为 {key : null, value:0} 对并用 {key: null, value:0} 对覆盖标题 b9b: (...第一行来自流文件) 的位置

将 cfbreak 移到 isNull(key) 上,停止(错误地)将文件内容作为标题字段读取。

不过只是猜测...

于 2021-04-27T08:51:54.680 回答