使用Cloud Functions for Firebase

时间:2017-05-03 02:57:33

标签: firebase google-cloud-functions

我目前的代码:

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]);

我在控制台中收到的错误:

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' ] }

我也试过Imagemagick:

return spawn('convert', [tempFilePath + '[0]', '-quiet',`${tempFilePath}.jpg`]);

也没有任何成功。

有人能指出我正确的方向吗?

2 个答案:

答案 0 :(得分:9)

@ andrew-robinson的帖子是一个好的开始。 以下将为图像和视频生成缩略图。

将以下内容添加到您的npm软件包中:

@ffmpeg-installer/ffmpeg
@google-cloud/storage
child-process-promise
mkdirp
mkdirp-promise

使用以下命令从较大的图像生成缩略图:

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();
    })
}

使用以下内容从视频生成缩略图:

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;
    })
}

将视频或图像上传到存储设备时,将执行以下操作。 它确定文件类型,将缩略图生成为临时文件,然后将缩略图上传到存储设备,然后调用“ updateDatabase()”,这应该是对数据库更新的承诺(如果需要):

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()应该解析您的文件名格式。至少它应该返回文件的基本名称和扩展名。

updateDatabase()应该返回一个诺言,该诺言将使用新生成的缩略图更新您的数据库(如有必要)。

请注意,@ ffmpeg-installer / ffmpeg消除了在您的云函数中直接包含ffmpeg二进制文件的需要。

答案 1 :(得分:5)

要使用未预先安装在firebase云功能容器上的ffmpeg或任何其他系统命令行工具,可以将预编译的二进制文件添加到functions文件夹(与index.js一起),然后将其上载以及部署步骤中的云功能代码。然后,您可以使用child-process-promise spawn执行二进制文件,就像使用ImageMagick(已安装)一样。

您可以在此处获取ffmpeg二进制文件https://johnvansickle.com/ffmpeg/

我使用了x86_64版本https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-64bit-static.tar.xz

解开

tar -xvzf ffmpeg-release-64bit-static.tar.xz

并将一个ffmpeg文件添加到functions文件夹中。

此链接说明了如何仅使用网址从视频中提取缩略图,因此无需完全下载文件。 https://wistia.com/blog/faster-thumbnail-extraction-ffmpeg

提取宽度为512px的缩略图并保持宽高比的命令为

const spawn = require('child-process-promise').spawn;

const extractThumbnailFromVideoUrl = (fileUrl, tempThumbnailFilePath) => {
  return spawn('./ffmpeg', ['-ss', '0', '-i', fileUrl, '-f', 'image2', '-vframes', '1', '-vf', 'scale=512:-1', tempThumbnailFilePath]);
};

注意./ in ./ffmpeg

有关比例参数的更多详细信息,请参阅此处https://trac.ffmpeg.org/wiki/Scaling%20(resizing)%20with%20ffmpeg

如果spawn命令失败,那么正如您所见,您将无法获得非常有用的错误输出。要获得更好的输出,您可以在ChildProcess上监听stdout和stderr事件流

const extractThumbnailFromVideoUrl = (fileUrl, tempThumbnailFilePath) => {
    const promise = spawn('./ffmpeg', ['-ss', '0', '-i', fileUrl, '-f', 'image2', '-vframes', '1', '-vf', 'scale=512:-1', tempThumbnailFilePath]);
    promise.childProcess.stdout.on('data', (data: any) => console.log('[spawn] stdout: ', data.toString()));
    promise.childProcess.stderr.on('data', (data: any) => console.log('[spawn] stderr: ', data.toString()));
    return promise;
};

ffmpeg调用的输出将显示在您的云功能日志中,就像您从终端本地运行命令一样。有关详细信息,请参阅https://www.npmjs.com/package/child-process-promise http://node.readthedocs.io/en/latest/api/child_process/

以下是云功能的完整版本,仅假设视频文件。如果您还想处理图像或其他文件,则可以添加代码以提前退出或调用不同的方法。这使得调用创建临时目录并在方法结束时清除这些目录,但是我省略了这些函数的细节。

import * as functions from 'firebase-functions';   
import * as gcs from '@google-cloud/storage';
import {cleanupFiles, makeTempDirectories} from '../services/system-utils';

const spawn = require('child-process-promise').spawn;
const storageProjectId = `${functions.config().project_id}.appspot.com`;

export const videoFileThumbnailGenerator = functions.storage.bucket(storageProjectId).object().onChange(event => {
  const object = event.data;

  const fileBucket = object.bucket; // The Storage bucket that contains the file.
  const filePathInBucket = object.name; // File path in the bucket.
  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.

  // Exit if this is a move or deletion event.
  if (resourceState === 'not_exists') {
    console.log('This is a deletion event.');
    return Promise.resolve();
  }

  // Exit if file exists but is not new and is only being triggered
  // because of a metadata change.
  if (resourceState === 'exists' && metageneration > 1) {
    console.log('This is a metadata change event.');
    return Promise.resolve();
  }

  const bucket = gcs({keyFilename: `${functions.config().firebase_admin_credentials}`}).bucket(fileBucket);

  const filePathSplit = filePathInBucket.split('/');
  const filename = filePathSplit.pop();
  const filenameSplit = filename.split('.');
  const fileExtension = filenameSplit.pop();
  const baseFilename = filenameSplit.join('.');

  const fileDir = filePathSplit.join('/') + (filePathSplit.length > 0 ? '/' : '');

  const file = bucket.file(filePathInBucket);

  const tempThumbnailDir = '/tmp/thumbnail/';
  const jpgFilename = `${baseFilename}.jpg`;
  const tempThumbnailFilePath = `${tempThumbnailDir}${jpgFilename}`;
  const thumbnailFilePath = `${fileDir}thumbnail/${jpgFilename}`;

  return makeTempDirectories([tempThumbnailDir])
    .then(() => file.getSignedUrl({action: 'read', expires: '05-24-2999'}))
    .then(signedUrl => signedUrl[0])
    .then(fileUrl => extractThumbnailFromVideoUrl(fileUrl, tempThumbnailFilePath))
    .then(() => bucket.upload(tempThumbnailFilePath, {destination: thumbnailFilePath}))
    .then(() => cleanupFiles([
      {directoryName: tempThumbnailFilePath},
    ]))
    .catch(err => console.error('Video upload error: ', err));
});

const extractThumbnailFromVideoUrl = (fileUrl, tempThumbnailFilePath) => {
  return spawn('./ffmpeg', ['-ss', '0', '-i', fileUrl, '-f', 'image2', '-vframes', '1', '-vf', 'scale=512:-1', tempThumbnailFilePath]);
};