Skip to content

Commit 878718d

Browse files
committed
Stream video with GridStoreAdapter
1 parent 3dbf0b2 commit 878718d

File tree

4 files changed

+162
-10
lines changed

4 files changed

+162
-10
lines changed

spec/ParseFile.spec.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,27 @@ describe('Parse.File testing', () => {
491491
});
492492
});
493493

494+
it('supports byte-range requests when requesting a video', done => {
495+
var headers = {
496+
'Content-Type': 'video/mp4',
497+
'X-Parse-Application-Id': 'test',
498+
'X-Parse-REST-API-Key': 'rest'
499+
};
500+
request.post({
501+
headers: headers,
502+
url: 'http://localhost:8378/1/files/file',
503+
body: '101010101001010101010101010101010010110101010101010101010'
504+
}, (error, response, body) => {
505+
expect(error).toBe(null);
506+
var b = JSON.parse(body);
507+
request.get(b.url, (error, response, body) =>{
508+
expect(response.headers['content-type']).toMatch(/^video\/mp4/);
509+
expect(response.headers['accept-ranges']).toMatch(/^bytes/);
510+
done();
511+
});
512+
});
513+
});
514+
494515
it_exclude_dbs(['postgres'])('creates correct url for old files hosted on files.parsetfss.com', done => {
495516
var file = {
496517
__type: 'File',

src/Adapters/Files/GridStoreAdapter.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,116 @@ export class GridStoreAdapter extends FilesAdapter {
6767
getFileLocation(config, filename) {
6868
return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
6969
}
70+
71+
handleVideoStream(filename, range, res, contentType) {
72+
return this._connect().then(database => {
73+
return GridStore.exist(database, filename)
74+
.then(() => {
75+
let gridStore = new GridStore(database, filename, 'r');
76+
gridStore.open((err, gridFile) => {
77+
if(!gridFile) {
78+
res.status(404);
79+
res.set('Content-Type', 'text/plain');
80+
res.end('File not found.');
81+
return;
82+
}
83+
streamVideo(gridFile,range, res, contentType);
84+
});
85+
});
86+
});
87+
}
88+
}
89+
90+
/**
91+
* streamVideo is licensed under Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/).
92+
* Author: LEROIB at weightingformypizza.(https://weightingformypizza.wordpress.com/2015/06/24/stream-html5-media-content-like-video-audio-from-mongodb-using-express-and-gridstore/)
93+
*/
94+
function streamVideo(gridFile, range, res, contentType) {
95+
var buffer_size = 1024 * 1024;//1024Kb
96+
if (range != null) {
97+
// Range request, partiall stream the file
98+
var parts = range.replace(/bytes=/, "").split("-");
99+
var partialstart = parts[0];
100+
var partialend = parts[1];
101+
var start = partialstart ? parseInt(partialstart, 10) : 0;
102+
var end = partialend ? parseInt(partialend, 10) : gridFile.length - 1;
103+
var chunksize = (end - start) + 1;
104+
105+
if(chunksize == 1){
106+
start = 0;
107+
partialend = false;
108+
}
109+
110+
if(!partialend){
111+
if(((gridFile.length-1) - start) < (buffer_size)){
112+
end = gridFile.length - 1;
113+
}else{
114+
end = start + (buffer_size);
115+
}
116+
chunksize = (end - start) + 1;
117+
}
118+
119+
if(start == 0 && end == 2){
120+
chunksize = 1;
121+
}
122+
123+
res.writeHead(206, {
124+
'Content-Range': 'bytes ' + start + '-' + end + '/' + gridFile.length,
125+
'Accept-Ranges': 'bytes',
126+
'Content-Length': chunksize,
127+
'Content-Type': contentType,
128+
});
129+
130+
gridFile.seek(start, function () {
131+
// get gridFile stream
132+
var stream = gridFile.stream(true);
133+
var ended = false;
134+
var bufferIdx = 0;
135+
var bufferAvail = 0;
136+
var range = (end - start) + 1;
137+
var totalbyteswanted = (end - start) + 1;
138+
var totalbyteswritten = 0;
139+
// write to response
140+
stream.on('data', function (buff) {
141+
bufferAvail += buff.length;
142+
//Ok check if we have enough to cover our range
143+
if(bufferAvail < range) {
144+
//Not enough bytes to satisfy our full range
145+
if(bufferAvail > 0)
146+
{
147+
//Write full buffer
148+
res.write(buff);
149+
totalbyteswritten += buff.length;
150+
range -= buff.length;
151+
bufferIdx += buff.length;
152+
bufferAvail -= buff.length;
153+
}
154+
}
155+
else{
156+
//Enough bytes to satisfy our full range!
157+
if(bufferAvail > 0) {
158+
var buffer = buff.slice(0,range);
159+
res.write(buffer);
160+
totalbyteswritten += buffer.length;
161+
bufferIdx += range;
162+
bufferAvail -= range;
163+
}
164+
}
165+
if(totalbyteswritten >= totalbyteswanted) {
166+
// totalbytes = 0;
167+
gridFile.close();
168+
res.end();
169+
this.destroy();
170+
}
171+
});
172+
});
173+
}else{
174+
// stream back whole file
175+
res.header("Accept-Ranges", "bytes");
176+
res.header('Content-Type', contentType);
177+
res.header('Content-Length', gridFile.length);
178+
var stream = gridFile.stream(true).pipe(res);
179+
}
70180
}
71181

72182
export default GridStoreAdapter;

src/Controllers/FilesController.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ export class FilesController extends AdaptableController {
8282
expectedAdapterType() {
8383
return FilesAdapter;
8484
}
85+
86+
87+
/**
88+
* Stream video file by serving data in chunks if FilesAdapter is GridStoreAdapter.
89+
* If not; handle the request as usual with "getFileData".
90+
*/
91+
handleVideoStream(filename, range, res, contentType) {
92+
if (this.adapter.constructor.name == 'GridStoreAdapter') {
93+
return this.adapter.handleVideoStream(filename,range,res,contentType);
94+
}else{
95+
return this.adapter.getFileData(filename);
96+
}
97+
}
8598
}
8699

87100
export default FilesController;

src/Routers/FilesRouter.js

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,24 @@ export class FilesRouter {
3636
const config = new Config(req.params.appId);
3737
const filesController = config.filesController;
3838
const filename = req.params.filename;
39-
filesController.getFileData(config, filename).then((data) => {
40-
res.status(200);
41-
var contentType = mime.lookup(filename);
42-
res.set('Content-Type', contentType);
43-
res.end(data);
44-
}).catch((err) => {
45-
res.status(404);
46-
res.set('Content-Type', 'text/plain');
47-
res.end('File not found.');
48-
});
39+
const contentType = mime.lookup(filename);
40+
if (contentType == 'video/mp4' || contentType == 'video/quicktime') {
41+
filesController.handleVideoStream(filename, req.get("Range"), res, contentType).catch((err) => {
42+
res.status(404);
43+
res.set('Content-Type', 'text/plain');
44+
res.end('File not found.');
45+
});
46+
}else{
47+
filesController.getFileData(config, filename).then((data) => {
48+
res.status(200);
49+
res.set('Content-Type', contentType);
50+
res.end(data);
51+
}).catch((err) => {
52+
res.status(404);
53+
res.set('Content-Type', 'text/plain');
54+
res.end('File not found.');
55+
});
56+
}
4957
}
5058

5159
createHandler(req, res, next) {

0 commit comments

Comments
 (0)