如何透明地将文件流式传输到浏览器?

时间:2013-10-15 15:31:25

标签: java http iis coldfusion streaming

受控环境:IE8,IIS 7,ColdFusion

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

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

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

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

看起来最有希望的解决方案是Using ColdFusion To Stream Files To The Client Without Loading The Entire File Into Memory,但似乎提供的唯一优势是文件在提供给浏览器时不会完全加载到内存中,但浏览器仍然等到最后转移,然后在Winwodw媒体播放器中打开文件。

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

3 个答案:

答案 0 :(得分:0)

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

摘自该文章:

  

了解Windows Media服务器与Web服务器之间的差异

     

您可以从运行Windows Media Services的服务器或从Web服务器向播放器(如Windows Media Player)传输基于Windows Media的内容。服务器和播放器既可以在Internet上使用,也可以在Intranet上使用,它们可以通过防火墙分开。虽然Windows Media服务器专门用于流式传输基于Windows Media的内容,但标准Web服务器却不是。如果您决定使用Web服务器,则需要了解内容传送方式的不同,这会影响播放质量。

     

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

     

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

     

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

答案 1 :(得分:0)

工作解决方案:

我终于找到了一个支持寻求的解决方案,而且不涉及太多的工作。我基本上创建了一个HttpForwardRequest组件,它通过向指定的媒体URL发出新的HTTP请求,同时保留其他初始servlet请求详细信息(如HTTP头),将请求处理委派给Web服务器。然后,Web服务器的响应将通过管道传递到servlet的响应输出流中。

在我们的案例中,由于Web服务器(ISS 7.0)已经知道如何进行HTTP流式传输,这是我们唯一需要做的事情。

注意:I have tried with 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方法,根据Using ColdFusion To Stream Files To The Client Without Loading The Entire File Into Memory

中的实现封装了流媒体流程

注意:它也适用于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解决方案)将永远不如使用媒体服务器那样高效,如the article中所述,@ Miguel- F建议

主要下降:之前的实施不支持寻求实际上可能使解决方案几乎无法使用的内容。

答案 2 :(得分:0)

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

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

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

我的解决方案(我没有真正的解释)是在 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 的位置:(...来自流文件的第一行)

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

但只是猜测...