UNPKG

videory

Version:

CLI for transcoding videos

169 lines (134 loc) 5.24 kB
import {deleteVideo, findNotTranscodedVideos} from "./db.js" import path from "path" import fs from "fs" import {checkFileExists} from "./helpers.js"; import {utimes} from "utimes"; import ffmpg from "dropconvert" import log from "log"; const debug = log.get("videory:transcode") /** * Transcode given videos to desired transcode directory * @param {[Object]} videos - array of video objects to transcode * @param {String} transcodeDir - directory to store the transcoded videos * @return {Promise<void>} */ async function transcodeAll(videos, transcodeDir) { for (let i = 0; i < videos.length; i++) { const movie = videos[i]; await transcode(movie, transcodeDir) .catch(debug.error); // fixme: decide on error handling } } /** * Transcode single video * @param {Object} v - video * @param {String} transcodeDir - directory to store the transcoded videos * @return {Promise<any>} */ async function transcode(v, transcodeDir) { return new Promise(async (resolve, reject) => { const inputPath = path.resolve(v.path); const codec = process.env.CODEC || "libx264"; const crf = process.env.CRF || 22; const preset = process.env.PRESET || "medium"; if (!fs.existsSync(inputPath)) { console.warn(`${inputPath} was about to be transcoded but cannot be located on filesystem. Video entry will be deleted from the DB.`); await deleteVideo(v); return resolve(); } async function defineOutPath(suffix = 0) { let outputPath = path.join(transcodeDir, "/", `${v.name}.${v.hash.substr(0, 5)}.${codec}.crf${crf}.${preset}${suffix ? "-" + suffix : ""}.mp4`); return outputPath; }; const onProgress = async p => { debug.notice("Processing ", { name: v.name, path: v.path, hash: v.hash, progress: `${Math.round(100 / v.frames * p.frames * 100) / 100} % `, ...p, }); }; const onEnd = async () => { // end transcoding state await Promise.all([ v.setIsTrancoding(false, codec, preset, crf, null), v.setTranscodePath(outputPath), ]); // update filesystem properties await new Promise((resolve, reject) => { fs.stat(inputPath, async (err, stats) => { if (err) { return reject(err); } try { const created = stats.ctime.getTime(); await utimes(outputPath, { atime: undefined, btime: created, mtime: created }); } catch (e) { return reject(e); } return resolve(); }); }); debug.notice("Video file " + inputPath + " was transcoded to " + outputPath); return resolve(v); }; const onError = async err => { debug.error(err); // fail transcoding state await Promise.all([ v.setIsTrancoding(false, null, null, null, null), v.setTranscodePath(null), v.setFailed(err), ]); return reject(err); }; // define output target path const outputPath = await defineOutPath(); // check if target exists on fs const exists = await checkFileExists(outputPath); if (exists) { // skip encoding existent file if (global.allowVersions) { // add suffix to out path return defineOutPath(++suffix); } // assume video was transcoded correctly await onEnd(); debug.notice(`Skip transcoding ${v.path} to ${outputPath} assuming file exists already`); return resolve; } // otherwise encode ffmpg(inputPath, outputPath, {onProgress, onEnd, onError}); // set transcoding state await v.setIsTrancoding(true, codec); debug.notice(`Start transcoding ${v.path} to ${outputPath} using`, {codec, preset, crf},); }); } /** * Fetch next batch of not transcoded videos and transcode them * @param {String} transcodeDir - output directory to save the transcoded videos */ async function transcodeNext(transcodeDir) { // find batch of not transcoded videos to process next const videos = await findNotTranscodedVideos(); if (videos) { debug.notice(`Start transcoding batch of ${videos.length} videos`); // transcode the next batch of videos await transcodeAll(videos, transcodeDir); debug.notice(`Finished transcoding batch of ${videos.length} videos`); } } /** * Schedule next transcoder iteration * @param {String} transcodeDir - directory to save transcoded videos */ export async function* schedule(transcodeDir) { while (true) { yield await transcodeNext(transcodeDir); } }