The code I currently have:
exports.generateThumbnail = functions.storage.object().onChange(event => {
...
.then(() => {
console.log('File downloaded locally to', tempFilePath);
// Generate a thumbnail using ImageMagick.
if (contentType.startsWith('video/')) {
return spawn('convert', [tempFilePath + '[0]', '-quiet', `${tempFilePath}.jpg`]);
} else if (contentType.startsWith('image/')){
return spawn('convert', [tempFilePath, '-thumbnail', '200x200', tempFilePath]);
The error I get in the console:
Failed AGAIN! { Error: spawn ffmpeg ENOENT
at exports._errnoException (util.js:1026:11)
at Process.ChildProcess._handle.onexit (internal/child_process.js:193:32)
at onErrorNT (internal/child_process.js:359:16)
at _combinedTickCallback (internal/process/next_tick.js:74:11)
at process._tickDomainCallback (internal/process/next_tick.js:122:9)
code: 'ENOENT',
errno: 'ENOENT',
syscall: 'spawn ffmpeg',
path: 'ffmpeg',
spawnargs: [ '-t', '1', '-i', '/tmp/myVideo.m4v', 'theThumbs.jpg' ] }
I also tried Imagemagick:
return spawn('convert', [tempFilePath + '[0]', '-quiet',`${tempFilePath}.jpg`]);
Also without any success.
Can anyone point me to the right direction here?
Google security, when uploading or downloading files from our firebase apps. We can store images, audio, video, or other user-generated content.
Cloud Functions includes a perpetual free tier for invocations to allow you to experiment with the platform at no charge. Note that even for free tier usage, we require a valid billing account.
@andrew-robinson post was a good start. The following will generate a thumbnail for both images and videos.
Add the following to your npm packages:
@ffmpeg-installer/ffmpeg
@google-cloud/storage
child-process-promise
mkdirp
mkdirp-promise
Use the following to generate a thumbnail from a larger image:
function generateFromImage(file, tempLocalThumbFile, fileName) {
const tempLocalFile = path.join(os.tmpdir(), fileName);
// Download file from bucket.
return file.download({destination: tempLocalFile}).then(() => {
console.info('The file has been downloaded to', tempLocalFile);
// Generate a thumbnail using ImageMagick with constant width and variable height (maintains ratio)
return spawn('convert', [tempLocalFile, '-thumbnail', THUMB_MAX_WIDTH, tempLocalThumbFile], {capture: ['stdout', 'stderr']});
}).then(() => {
fs.unlinkSync(tempLocalFile);
return Promise.resolve();
})
}
Use the following to generate a thumbnail from a video:
function generateFromVideo(file, tempLocalThumbFile) {
return file.getSignedUrl({action: 'read', expires: '05-24-2999'}).then((signedUrl) => {
const fileUrl = signedUrl[0];
const promise = spawn(ffmpegPath, ['-ss', '0', '-i', fileUrl, '-f', 'image2', '-vframes', '1', '-vf', `scale=${THUMB_MAX_WIDTH}:-1`, tempLocalThumbFile]);
// promise.childProcess.stdout.on('data', (data) => console.info('[spawn] stdout: ', data.toString()));
// promise.childProcess.stderr.on('data', (data) => console.info('[spawn] stderr: ', data.toString()));
return promise;
})
}
The following will execute when a video or image is uploaded to storage. It determines the file type, generates the thumbnail to a temp file, uploads the thumbnail to storage, then call 'updateDatabase()' which should be a promise that updates your database (if necessary):
const functions = require('firebase-functions');
const mkdirp = require('mkdirp-promise');
const gcs = require('@google-cloud/storage');
const admin = require('firebase-admin');
const spawn = require('child-process-promise').spawn;
const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path;
const path = require('path');
const os = require('os');
const fs = require('fs');
const db = admin.firestore();
// Max height and width of the thumbnail in pixels.
const THUMB_MAX_WIDTH = 384;
const SERVICE_ACCOUNT = '<your firebase credentials file>.json';
const adminConfig = JSON.parse(process.env.FIREBASE_CONFIG);
module.exports = functions.storage.bucket(adminConfig.storageBucket).object().onFinalize(object => {
const fileBucket = object.bucket; // The Storage bucket that contains the file.
const filePathInBucket = object.name;
const resourceState = object.resourceState; // The resourceState is 'exists' or 'not_exists' (for file/folder deletions).
const metageneration = object.metageneration; // Number of times metadata has been generated. New objects have a value of 1.
const contentType = object.contentType; // This is the image MIME type
const isImage = contentType.startsWith('image/');
const isVideo = contentType.startsWith('video/');
// Exit if this is a move or deletion event.
if (resourceState === 'not_exists') {
return Promise.resolve();
}
// Exit if file exists but is not new and is only being triggered
// because of a metadata change.
else if (resourceState === 'exists' && metageneration > 1) {
return Promise.resolve();
}
// Exit if the image is already a thumbnail.
else if (filePathInBucket.indexOf('.thumbnail.') !== -1) {
return Promise.resolve();
}
// Exit if this is triggered on a file that is not an image or video.
else if (!(isImage || isVideo)) {
return Promise.resolve();
}
const fileDir = path.dirname(filePathInBucket);
const fileName = path.basename(filePathInBucket);
const fileInfo = parseName(fileName);
const thumbFileExt = isVideo ? 'jpg' : fileInfo.ext;
let thumbFilePath = path.normalize(path.join(fileDir, `${fileInfo.name}_${fileInfo.timestamp}.thumbnail.${thumbFileExt}`));
const tempLocalThumbFile = path.join(os.tmpdir(), thumbFilePath);
const tempLocalDir = path.join(os.tmpdir(), fileDir);
const generateOperation = isVideo ? generateFromVideo : generateFromImage;
// Cloud Storage files.
const bucket = gcs({keyFilename: SERVICE_ACCOUNT}).bucket(fileBucket);
const file = bucket.file(filePathInBucket);
const metadata = {
contentType: isVideo ? 'image/jpeg' : contentType,
// To enable Client-side caching you can set the Cache-Control headers here. Uncomment below.
// 'Cache-Control': 'public,max-age=3600',
};
// Create the temp directory where the storage file will be downloaded.
return mkdirp(tempLocalDir).then(() => {
return generateOperation(file, tempLocalThumbFile, fileName);
}).then(() => {
console.info('Thumbnail created at', tempLocalThumbFile);
// Get the thumbnail dimensions
return spawn('identify', ['-ping', '-format', '%wx%h', tempLocalThumbFile], {capture: ['stdout', 'stderr']});
}).then((result) => {
const dim = result.stdout.toString();
const idx = thumbFilePath.indexOf('.');
thumbFilePath = `${thumbFilePath.substring(0,idx)}_${dim}${thumbFilePath.substring(idx)}`;
console.info('Thumbnail dimensions:', dim);
// Uploading the Thumbnail.
return bucket.upload(tempLocalThumbFile, {destination: thumbFilePath, metadata: metadata});
}).then(() => {
console.info('Thumbnail uploaded to Storage at', thumbFilePath);
const thumbFilename = path.basename(thumbFilePath);
return updateDatabase(fileDir, fileName, thumbFilename);
}).then(() => {
console.info('Thumbnail generated.');
fs.unlinkSync(tempLocalThumbFile);
return Promise.resolve();
})
});
parseName() should parse your filename format. At the very least it should return the file's basename and extension.
updateDatabase() should return a promise that updates your database with the newly generated thumbnail (if necessary).
Note that @ffmpeg-installer/ffmpeg removes the need of directly including a ffmpeg binary in your cloud function.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With