我目前的代码:
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`]);
也没有任何成功。
有人能指出我正确的方向吗?
答案 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]);
};