Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting a thumbnail from a video using Cloud Functions for Firebase

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?

like image 307
V Cezar Avatar asked May 03 '17 02:05

V Cezar


People also ask

Can firebase storage store videos?

Google security, when uploading or downloading files from our firebase apps. We can store images, audio, video, or other user-generated content.

Is cloud function free in firebase?

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.


1 Answers

@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.

like image 60
driedler Avatar answered Sep 28 '22 14:09

driedler