Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stream video with GridFSBucketAdapter (implements byte-range requests) #6028

Merged
merged 2 commits into from
Sep 11, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions src/Adapters/Files/GridFSBucketAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class GridFSBucketAdapter extends FilesAdapter {

async deleteFile(filename: string) {
const bucket = await this._getBucket();
const documents = await bucket.find({ filename: filename }).toArray();
const documents = await bucket.find({ filename }).toArray();
if (documents.length === 0) {
throw new Error('FileNotFound');
}
Expand All @@ -71,7 +71,8 @@ export class GridFSBucketAdapter extends FilesAdapter {
}

async getFileData(filename: string) {
const stream = await this.getDownloadStream(filename);
const bucket = await this._getBucket();
const stream = bucket.openDownloadStreamByName(filename);
stream.read();
return new Promise((resolve, reject) => {
const chunks = [];
Expand All @@ -97,9 +98,39 @@ export class GridFSBucketAdapter extends FilesAdapter {
);
}

async getDownloadStream(filename: string) {
async getFileStream(filename: string, req, res, contentType) {
const bucket = await this._getBucket();
return bucket.openDownloadStreamByName(filename);
const files = await bucket.find({ filename }).toArray();
if (files.length === 0) {
throw new Error('FileNotFound');
}
const parts = req
.get('range')
.replace(/bytes=/, '')
.split('-');
const partialstart = parts[0];
const partialend = parts[1];

const start = parseInt(partialstart, 10);
const end = partialend ? parseInt(partialend, 10) : files[0].length - 1;

res.writeHead(206, {
'Accept-Ranges': 'bytes',
'Content-Length': end - start + 1,
'Content-Range': 'bytes ' + start + '-' + end + '/' + files[0].length,
'Content-Type': contentType,
});
const stream = bucket.openDownloadStreamByName(filename);
stream.start(start);
stream.on('data', chunk => {
res.write(chunk);
});
stream.on('error', () => {
res.sendStatus(404);
});
stream.on('end', () => {
res.end();
});
}

handleShutdown() {
Expand Down
74 changes: 72 additions & 2 deletions src/Adapters/Files/GridStoreAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,14 @@ export class GridStoreAdapter extends FilesAdapter {
);
}

getFileStream(filename: string) {
return this._connect().then(database => {
async getFileStream(filename: string, req, res, contentType) {
const stream = await this._connect().then(database => {
return GridStore.exist(database, filename).then(() => {
const gridStore = new GridStore(database, filename, 'r');
return gridStore.open();
});
});
handleFileStream(stream, req, res, contentType);
}

handleShutdown() {
Expand All @@ -111,4 +112,73 @@ export class GridStoreAdapter extends FilesAdapter {
}
}

// handleFileStream is licensed under Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/).
// Author: LEROIB at weightingformypizza (https://weightingformypizza.wordpress.com/2015/06/24/stream-html5-media-content-like-video-audio-from-mongodb-using-express-and-gridstore/).
function handleFileStream(stream, req, res, contentType) {
const buffer_size = 1024 * 1024; //1024Kb
// Range request, partial stream the file
const parts = req
.get('range')
.replace(/bytes=/, '')
.split('-');
let [start, end] = parts;
const notEnded = !end && end !== 0;
const notStarted = !start && start !== 0;
// No end provided, we want all bytes
if (notEnded) {
end = stream.length - 1;
}
// No start provided, we're reading backwards
if (notStarted) {
start = stream.length - end;
end = start + end - 1;
}

// Data exceeds the buffer_size, cap
if (end - start >= buffer_size) {
end = start + buffer_size - 1;
}

const contentLength = end - start + 1;

res.writeHead(206, {
'Content-Range': 'bytes ' + start + '-' + end + '/' + stream.length,
'Accept-Ranges': 'bytes',
'Content-Length': contentLength,
'Content-Type': contentType,
});

stream.seek(start, function() {
// Get gridFile stream
const gridFileStream = stream.stream(true);
let bufferAvail = 0;
let remainingBytesToWrite = contentLength;
let totalBytesWritten = 0;
// Write to response
gridFileStream.on('data', function(data) {
bufferAvail += data.length;
if (bufferAvail > 0) {
// slice returns the same buffer if overflowing
// safe to call in any case
const buffer = data.slice(0, remainingBytesToWrite);
// Write the buffer
res.write(buffer);
// Increment total
totalBytesWritten += buffer.length;
// Decrement remaining
remainingBytesToWrite -= data.length;
// Decrement the available buffer
bufferAvail -= buffer.length;
}
// In case of small slices, all values will be good at that point
// we've written enough, end...
if (totalBytesWritten >= contentLength) {
stream.close();
res.end();
this.destroy();
}
});
});
}

export default GridStoreAdapter;
4 changes: 2 additions & 2 deletions src/Controllers/FilesController.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ export class FilesController extends AdaptableController {
return FilesAdapter;
}

getFileStream(config, filename) {
return this.adapter.getFileStream(filename);
getFileStream(config, filename, req, res, contentType) {
return this.adapter.getFileStream(filename, req, res, contentType);
}
}

Expand Down
79 changes: 1 addition & 78 deletions src/Routers/FilesRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,7 @@ export class FilesRouter {
const contentType = mime.getType(filename);
if (isFileStreamable(req, filesController)) {
filesController
.getFileStream(config, filename)
.then(stream => {
handleFileStream(stream, req, res, contentType);
})
.getFileStream(config, filename, req, res, contentType)
.catch(() => {
res.status(404);
res.set('Content-Type', 'text/plain');
Expand Down Expand Up @@ -145,77 +142,3 @@ function isFileStreamable(req, filesController) {
typeof filesController.adapter.getFileStream === 'function'
);
}

function getRange(req) {
const parts = req
.get('Range')
.replace(/bytes=/, '')
.split('-');
return { start: parseInt(parts[0], 10), end: parseInt(parts[1], 10) };
}

// handleFileStream is licenced under Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/).
// Author: LEROIB at weightingformypizza (https://weightingformypizza.wordpress.com/2015/06/24/stream-html5-media-content-like-video-audio-from-mongodb-using-express-and-gridstore/).
function handleFileStream(stream, req, res, contentType) {
const buffer_size = 1024 * 1024; //1024Kb
// Range request, partiall stream the file
let { start, end } = getRange(req);

const notEnded = !end && end !== 0;
const notStarted = !start && start !== 0;
// No end provided, we want all bytes
if (notEnded) {
end = stream.length - 1;
}
// No start provided, we're reading backwards
if (notStarted) {
start = stream.length - end;
end = start + end - 1;
}

// Data exceeds the buffer_size, cap
if (end - start >= buffer_size) {
end = start + buffer_size - 1;
}

const contentLength = end - start + 1;

res.writeHead(206, {
'Content-Range': 'bytes ' + start + '-' + end + '/' + stream.length,
'Accept-Ranges': 'bytes',
'Content-Length': contentLength,
'Content-Type': contentType,
});

stream.seek(start, function() {
// get gridFile stream
const gridFileStream = stream.stream(true);
let bufferAvail = 0;
let remainingBytesToWrite = contentLength;
let totalBytesWritten = 0;
// write to response
gridFileStream.on('data', function(data) {
bufferAvail += data.length;
if (bufferAvail > 0) {
// slice returns the same buffer if overflowing
// safe to call in any case
const buffer = data.slice(0, remainingBytesToWrite);
// write the buffer
res.write(buffer);
// increment total
totalBytesWritten += buffer.length;
// decrement remaining
remainingBytesToWrite -= data.length;
// decrement the avaialbe buffer
bufferAvail -= buffer.length;
}
// in case of small slices, all values will be good at that point
// we've written enough, end...
if (totalBytesWritten >= contentLength) {
stream.close();
res.end();
this.destroy();
}
});
});
}