UNPKG

@bigfootds/nodejs-trickplay

Version:

Generate trickplay images for a given video file, for usage in NodeJS environments.

170 lines (169 loc) 9.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateTrickplay = generateTrickplay; const ffmpeg_1 = __importDefault(require("@ffmpeg-installer/ffmpeg")); const ffprobe_1 = __importDefault(require("@ffprobe-installer/ffprobe")); const fluent_ffmpeg_1 = __importDefault(require("fluent-ffmpeg")); const node_path_1 = __importDefault(require("node:path")); const node_fs_1 = __importDefault(require("node:fs")); const jimp_1 = require("jimp"); fluent_ffmpeg_1.default.setFfmpegPath(ffmpeg_1.default.path); fluent_ffmpeg_1.default.setFfprobePath(ffprobe_1.default.path); function generateTrickplay(targetVideoPath_1) { return __awaiter(this, arguments, void 0, function* (targetVideoPath, options = {}) { //#region Function prep console.log("Performing trickplay on: " + targetVideoPath); // Make sure target video exists if (!node_fs_1.default.existsSync(targetVideoPath)) { console.log("Path does not lead to a file: " + targetVideoPath); } // Analyse video for metadata such as its duration. // We need things like duration to figure out how many images to make. let videoMetadata = null; videoMetadata = yield new Promise((resolve, reject) => { (0, fluent_ffmpeg_1.default)(targetVideoPath).ffprobe((error, metadata) => { if (error) { reject(error); } videoMetadata = metadata; resolve(videoMetadata); }); }); // If the video was invalid or no important metadata could be found, cancel. if (videoMetadata == null || videoMetadata.format.duration == undefined) return; console.log(videoMetadata.format.duration); let targetVideoFilename = node_path_1.default.basename(targetVideoPath); console.log(targetVideoFilename); //#endregion //#region Prepare the generator options let localOptions = { trickplayOutputDir: options.trickplayOutputDir || targetVideoPath.slice(0, targetVideoPath.indexOf(targetVideoFilename)) + targetVideoFilename.slice(0, targetVideoFilename.lastIndexOf(".")) + ".trickplay", secondsBetweenFrames: options.secondsBetweenFrames || 10, numberOfFramesToGrab: Math.floor(videoMetadata.format.duration / (options.secondsBetweenFrames || 10)), frameTimestamps: options.frameTimestamps || [], trickplayImageWidth: options.trickplayImageWidth || 320, trickplaySheetRows: options.trickplaySheetRows || 10, trickplaySheetColumns: options.trickplaySheetColumns || 10, skipIndividualFrameGeneration: options.skipIndividualFrameGeneration || false, individualFrameFileFormat: options.individualFrameFileFormat || "jpg", tilesheetFileFormat: options.tilesheetFileFormat || "jpg" }; localOptions.frameTimestamps = new Array(Math.floor(localOptions.numberOfFramesToGrab)).fill(0).map((_, index) => { return (index * localOptions.secondsBetweenFrames).toString(); }); console.log(JSON.stringify(localOptions, null, 4)); //#endregion //#region Generate the trickplay images if (!localOptions.skipIndividualFrameGeneration) { // Make sure raw frames directory exists let rawFramesDirectory = node_path_1.default.resolve(localOptions.trickplayOutputDir, "frames"); if (!node_fs_1.default.existsSync(rawFramesDirectory)) { console.log("Making raw frames directory at:\n" + rawFramesDirectory); node_fs_1.default.mkdirSync(rawFramesDirectory, { recursive: true }); } let createdTrickplayImagePaths = []; let screenshotOptions = { count: localOptions.frameTimestamps.length, timemarks: localOptions.frameTimestamps, size: localOptions.trickplayImageWidth + "x?", filename: "%i." + localOptions.individualFrameFileFormat }; console.log("---"); console.log(JSON.stringify(screenshotOptions, null, 4)); console.log("---"); let resultViaMethods = yield new Promise((resolve, reject) => { (0, fluent_ffmpeg_1.default)(targetVideoPath).takeScreenshots(screenshotOptions, localOptions.trickplayOutputDir + "/frames").on("filenames", (filenames) => { createdTrickplayImagePaths = [...filenames]; }).on("end", () => { resolve(true); }).on("error", () => { reject(false); }); }); if (resultViaMethods) { console.log("Image generation successful: " + resultViaMethods); console.log(createdTrickplayImagePaths); } else { return; } } //#endregion //#region Composite (a.k.a stitch) the raw trickplay images together into a trickplay tilesheet. // Prepare a tilesheet output directory let tilesheetDirectory = node_path_1.default.resolve(localOptions.trickplayOutputDir, `${localOptions.trickplayImageWidth} - ${localOptions.trickplaySheetColumns}x${localOptions.trickplaySheetRows}`); console.log("Preparing to put the trickplay tilesheet into:\n" + tilesheetDirectory); if (!node_fs_1.default.existsSync(tilesheetDirectory)) { node_fs_1.default.mkdirSync(tilesheetDirectory); } let individualFramePaths = []; let rawFramesDirectory = node_path_1.default.resolve(localOptions.trickplayOutputDir, "frames"); console.log("Raw frames should be expected at: \n" + rawFramesDirectory); node_fs_1.default.readdirSync(rawFramesDirectory, { withFileTypes: true }).forEach((foundFile) => { if (foundFile.isFile()) { let foundFileExtension = foundFile.name.slice(foundFile.name.lastIndexOf(".") + 1); if (foundFileExtension.toLocaleLowerCase() == localOptions.individualFrameFileFormat.toLocaleLowerCase()) { individualFramePaths.push(foundFile.name); } } }); individualFramePaths.sort((a, b) => { let aNoExt = Number.parseInt(a.slice(0, a.lastIndexOf("."))); let bNoExt = Number.parseInt(b.slice(0, b.lastIndexOf("."))); return aNoExt - bNoExt; }); console.log(individualFramePaths); let framePaths2dGrid = []; while (individualFramePaths.length) { framePaths2dGrid.push(individualFramePaths.splice(0, 10)); } console.log(framePaths2dGrid.length); let tilesheetCount = Math.ceil(framePaths2dGrid.length / localOptions.trickplaySheetRows); console.log(tilesheetCount); let sampleFrame = yield jimp_1.Jimp.read(node_path_1.default.resolve(rawFramesDirectory, framePaths2dGrid[0][0])); let tilesheets = []; for (let index = 0; index < tilesheetCount; index++) { tilesheets.push(new jimp_1.Jimp({ width: localOptions.trickplayImageWidth * localOptions.trickplaySheetColumns, height: sampleFrame.height * localOptions.trickplaySheetRows, color: "0xffffffff" })); } console.log(`Tilesheet dimensions:\nwidth:${tilesheets[0].width}\nheight:${tilesheets[0].height}`); let imageOperations = []; framePaths2dGrid.forEach((row, rowIndex) => __awaiter(this, void 0, void 0, function* () { let targetTilesheetIndex = Math.floor(rowIndex / localOptions.trickplaySheetRows); console.log(targetTilesheetIndex); row.forEach((item, itemIndex) => __awaiter(this, void 0, void 0, function* () { imageOperations.push(new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { let targetItemPath = node_path_1.default.resolve(rawFramesDirectory, item); console.log(targetItemPath); let foundImage = yield jimp_1.Jimp.read(targetItemPath); let xPosition = itemIndex * localOptions.trickplayImageWidth; let yPosition = (rowIndex - (targetTilesheetIndex * 10)) * sampleFrame.height; console.log(`Would place image ${item} at tilesheet position x: ${xPosition}, y: ${yPosition}, row ${rowIndex}, tilesheet ${targetTilesheetIndex}`); yield tilesheets[targetTilesheetIndex].composite(foundImage, xPosition, yPosition); resolve(true); }))); })); })); yield Promise.all(imageOperations); tilesheets.forEach((tilesheet, index) => __awaiter(this, void 0, void 0, function* () { yield tilesheet.write(`${tilesheetDirectory}/${index}.${localOptions.tilesheetFileFormat}`); })); //#endregion }); }