Получение миниатюры из видео с использованием функций Cloud для Firebase

Код, который у меня есть в настоящее время:

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

Также без каких-либо успехов.

Может ли кто-нибудь указать мне правильное направление здесь?

Ответ 1

Чтобы использовать ffmpeg или любой другой инструмент командной строки системы, который не предварительно установлен в контейнере облачных объектов firebase, вы можете добавить предварительно скомпилированный двоичный файл в папку с функциями (рядом с index.js), и он будет загружать его вместе с кодом облачной функции на этапе развертывания. Затем вы можете выполнить двоичный файл, используя нерегулярный вызов child-process, как вы делали с ImageMagick (который уже установлен).

Здесь вы можете получить двоичный файл ffmpeg https://johnvansickle.com/ffmpeg/

Я использовал конструкцию x86_64 https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-64bit-static.tar.xz

Untar с

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

и просто добавьте один файл ffmpeg в папку функций.

В этой ссылке объясняется, как вы можете извлечь миниатюру из видео только с URL-адресом, поэтому нет необходимости полностью загружать файл. https://wistia.com/blog/faster-thumbnail-extraction-ffmpeg

Команда для извлечения миниатюры с шириной 512 пикселей и сохранения пропорций

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

Обратите внимание на./в./ffmpeg

Подробнее о аргументах шкалы вы можете узнать здесь https://trac.ffmpeg.org/wiki/Scaling%20(resizing)%20with%20ffmpeg

Если команда spawn не работает, то, как вы видели, вы не получите очень полезный вывод ошибки. Чтобы получить лучший результат, вы можете прослушивать потоки событий stdout и stderr в ChildProcess

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