diff --git a/examples/VideoStreamer.js b/examples/VideoStreamer.js new file mode 100644 index 0000000..e249609 --- /dev/null +++ b/examples/VideoStreamer.js @@ -0,0 +1,79 @@ +/* This is an example of async streaming of large files. + * Try navigating to the adderss with Chrome and see the video + * in real time. */ + +const uWS = require('../dist/uws.js'); +const fs = require('fs'); +const crypto = require('crypto'); + +const port = 9001; +const fileName = '/home/alexhultman/Downloads/Sintel.2010.720p.mkv'; +const totalSize = fs.statSync(fileName).size; + +console.log('Video size is: ' + totalSize + ' bytes'); + +/* Helper function converting Node.js buffer to ArrayBuffer */ +function toArrayBuffer(buffer) { + return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); +} + +/* Returns true on success, false if it's having backpressure */ +function tryStream(res, chunk, requestDataCb) { + /* Stream as far as we can */ + let lastOffset = res.getWriteOffset(); + if (!res.tryEnd(chunk, totalSize)) { + /* Save unsent chunk for when we can send it */ + res.chunk = chunk; + res.chunkOffset = lastOffset; + + res.onWritable((offset) => { + if (res.tryEnd(res.chunk.slice(offset - res.chunkOffset), totalSize)) { + requestDataCb(); + return true; + } + return false; + }); + /* Return failure */ + return false; + } + /* Return success */ + return true; +} + +/* Yes, you can easily swap to SSL streaming by uncommenting here */ +const app = uWS./*SSL*/App({ + key_file_name: '/home/alexhultman/key.pem', + cert_file_name: '/home/alexhultman/cert.pem', + passphrase: '1234' +}).get('/sintel.mkv', (res, req) => { + /* Log */ + console.log("Streaming Sintel video..."); + + /* Create read stream with Node.js and start streaming over Http */ + const readStream = fs.createReadStream(fileName); + const hash = crypto.createHash('md5'); + readStream.on('data', (chunk) => { + + const ab = toArrayBuffer(chunk); + hash.update(chunk); + + if (!tryStream(res, ab, () => { + /* Called once we want more data */ + readStream.resume(); + })) { + /* If we could not send this chunk, pause */ + readStream.pause(); + } + }).on('end', () => { + console.log("md5: " + hash.digest('hex')); + }); +}).get('/*', (res, req) => { + /* Make sure to always handle every route */ + res.end('Nothing to see here!'); +}).listen(port, (token) => { + if (token) { + console.log('Listening to port ' + port); + } else { + console.log('Failed to listen to port ' + port); + } +}); diff --git a/src/HttpResponseWrapper.h b/src/HttpResponseWrapper.h index 431a318..8ddcc2d 100644 --- a/src/HttpResponseWrapper.h +++ b/src/HttpResponseWrapper.h @@ -14,6 +14,12 @@ struct HttpResponseWrapper { // res.onData(JS function) // res.onAborted + /* Returns the current write offset */ + template + static void res_getWriteOffset(const FunctionCallbackInfo &args) { + args.GetReturnValue().Set(Integer::New(isolate, getHttpResponse(args)->getWriteOffset())); + } + /* Takes function of bool(int), returns this */ template static void res_onWritable(const FunctionCallbackInfo &args) { @@ -100,6 +106,7 @@ struct HttpResponseWrapper { resTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "write"), FunctionTemplate::New(isolate, res_write)); resTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "writeHeader"), FunctionTemplate::New(isolate, res_writeHeader)); + resTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "getWriteOffset"), FunctionTemplate::New(isolate, res_getWriteOffset)); resTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "onWritable"), FunctionTemplate::New(isolate, res_onWritable)); /* Create our template */ diff --git a/uWebSockets b/uWebSockets index 8efea34..0ca061e 160000 --- a/uWebSockets +++ b/uWebSockets @@ -1 +1 @@ -Subproject commit 8efea340e66bc91efbe31b618a446f821c214bbf +Subproject commit 0ca061e7727df4fcc0c840e67188e093f966e7d3