diff --git a/package-lock.json b/package-lock.json index 5dd17e968..06555be01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -936,6 +936,12 @@ "@types/node": "*" } }, + "@types/tmp": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.0.tgz", + "integrity": "sha512-flgpHJjntpBAdJD43ShRosQvNC0ME97DCfGvZEDlAThQmnerRXrLbX6YgzRBQCZTthET9eAWFAMaYP0m0Y4HzQ==", + "dev": true + }, "@types/ws": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.2.tgz", @@ -4039,7 +4045,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4050,7 +4055,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "requires": { "isexe": "^2.0.0" } @@ -4883,30 +4887,25 @@ "dev": true }, "execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "dev": true, + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz", + "integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==", "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" }, "dependencies": { "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" } } }, @@ -5021,6 +5020,17 @@ "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + } } }, "extglob": { @@ -6708,10 +6718,9 @@ } }, "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" }, "husky": { "version": "6.0.0", @@ -7294,8 +7303,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isobject": { "version": "3.0.1", @@ -7744,6 +7752,11 @@ "integrity": "sha1-eB4YMpaqlPbU2RbcM10NF676I/g=", "dev": true }, + "lookpath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/lookpath/-/lookpath-1.2.0.tgz", + "integrity": "sha512-cUl+R2bGJcSJiHLVKzGHRTYTBhudbHIgd7s63gfGHteaz0BBKEEz2yw2rgbxZAFze92KlbkiWzL1ylYOmqIPVA==" + }, "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", @@ -8132,8 +8145,7 @@ "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, "merge2": { "version": "1.4.1", @@ -8167,8 +8179,7 @@ "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, "mimic-response": { "version": "1.0.1", @@ -8714,7 +8725,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, "requires": { "path-key": "^3.0.0" } @@ -8889,7 +8899,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, "requires": { "mimic-fn": "^2.1.0" } @@ -9207,8 +9216,7 @@ "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, "path-parse": { "version": "1.0.6", @@ -9392,6 +9400,23 @@ "supports-color": "^7.1.0" } }, + "execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -9402,6 +9427,21 @@ "path-exists": "^4.0.0" } }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -10694,7 +10734,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "requires": { "shebang-regex": "^3.0.0" } @@ -10702,8 +10741,7 @@ "shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, "shelljs": { "version": "0.8.4", @@ -11233,8 +11271,7 @@ "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" }, "strip-indent": { "version": "3.0.0", @@ -11496,12 +11533,11 @@ "dev": true }, "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "requires": { - "os-tmpdir": "~1.0.2" + "rimraf": "^3.0.0" } }, "to-absolute-glob": { @@ -12380,6 +12416,40 @@ "dev": true, "requires": { "execa": "^4.0.2" + }, + "dependencies": { + "execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + } } }, "winston": { diff --git a/package.json b/package.json index b13957d3a..71251ad06 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@types/rimraf": "^3.0.0", "@types/sharp": "^0.28.0", "@types/shelljs": "^0.8.8", + "@types/tmp": "^0.2.0", "@types/ws": "^7.4.0", "@typescript-eslint/eslint-plugin": "^4.16.1", "@typescript-eslint/parser": "^4.16.1", @@ -110,9 +111,11 @@ "catch-exit": "^1.1.0", "chalk": "^4.0.0", "chrome-launcher": "^0.13.1", + "execa": "^5.0.0", "fsevents": "^2.3.1", "futoin-hkdf": "^1.3.2", "latest-version": "^5.1.0", + "lookpath": "^1.2.0", "mime-types": "^2.1.28", "puppeteer": "^5.5.0", "puppeteer-extra": "^3.1.16", @@ -122,6 +125,7 @@ "rimraf": "^3.0.2", "sanitize-filename": "^1.6.3", "sharp": "^0.28.0", + "tmp": "^0.2.1", "tree-kill": "^1.2.2", "winston": "^3.3.3" }, diff --git a/src/api/layers/sender.layer.ts b/src/api/layers/sender.layer.ts index 36d8d0824..2965fabef 100644 --- a/src/api/layers/sender.layer.ts +++ b/src/api/layers/sender.layer.ts @@ -18,6 +18,7 @@ import * as path from 'path'; import { Page } from 'puppeteer'; import { CreateConfig } from '../../config/create-config'; +import { convertToMP4GIF } from '../../utils/ffmpeg'; import { base64MimeType, downloadFileToBase64, @@ -472,16 +473,74 @@ export class SenderLayer extends ListenerLayer { to: string, base64: string, filename: string, - caption: string + caption?: string ) { return await this.page.evaluate( - ({ to, base64, filename, caption }) => { - WAPI.sendVideoAsGif(base64, to, filename, caption); - }, + ({ to, base64, filename, caption }) => + WAPI.sendVideoAsGif(base64, to, filename, caption), { to, base64, filename, caption } ); } + /** + * Sends a video to given chat as a gif, with caption or not, using base64 + * @category Chat + * @param to Chat id + * @param filePath File path + * @param filename + * @param caption + */ + public async sendGif( + to: string, + filePath: string, + filename?: string, + caption?: string + ) { + return new Promise(async (resolve, reject) => { + let base64 = await downloadFileToBase64(filePath), + obj: { erro: boolean; to: string; text: string }; + + if (!base64) { + base64 = await fileToBase64(filePath); + } + + if (!base64) { + obj = { + erro: true, + to: to, + text: 'No such file or directory, open "' + filePath + '"', + }; + return reject(obj); + } + + if (!filename) { + filename = path.basename(filePath); + } + + this.sendGifFromBase64(to, base64, filename, caption) + .then(resolve) + .catch(reject); + }); + } + + /** + * Sends a video to given chat as a gif, with caption or not, using base64 + * @category Chat + * @param to chat id xxxxx@us.c + * @param base64 base64 data:video/xxx;base64,xxx + * @param filename string xxxxx + * @param caption string xxxxx + */ + public async sendGifFromBase64( + to: string, + base64: string, + filename: string, + caption?: string + ) { + base64 = await convertToMP4GIF(base64); + + return await this.sendVideoAsGifFromBase64(to, base64, filename, caption); + } /** * Sends contact card to iven chat id * @category Chat diff --git a/src/lib/wapi/functions/send-video-as-gif.js b/src/lib/wapi/functions/send-video-as-gif.js index f4b2e3a58..1cb58fbb2 100644 --- a/src/lib/wapi/functions/send-video-as-gif.js +++ b/src/lib/wapi/functions/send-video-as-gif.js @@ -26,17 +26,37 @@ import { base64ToFile } from '../helper'; * @param {string} caption * @param {Function} done Optional callback */ -export function sendVideoAsGif(dataBase64, chatid, filename, caption, done) { - // const idUser = new window.Store.UserConstructor(chatid); - const idUser = new Store.WidFactory.createWid(chatid); - return Store.Chat.find(idUser).then((chat) => { +export async function sendVideoAsGif( + dataBase64, + chatid, + filename, + caption, + done +) { + var chat = await WAPI.sendExist(chatid); + if (!chat.erro) { var mediaBlob = base64ToFile(dataBase64, filename); - processFiles(chat, mediaBlob).then((mc) => { - var media = mc.models[0]; - media.mediaPrep._mediaData.isGif = true; - media.mediaPrep._mediaData.gifAttribution = 1; - media.mediaPrep.sendToChat(chat, { caption: caption }); + var mediaCollection = await processFiles(chat, mediaBlob); + var media = mediaCollection.models[0]; + media.mediaPrep._mediaData.isGif = true; + media.mediaPrep._mediaData.gifAttribution = 1; + media.mediaPrep.sendToChat(chat, { caption: caption }); + + var result = (await media.sendToChat(chat, { caption: caption })) || ''; + var m = { filename: filename, text: caption }, + To = await WAPI.getchatId(chat.id); + if (result === 'success' || result === 'OK') { + if (done !== undefined) done(false); + var obj = WAPI.scope(To, false, result, null); + Object.assign(obj, m); + return obj; + } else { if (done !== undefined) done(true); - }); - }); + var obj = WAPI.scope(To, true, result, null); + Object.assign(obj, m); + return obj; + } + } else { + return chat; + } } diff --git a/src/utils/ffmpeg.ts b/src/utils/ffmpeg.ts new file mode 100644 index 000000000..9b1d0a02f --- /dev/null +++ b/src/utils/ffmpeg.ts @@ -0,0 +1,124 @@ +/* + * This file is part of WPPConnect. + * + * WPPConnect is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * WPPConnect is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with WPPConnect. If not, see . + */ +import { addExitCallback } from 'catch-exit'; +import execa from 'execa'; +import * as fs from 'fs'; +import * as path from 'path'; +import rimraf from 'rimraf'; +import * as tmp from 'tmp'; +import { lookpath } from 'lookpath'; + +const tmpDir = tmp.dirSync({}); + +let i = 0; + +addExitCallback((signal) => { + // Remove only on exit signal + try { + // Use rimraf because it is synchronous + rimraf.sync(tmpDir.name); + } catch (error) {} +}); + +export async function getFfmpegPath() { + let ffmpegPath = process.env['FFMPEG_PATH']; + + if (ffmpegPath) { + const isExecutable = await fs.promises + .access(ffmpegPath, fs.constants.X_OK) + .then(() => true) + .catch(() => false); + + if (isExecutable) { + return ffmpegPath; + } + } + + ffmpegPath = await lookpath('ffmpeg', { + include: [ + 'C:\\FFmpeg\\bin', + 'C:\\FFmpeg\\FFmpeg\\bin', + 'C:\\Program Files\\ffmpeg\\bin', + 'C:\\Program Files (x86)\\ffmpeg\\bin', + ], + }); + + if (!ffmpegPath) { + try { + ffmpegPath = require('ffmpeg-static'); + } catch (error) {} + } + + if (!ffmpegPath) { + throw new Error( + 'Error: FFMPEG not found. Please install ffmpeg or define the env FFMPEG_PATH or install ffmpeg-static' + ); + } + + return ffmpegPath; +} + +/** + * Convert a file to a compatible MP4-GIF for WhatsApp + * @param inputBase64 Gif in base64 format + * @returns base64 of a MP4-GIF for WhatsApp + */ +export async function convertToMP4GIF(inputBase64: string): Promise { + const inputPath = path.join(tmpDir.name, `${i++}`); + const outputPath = path.join(tmpDir.name, `${i++}.mp4`); + + if (inputBase64.includes(',')) { + inputBase64 = inputBase64.split(',')[1]; + } + + fs.writeFileSync(inputPath, Buffer.from(inputBase64, 'base64')); + + /** + * fluent-ffmpeg is a good alternative, + * but to work with MP4 you must use fisical file or ffmpeg will not work + * Because of that, I made the choice to use temporary file + */ + const ffmpegPath = await getFfmpegPath(); + + try { + const out = await execa(ffmpegPath, [ + '-i', + inputPath, + '-movflags', + 'faststart', + '-pix_fmt', + 'yuv420p', + '-vf', + 'scale=trunc(iw/2)*2:trunc(ih/2)*2', + '-f', + 'mp4', + outputPath, + ]); + + if (out.exitCode === 0) { + const outputBase64 = fs.readFileSync(outputPath); + return 'data:video/mp4;base64,' + outputBase64.toString('base64'); + } + + throw out.stderr; + } finally { + rimraf(inputPath, () => null); + rimraf(outputPath, () => null); + } + + return ''; +}