Hello,
Im using Meteor-files + AWS adaptor in my website for video playing(206 status). I came up couple questions:
Hello @haojia321 ,
Haven't heard from you for a while :)
Range header to control buffer, and chunk size. Using interceptDownload hook you may alter the size of the chunk, by changing "end position". For more see AWS:S3 integration docs, you're looking for if (http.request.headers.range) { /*...*/ } block. __Note:__ not all browsers sends this header, and most of the browsers are willing to not set "end position", I recommend to start with logging Range header, to check how player (default or custom) manages this feature.2 and 3 is made this way for security reasons, upload/download flow control and users permissions control.
Hello @dr-dimitru ,
Take play video from AWS for example. I know the file from meteor server to browser will be send as multiple chunks instead of one file.
My question is will the file send from AWS to meteor server in one file or chunks? Will this file be deleted automatically from meteor server(point to the code)? Where exactly is this file stored in meteor?
Our issue now is galaxy will crash when we play video. There is only one user and not a heavy traffic on our server when we faced the issue. From galaxy it says out of memory.
s3.getObject in current AWS integration guideP.S. Galaxy isn't a silver bullet, simple dedicated server or AWS EC2 will serve your needs with ease. Of course, you need to know Linux well...
Galaxy only says "Application exited with signal: killed (out of memory)" from log. Any thoughts why this happens?
I have attached collection code here. Would you mind help me take a look if there is any potential code can cause this issue please?
import { Meteor } from 'meteor/meteor';
import { _ } from 'meteor/underscore';
import { Random } from 'meteor/random';
import { FilesCollection } from 'meteor/ostrio:files';
import stream from 'stream';
import S3 from 'aws-sdk/clients/s3'; // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html
// See fs-extra and graceful-fs NPM packages
// For better i/o performance
import fs from 'fs';
// Example: S3='{"s3":{"key": "xxx", "secret": "xxx", "bucket": "xxx", "region": "xxx""}}' meteor
if (process.env.S3) {
Meteor.settings.s3 = JSON.parse(process.env.S3).s3;
}
const s3Conf = Meteor.settings.s3 || {};
const bound = Meteor.bindEnvironment((callback) => {
return callback();
});
// Check settings existence in Meteor.settings
// This is the best practice for app security
if (Meteor.isServer && s3Conf && s3Conf.key && s3Conf.secret && s3Conf.bucket && s3Conf.region) {
// Create a new S3 object
const s3 = new S3({
secretAccessKey: s3Conf.secret,
accessKeyId: s3Conf.key,
region: s3Conf.region,
// sslEnabled: true, // optional
httpOptions: {
timeout: 6000,
agent: false
}
});
// Declare the Meteor file collection on the Server
OstrioVideo = new FilesCollection({
debug: false, // Change to `true` for debugging
storagePath: 'assets/app/uploads/uploadedFiles/videos',
collectionName: 'OstrioVideo',
// Disallow Client to execute remove, use the Meteor.method
allowClientCode: false,
chunkSize: 5 * 1024 * 1024,
onBeforeUpload(file) {
// Allow upload files under 10MB, and only in png/jpg/jpeg formats
// Note: You should never trust to extension and mime-type here
// as this data comes from client and can be easily substitute
// to check file's "magic-numbers" use `mmmagic` or `file-type` package
// real extension and mime-type can be checked on client (untrusted side)
// and on server at `onAfterUpload` hook (trusted side)
if (file.size <= 1024 * 1024 * 1024 && /mp4|mov/i.test(file.ext)) {
return true;
} else {
return 'Please upload video, with size equal or less than 1GB';
}
},
// Start moving files to AWS:S3
// after fully received by the Meteor server
onAfterUpload(fileRef) {
// Run through each of the uploaded file
_.each(fileRef.versions, (vRef, version) => {
const ffprobe = require('ffprobe');
const ffprobeStatic = require('ffprobe-static');
ffprobe(vRef.path, {
path: ffprobeStatic.path
}, (err, info) => {
if (err) {
console.error(err, info);
}
this.collection.update({
_id: fileRef._id
}, {
$set: { 'meta.ffprobe': info }
}, (updError) => {
if (updError) {
console.error(updError);
}
const filePath = 'videos/' + (Random.id()) + '-' + version + '.' + fileRef.extension;
s3.putObject({
// ServerSideEncryption: 'AES256', // Optional
StorageClass: 'STANDARD',
Bucket: s3Conf.bucket,
Key: filePath,
Body: fs.createReadStream(vRef.path),
ContentType: vRef.type,
}, (error, data) => {
bound(() => {
if (error) {
console.error(error);
} else {
const upd = { $set: {} };
upd['$set']['versions.' + version + '.meta.pipePath'] = filePath;
upd['$set']['meta.isUploaded'] = true;
// Update FilesCollection with link to the file at AWS
this.collection.update({
_id: fileRef._id
}, upd, (updError) => {
if (updError) {
console.error(updError);
} else {
// Unlink original files from FS after successful upload to AWS:S3
this.unlink(this.collection.findOne(fileRef._id), version);
}
});
}
});
});
});
});
});
},
// Intercept access to the file
// And redirect request to AWS:S3
interceptDownload(http, fileRef, version) {
let path;
if (fileRef && fileRef.versions && fileRef.versions[version] && fileRef.versions[version].meta && fileRef.versions[version].meta.pipePath) {
path = fileRef.versions[version].meta.pipePath;
}
if (path) {
// If file is successfully moved to AWS:S3
// We will pipe request to AWS:S3
// So, original link will stay always secure
// To force ?play and ?download parameters
// and to keep original file name, content-type,
// content-disposition, chunked "streaming" and cache-control
// we're using low-level .serve() method
const opts = {
Bucket: s3Conf.bucket,
Key: path
};
if (http.request.headers.range) {
const vRef = fileRef.versions[version];
let range = _.clone(http.request.headers.range);
const array = range.split(/bytes=([0-9]*)-([0-9]*)/);
const start = parseInt(array[1]);
let end = parseInt(array[2]);
if (isNaN(end)) {
// Request data from AWS:S3 by small chunks
end = (start + this.chunkSize) - 1;
if (end >= vRef.size) {
end = vRef.size - 1;
}
}
opts.Range = `bytes=${start}-${end}`;
http.request.headers.range = `bytes=${start}-${end}`;
}
const fileColl = this;
s3.getObject(opts, function(error) {
if (error) {
console.error(error);
if (!http.response.finished) {
http.response.end();
}
} else {
if (http.request.headers.range && this.httpResponse.headers['content-range']) {
// Set proper range header in according to what is returned from AWS:S3
http.request.headers.range = this.httpResponse.headers['content-range'].split('/')[0].replace('bytes ', 'bytes=');
}
const dataStream = new stream.PassThrough();
fileColl.serve(http, fileRef, fileRef.versions[version], version, dataStream);
dataStream.end(this.data.Body);
}
});
return true;
}
// While file is not yet uploaded to AWS:S3
// It will be served file from FS
return false;
}
});
// Intercept FilesCollection's remove method to remove file from AWS:S3
const _origRemove = OstrioVideo.remove;
OstrioVideo.remove = function(search) {
const cursor = this.collection.find(search);
cursor.forEach((fileRef) => {
_.each(fileRef.versions, (vRef) => {
if (vRef && vRef.meta && vRef.meta.pipePath) {
// Remove the object from AWS:S3 first, then we will call the original FilesCollection remove
s3.deleteObject({
Bucket: s3Conf.bucket,
Key: vRef.meta.pipePath,
}, (error) => {
bound(() => {
if (error) {
console.error(error);
}
});
});
}
});
});
//remove original file from database
_origRemove.call(this, search);
};
} else {
OstrioVideo = new FilesCollection({
collectionName: 'OstrioVideo',
allowClientCode: false,
chunkSize: 5 * 1024 * 1024,
onBeforeUpload(file) {
// Allow upload files under 10MB, and only in png/jpg/jpeg formats
// Note: You should never trust to extension and mime-type here
// as this data comes from client and can be easily substitute
// to check file's "magic-numbers" use mmmagic or file-type package
// real extension and mime-type can be checked on client (untrusted side)
// and on server at onAfterUpload hook (trusted side)
if (file.size <= 1024 * 1024 * 1024 && /mp4|mov/i.test(file.ext)) {
return true;
} else {
return 'Please upload video, with size equal or less than 1GB';
}
},
onAfterUpload(fileRef) {
_.each(fileRef.versions, (vRef, version) => {
const ffprobe = require('ffprobe');
const ffprobeStatic = require('ffprobe-static');
ffprobe(vRef.path, {
path: ffprobeStatic.path
}, (err, info) => {
if (err) {
console.error(err, info);
}
this.collection.update({
_id: fileRef._id
}, {
$set: {
'meta.ffprobe': info,
'meta.isUploaded': true
}
}, (updError) => {
if (updError) {
console.error(updError);
}
});
});
});
}
});
}
Any thoughts why this happens?
if (http.request.headers.range) { /*...*/ } block as at AWS:S3 integration docs.@haojia321 any news on this one?
Hi @dr-dimitru
Sorry for the delay. We are still stuck here.
We are working on deploy meteor app to a cloud server with a docker now.
We are concerned about if it will scale to deploy to a cloud server. We know it will always hit our meteor server when user play video. So even we host our video in aws s3 and supports multiple region, it will always come to the meteor server which might be a bottleneck for us.
Below is a chunk from the network tab. It is 5mb in chuck and total 27mb.
Accept-Ranges:bytes
Cache-Control:public, max-age=31536000, s-maxage=31536000
Content-Disposition:inline; filename="2015-08-18%20372.MOV"; filename*=UTF-8''2015-08-18%20372.MOV; charset=UTF-8
Content-Length:5242880
Content-Range:bytes 18317312-23560191/27207738
Content-Type:video/quicktime
Date:Sun, 24 Dec 2017 02:27:35 GMT
Pragma:private
Trailer:Expires
Transfer-Encoding:chunked
@dr-dimitru is below issue will happen if I use third party storage like AWS S3?https://github.com/VeliovGroup/Meteor-Files/wiki/MeteorUp-(MUP)-Usage
This question came when we were discussing to using MUP.
Yes, onAfterUpload hook is the right place for post-processing of uploaded files.
Hi @dr-dimitru ,
Now I can use ffmpeg to compress the video in onAfterUpload function and I can successfully upload the compressed video to aws.
But I cannot play that video after upload to aws. Below is my onAfterUpload function. Could you help me to see if anything is wrong here please?
onAfterUpload(fileRef) {
_.each(fileRef.versions, (vRef, version) => {
const filePath = 'files2/' + (Random.id()) + '-' + version + '.mp4';
var self = this;
var processedFilePath = fileRef._storagePath + '/processed-' + fileRef._id + '.mp4';
var ffmpeg = require("ffmpeg");
var ffmpegProcess = new ffmpeg(vRef.path);
ffmpegProcess.then(function(video) {
video.addCommand('-vf', '"scale=\'min(1280,iw)\':\'min(720,ih)\':force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2"');
video.addCommand('-c:v', 'libx264');
video.save(processedFilePath, function(error, file) {
s3.putObject({
// ServerSideEncryption: 'AES256', // Optional
StorageClass: 'STANDARD',
Bucket: s3Conf.bucket,
Key: filePath,
Body: fs.createReadStream(processedFilePath),
ContentType: vRef.type,
}, (error, data) => {
bound(() => {
if (error) {
console.error(error);
} else {
console.log(data);
// Update FilesCollection with link to the file at AWS
const upd = { $set: {} };
upd['$set']['versions.' + version + '.meta.pipePath'] = filePath;
self.collection.update({
_id: fileRef._id
}, upd, (updError) => {
if (updError) {
console.error(updError);
} else {
// Unlink original files from FS after successful upload to AWS:S3
var f = self.collection.findOne(fileRef._id);
//console.log(this.link(f));
self.unlink(self.collection.findOne(fileRef._id), version);
}
});
}
});
});
});
}, function(error) {
console.log(error);
});
});
}
Paste code in here looks ugly so I uploaded the file here so that you can view more clearly.
I've fixed it. Consider reading this docs.
ffmpegProcess.then callbacks should be wrapped into bound(() => { /*...*/ });video.save callback should be wrapped into bound(() => { /*...*/ });But I cannot play that video after upload to aws.
ffmpegProcess.then callbacks should be wrapped into bound(() => { /.../ });
video.save callback should be wrapped into bound(() => { /.../ });
bound(() => { /.../ }) is called when file is uploaded to AWS. Here we need to first process the video, then upload to aws, right? So I think bound(() => { /.../ }) should inside video.save callback?
Have you tried to play videos without using AWS:S3, does processed videos playing locally?
I think the issue here is collection can only knows the original uploaded video. It doesnt aware of the processed video.
Have you updated metadata?
I didn't. Is this the cause? File size definitely smaller after compression.
This is the source url from video player. If I paste it in a new tab. I see errors.
Request URL:http://localhost:3000/cdn/storage/AttachmentFile/e4kPM9tnCFfxdwGCA/original/e4kPM9tnCFfxdwGCA?play=true
Referrer Policy:no-referrer-when-downgrade
Request Headers
**Provisional headers are shown**
Accept-Encoding:identity;q=1, *;q=0
Range:bytes=458752-
Referer:http://localhost:3000/cdn/storage/AttachmentFile/e4kPM9tnCFfxdwGCA/original/e4kPM9tnCFfxdwGCA?play=true
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36
Query String Parameters
view source
view URL encoded
play:true
@haojia321
//cc @mksh-su need help to locate the issue, with reproducible steps.
Hi @dr-dimitru
[UPDATE] I see error in server now. Sorry for the wrong info before.
{ InvalidRange: The requested range is not satisfiable
W20180117-19:22:15.666(-5)? (STDERR) at Request.extractError (/Users/I839603/edueex/code/prototypes/videoaws/node_modules/aws-sdk/lib/services/s3.js:577:35)
W20180117-19:22:15.666(-5)? (STDERR) at Request.callListeners (/Users/I839603/edueex/code/prototypes/videoaws/node_modules/aws-sdk/lib/sequential_executor.js:105:20)
W20180117-19:22:15.666(-5)? (STDERR) at Request.emit (/Users/I839603/edueex/code/prototypes/videoaws/node_modules/aws-sdk/lib/sequential_executor.js:77:10)
W20180117-19:22:15.666(-5)? (STDERR) at Request.emit (/Users/I839603/edueex/code/prototypes/videoaws/node_modules/aws-sdk/lib/request.js:683:14)
W20180117-19:22:15.667(-5)? (STDERR) at Request.transition (/Users/I839603/edueex/code/prototypes/videoaws/node_modules/aws-sdk/lib/request.js:22:10)
W20180117-19:22:15.667(-5)? (STDERR) at AcceptorStateMachine.runTo (/Users/I839603/edueex/code/prototypes/videoaws/node_modules/aws-sdk/lib/state_machine.js:14:12)
W20180117-19:22:15.667(-5)? (STDERR) at /Users/I839603/edueex/code/prototypes/videoaws/node_modules/aws-sdk/lib/state_machine.js:26:10
W20180117-19:22:15.667(-5)? (STDERR) at Request.<anonymous> (/Users/I839603/edueex/code/prototypes/videoaws/node_modules/aws-sdk/lib/request.js:38:9)
W20180117-19:22:15.667(-5)? (STDERR) at Request.<anonymous> (/Users/I839603/edueex/code/prototypes/videoaws/node_modules/aws-sdk/lib/request.js:685:12)
W20180117-19:22:15.667(-5)? (STDERR) at Request.callListeners (/Users/I839603/edueex/code/prototypes/videoaws/node_modules/aws-sdk/lib/sequential_executor.js:115:18)
W20180117-19:22:15.668(-5)? (STDERR) message: 'The requested range is not satisfiable',
W20180117-19:22:15.668(-5)? (STDERR) code: 'InvalidRange',
W20180117-19:22:15.668(-5)? (STDERR) region: null,
W20180117-19:22:15.668(-5)? (STDERR) time: 2018-01-18T00:22:15.665Z,
W20180117-19:22:15.668(-5)? (STDERR) requestId: 'B438173E579484AB',
W20180117-19:22:15.669(-5)? (STDERR) extendedRequestId: 'O4/m4joo1c7lM8T7/3EwKuW345sXTEjcwFWlbP+6nakoCLjABzo2gKNprcpSeebnqEVEEXcoUAM=',
W20180117-19:22:15.669(-5)? (STDERR) cfId: undefined,
W20180117-19:22:15.669(-5)? (STDERR) statusCode: 416,
W20180117-19:22:15.669(-5)? (STDERR) retryable: false,
W20180117-19:22:15.669(-5)? (STDERR) retryDelay: 49.171270532532475 }
If you see onAfterUpload function with code Body: fs.createReadStream(processedFilePath), Im not uploading the original video to AWS. Im uploading the processed video to AWS. This actually brings inconsistency between the collection info in mongodb and the processed video file. For example I have converted the video from MOV to MP4. But in mongodb record, type is still video/quicktime. Size is also not consistent. Will this be the reason to cause the issue?
Any suggestions how to fix the inconsistency?
Will this be the reason to cause the issue?
As I've said before you should update file's meta-data.
Any suggestions how to fix the inconsistency?
Update file's meta-data or update MongoDB's record with new file's subversion. See this wiki doc for reference.
How does Meteor-Files getting a file's size under the hood?
Looks like ffmpeg cannot get a video files size.
Not funny, it's node.js, - use fs.stat.
This discussion is already far beyond ostrio:files package.
Thanks for your help. The issue is I need to update the size from mongodb. Video will play after update it.
Since this issue is not ostrio:files related, we can close this issue now.
@dr-dimitru I have one more question related to ostrio:files. Below is a copy of file record from mongodb. We already have file info inside the versions array. Why do we still need to put similar file info like size, extension in the root of this record?
{
"_id" : "d4AusDrgCmNWgNSNm",
"size" : 6613307,
"type" : "video/mp4",
"name" : "output_5173.mp4",
"meta" : {},
"ext" : "mp4",
"extension" : "mp4",
"extensionWithDot" : ".mp4",
"mime" : "video/mp4",
"mime-type" : "video/mp4",
"userId" : null,
"path" : "assets/app/uploads/uploadedFiles/d4AusDrgCmNWgNSNm.mp4",
"versions" : {
"original" : {
"path" : "assets/app/uploads/uploadedFiles/d4AusDrgCmNWgNSNm.mp4",
"size" : 6809260,
"type" : "video/mp4",
"extension" : "mp4",
"meta" : {
"pipePath" : "files2/cGequZeMknDrMAH9X-original.mp4"
}
}
},
"_downloadRoute" : "/cdn/storage",
"_collectionName" : "AttachmentFile",
"isVideo" : true,
"isAudio" : false,
"isImage" : false,
"isText" : false,
"isJSON" : false,
"isPDF" : false,
"_storagePath" : "assets/app/uploads/uploadedFiles",
"public" : false
}