Skip to content

Commit 63cabb8

Browse files
authored
Stream video with GridFSBucketAdapter (implements byte-range requests) (#6028)
* Stream video with GridFSBucketAdapter (implements byte-range requests) Closes: #5834 Similar to #2437 I ran into this issue while trying to view a mov file in safari from the dashboard. * Rename getFileStream to handleFileStream
1 parent 8477681 commit 63cabb8

File tree

4 files changed

+111
-87
lines changed

4 files changed

+111
-87
lines changed

src/Adapters/Files/GridFSBucketAdapter.js

+35-4
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class GridFSBucketAdapter extends FilesAdapter {
5959

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

7373
async getFileData(filename: string) {
74-
const stream = await this.getDownloadStream(filename);
74+
const bucket = await this._getBucket();
75+
const stream = bucket.openDownloadStreamByName(filename);
7576
stream.read();
7677
return new Promise((resolve, reject) => {
7778
const chunks = [];
@@ -97,9 +98,39 @@ export class GridFSBucketAdapter extends FilesAdapter {
9798
);
9899
}
99100

100-
async getDownloadStream(filename: string) {
101+
async handleFileStream(filename: string, req, res, contentType) {
101102
const bucket = await this._getBucket();
102-
return bucket.openDownloadStreamByName(filename);
103+
const files = await bucket.find({ filename }).toArray();
104+
if (files.length === 0) {
105+
throw new Error('FileNotFound');
106+
}
107+
const parts = req
108+
.get('Range')
109+
.replace(/bytes=/, '')
110+
.split('-');
111+
const partialstart = parts[0];
112+
const partialend = parts[1];
113+
114+
const start = parseInt(partialstart, 10);
115+
const end = partialend ? parseInt(partialend, 10) : files[0].length - 1;
116+
117+
res.writeHead(206, {
118+
'Accept-Ranges': 'bytes',
119+
'Content-Length': end - start + 1,
120+
'Content-Range': 'bytes ' + start + '-' + end + '/' + files[0].length,
121+
'Content-Type': contentType,
122+
});
123+
const stream = bucket.openDownloadStreamByName(filename);
124+
stream.start(start);
125+
stream.on('data', chunk => {
126+
res.write(chunk);
127+
});
128+
stream.on('error', () => {
129+
res.sendStatus(404);
130+
});
131+
stream.on('end', () => {
132+
res.end();
133+
});
103134
}
104135

105136
handleShutdown() {

src/Adapters/Files/GridStoreAdapter.js

+72-2
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,14 @@ export class GridStoreAdapter extends FilesAdapter {
9494
);
9595
}
9696

97-
getFileStream(filename: string) {
98-
return this._connect().then(database => {
97+
async handleFileStream(filename: string, req, res, contentType) {
98+
const stream = await this._connect().then(database => {
9999
return GridStore.exist(database, filename).then(() => {
100100
const gridStore = new GridStore(database, filename, 'r');
101101
return gridStore.open();
102102
});
103103
});
104+
handleRangeRequest(stream, req, res, contentType);
104105
}
105106

106107
handleShutdown() {
@@ -111,4 +112,73 @@ export class GridStoreAdapter extends FilesAdapter {
111112
}
112113
}
113114

115+
// handleRangeRequest is licensed under Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/).
116+
// Author: LEROIB at weightingformypizza (https://weightingformypizza.wordpress.com/2015/06/24/stream-html5-media-content-like-video-audio-from-mongodb-using-express-and-gridstore/).
117+
function handleRangeRequest(stream, req, res, contentType) {
118+
const buffer_size = 1024 * 1024; //1024Kb
119+
// Range request, partial stream the file
120+
const parts = req
121+
.get('Range')
122+
.replace(/bytes=/, '')
123+
.split('-');
124+
let [start, end] = parts;
125+
const notEnded = !end && end !== 0;
126+
const notStarted = !start && start !== 0;
127+
// No end provided, we want all bytes
128+
if (notEnded) {
129+
end = stream.length - 1;
130+
}
131+
// No start provided, we're reading backwards
132+
if (notStarted) {
133+
start = stream.length - end;
134+
end = start + end - 1;
135+
}
136+
137+
// Data exceeds the buffer_size, cap
138+
if (end - start >= buffer_size) {
139+
end = start + buffer_size - 1;
140+
}
141+
142+
const contentLength = end - start + 1;
143+
144+
res.writeHead(206, {
145+
'Content-Range': 'bytes ' + start + '-' + end + '/' + stream.length,
146+
'Accept-Ranges': 'bytes',
147+
'Content-Length': contentLength,
148+
'Content-Type': contentType,
149+
});
150+
151+
stream.seek(start, function() {
152+
// Get gridFile stream
153+
const gridFileStream = stream.stream(true);
154+
let bufferAvail = 0;
155+
let remainingBytesToWrite = contentLength;
156+
let totalBytesWritten = 0;
157+
// Write to response
158+
gridFileStream.on('data', function(data) {
159+
bufferAvail += data.length;
160+
if (bufferAvail > 0) {
161+
// slice returns the same buffer if overflowing
162+
// safe to call in any case
163+
const buffer = data.slice(0, remainingBytesToWrite);
164+
// Write the buffer
165+
res.write(buffer);
166+
// Increment total
167+
totalBytesWritten += buffer.length;
168+
// Decrement remaining
169+
remainingBytesToWrite -= data.length;
170+
// Decrement the available buffer
171+
bufferAvail -= buffer.length;
172+
}
173+
// In case of small slices, all values will be good at that point
174+
// we've written enough, end...
175+
if (totalBytesWritten >= contentLength) {
176+
stream.close();
177+
res.end();
178+
this.destroy();
179+
}
180+
});
181+
});
182+
}
183+
114184
export default GridStoreAdapter;

src/Controllers/FilesController.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ export class FilesController extends AdaptableController {
9292
return FilesAdapter;
9393
}
9494

95-
getFileStream(config, filename) {
96-
return this.adapter.getFileStream(filename);
95+
handleFileStream(config, filename, req, res, contentType) {
96+
return this.adapter.handleFileStream(filename, req, res, contentType);
9797
}
9898
}
9999

src/Routers/FilesRouter.js

+2-79
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,7 @@ export class FilesRouter {
4545
const contentType = mime.getType(filename);
4646
if (isFileStreamable(req, filesController)) {
4747
filesController
48-
.getFileStream(config, filename)
49-
.then(stream => {
50-
handleFileStream(stream, req, res, contentType);
51-
})
48+
.handleFileStream(config, filename, req, res, contentType)
5249
.catch(() => {
5350
res.status(404);
5451
res.set('Content-Type', 'text/plain');
@@ -142,80 +139,6 @@ export class FilesRouter {
142139
function isFileStreamable(req, filesController) {
143140
return (
144141
req.get('Range') &&
145-
typeof filesController.adapter.getFileStream === 'function'
142+
typeof filesController.adapter.handleFileStream === 'function'
146143
);
147144
}
148-
149-
function getRange(req) {
150-
const parts = req
151-
.get('Range')
152-
.replace(/bytes=/, '')
153-
.split('-');
154-
return { start: parseInt(parts[0], 10), end: parseInt(parts[1], 10) };
155-
}
156-
157-
// handleFileStream is licenced under Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/).
158-
// Author: LEROIB at weightingformypizza (https://weightingformypizza.wordpress.com/2015/06/24/stream-html5-media-content-like-video-audio-from-mongodb-using-express-and-gridstore/).
159-
function handleFileStream(stream, req, res, contentType) {
160-
const buffer_size = 1024 * 1024; //1024Kb
161-
// Range request, partiall stream the file
162-
let { start, end } = getRange(req);
163-
164-
const notEnded = !end && end !== 0;
165-
const notStarted = !start && start !== 0;
166-
// No end provided, we want all bytes
167-
if (notEnded) {
168-
end = stream.length - 1;
169-
}
170-
// No start provided, we're reading backwards
171-
if (notStarted) {
172-
start = stream.length - end;
173-
end = start + end - 1;
174-
}
175-
176-
// Data exceeds the buffer_size, cap
177-
if (end - start >= buffer_size) {
178-
end = start + buffer_size - 1;
179-
}
180-
181-
const contentLength = end - start + 1;
182-
183-
res.writeHead(206, {
184-
'Content-Range': 'bytes ' + start + '-' + end + '/' + stream.length,
185-
'Accept-Ranges': 'bytes',
186-
'Content-Length': contentLength,
187-
'Content-Type': contentType,
188-
});
189-
190-
stream.seek(start, function() {
191-
// get gridFile stream
192-
const gridFileStream = stream.stream(true);
193-
let bufferAvail = 0;
194-
let remainingBytesToWrite = contentLength;
195-
let totalBytesWritten = 0;
196-
// write to response
197-
gridFileStream.on('data', function(data) {
198-
bufferAvail += data.length;
199-
if (bufferAvail > 0) {
200-
// slice returns the same buffer if overflowing
201-
// safe to call in any case
202-
const buffer = data.slice(0, remainingBytesToWrite);
203-
// write the buffer
204-
res.write(buffer);
205-
// increment total
206-
totalBytesWritten += buffer.length;
207-
// decrement remaining
208-
remainingBytesToWrite -= data.length;
209-
// decrement the avaialbe buffer
210-
bufferAvail -= buffer.length;
211-
}
212-
// in case of small slices, all values will be good at that point
213-
// we've written enough, end...
214-
if (totalBytesWritten >= contentLength) {
215-
stream.close();
216-
res.end();
217-
this.destroy();
218-
}
219-
});
220-
});
221-
}

0 commit comments

Comments
 (0)