Method for Streaming Data from Browser to Server via Http

How to process streaming HTTP GET data?

So I found the answer, and it's Server-sent events. It basically enables one-way http-streams that the browser can handle a chunk at a time. It can be a little tricky because some existing stream libs are broken (they don't assume you have \n in your stream, and hence you get partial data), or have little documentation. But it's not hard to roll your own (once you figure it out).

You can define your sse_transform like this:

// file sse_stream.coffee
var Transform = require('stream').Transform;
var util = require('util');
util.inherits(SSEStream, Transform);

function SSEStream(option) {
Transform.call(this, option);
this.id = 0;
this.retry = (option && option.retry) || 0;
}

SSEStream.prototype._transform = function(chunk, encoding, cb) {
var data = chunk.toString();
if (data) {
this.push("id:" + this.id + "\n" +
data.split("\n").map(function (e) {
return "data:" + e
}).join("\n") + "\n\n");
//"retry: " + this.retry);
}
this.id++;
cb();
};

SSEStream.prototype._flush = function(next) {
this.push("event: end\n" + "data: end" + "\n\n");
next();
}

module.exports = SSEStream;

Then on the server side (I was using express), you can do something like this:

sse_stream = require('sse_stream')
app.get '/blob', (req, res, next) ->
sse = new sse_stream()

# It may differ here for you, but this is just a stream source.
blobStream = repo.git.streamcmd("cat-file", { p: true }, [blob.id])

if (req.headers["accept"] is "text/event-stream")
res.type('text/event-stream')
blobStream.on("end", () -> res.removeAllListeners()).stdout
.pipe(
sse.on("end", () -> res.end())
).pipe(res)
else
blobStream.stdout.pipe(res)

Then on the browser side, you can do:

    source = new EventSource("/blob")
source.addEventListener('open', (event) ->
console.log "On open..."
, false)
source.addEventListener('message', (event) ->
processData(event.data)
, false)
source.addEventListener('end', (event) ->
console.log "On end"
source.close()
, false)
source.addEventListener('error', (event) ->
console.log "On Error"
if event.currentTarget.readyState == EventSource.CLOSED
console.log "Connection was closed"
source.close()
, false)

Notice that you need to listen for the event 'end', that is sent from the server in the transform stream's _flush() method. Otherwise, EventSource in the browser is just going to request the same file over and over again.

Note that you can use libraries on the server side to generate SSE. On the browser side, you can use portal.js to handle SSE. I just spelt things out, so you can see how things would work.

Pushing data from the server to browser via http

Yes, it's impossible to push data from server directly to your browser client.

But you can to check server for new messages every, let's say, 3 seconds and refresh your client interface.

Maybe you want to take a look on some Comet implementations

Receive binary data in web browser from HTTP server in a streaming style

Use XMLHttpRequest can't fulfill all my requests. But, with some trick I can read the chunk binary data once its arrived. Generally speaking, override the minetype to be 'text/plain; charset=x-user-defined' which will stream the binary data as text, and once one package is ready I can get it and convert it to arrayBuffer.

var xhr = new XMLHttpRequest();
var streamOffset = 0;

xhr.overrideMimeType('text/plain; charset=x-user-defined');
xhr.open("GET", url, true);
xhr.send();
xhr.onreadystatechange = function () {
var textBuffer = xhr.responseText;
var arrayBuffer = textToArrayBuffer(textBuffer, streamOffset);
}
function textToArrayBuffer(textBuffer, startOffset) {
var len = textBuffer.length - startOffset;
var arrayBuffer = new ArrayBuffer(len);
var ui8a = new Uint8Array(arrayBuffer, 0);
for (var i = 0, j = startOffset; i < len; i++, j++)
ui8a[i] = (textBuffer.charCodeAt(j) & 0xff);
return arrayBuffer;
}

Though, in this way I can get binary data in a streaming way, it can't be throw away after processing each chunk until the request is complete. Anyway, this give me the chance to handle the binary data as soon as it arrives.

How to transparently stream a file to the browser?

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.

Stream realtime data to web page

You are going to want to use a technique called "Comet."

From the front-end you will need to utilize HTML5 web sockets, long-polling, or the hidden iframe technique to provide a live-enabled interface (or a combination of all 3 depending on the browser engine detected)

From the server-side (if you are using lighttpd), I would suggest using a message queue system like beanstalkd. Combining beanstalkd, lighttpd, and php, you can create "infinitely loading" page that will sit and wait ("block") for new messages as they arrive in the beanstalkd queue (called a tube). Once a new message is received, it can be outputted to the browser front end

Fetch with ReadableStream as Request Body

We're working on making this work, see https://github.com/whatwg/fetch/pull/425 for the PR to the Fetch Standard. Once that is done you can expect this to make its way into browsers (slowly).



Related Topics



Leave a reply



Submit