import type { Task, Helpers } from "graphile-worker"; import { PrismaClient } from "../../generated/prisma"; import { withAccelerate } from "@prisma/extension-accelerate"; import { getOrDownloadAsset } from "../utils/cache"; import { env } from "../config/env"; import { S3Client } from "@aws-sdk/client-s3"; import { getS3Client, uploadFile } from "../utils/s3"; import { nanoid } from "nanoid"; import { getNanoSpawn } from "../utils/nanoSpawn"; const prisma = new PrismaClient().$extends(withAccelerate()); interface Payload { vodId: string; } async function createThumbnail(helpers: Helpers, inputFilePath: string) { helpers.logger.debug(`createThumbnail with inputFilePath=${inputFilePath}`) if (!inputFilePath) { throw new Error("inputFilePath is missing"); } const outputFilePath = inputFilePath.replace(/\.[^/.]+$/, '') + '-thumb.png'; const spawn = await getNanoSpawn(); const result = await spawn('vcsi', [ inputFilePath, '--metadata-position', 'hidden', '--metadata-margin', '0', '--metadata-horizontal-margin', '0', '--metadata-vertical-margin', '0', '--grid-spacing', '0', '--grid-horizontal-spacing', '0', '--grid-vertical-spacing', '0', '--timestamp-horizontal-margin', '0', '--timestamp-vertical-margin', '0', '--timestamp-horizontal-padding', '0', '--timestamp-vertical-padding', '0', '-w', '830', '-g', '5x5', '-o', outputFilePath ], { stdout: 'inherit', stderr: 'inherit', }); // const exitCode = await subprocess; // if (exitCode !== 0) { // console.error(`vcsi failed with exit code ${exitCode}`); // process.exit(exitCode); // } helpers.logger.debug('result as follows') helpers.logger.debug(JSON.stringify(result, null, 2)) helpers.logger.info(`✅ Thumbnail saved to: ${outputFilePath}`); return outputFilePath } function assertPayload(payload: any): asserts payload is Payload { if (typeof payload !== "object" || !payload) throw new Error("invalid payload-- was not an object."); if (typeof payload.vodId !== "string") throw new Error("invalid payload-- was missing vodId"); } export default async function createVideoThumbnail(payload: any, helpers: Helpers) { assertPayload(payload) const { vodId } = payload const vod = await prisma.vod.findFirstOrThrow({ where: { id: vodId } }) // * [x] load vod // * [x] exit if video.thumbnail already defined if (vod.thumbnail) { helpers.logger.info(`Doing nothing-- vod ${vodId} already has a thumbnail.`) return; // Exit the function early } if (!vod.sourceVideo) { throw new Error(`Failed to create thumbnail-- vod ${vodId} is missing a sourceVideo.`); } helpers.logger.info('Creating Video Thumbnail') const s3Client = getS3Client() // * [x] download video segments from pull-thru cache const videoFilePath = await getOrDownloadAsset(s3Client, env.S3_BUCKET, vod.sourceVideo) console.log(`videoFilePath=${videoFilePath}`) // * [x] run vcsi const thumbnailPath = await createThumbnail(helpers, videoFilePath) console.log(`thumbnailPath=${thumbnailPath}`) // * [x] generate thumbnail s3 key const s3Key = `/thumb/${nanoid()}` // * [x] upload thumbnail to s3 await uploadFile(s3Client, env.S3_BUCKET, s3Key, thumbnailPath, 'image/png') // * [x] update vod record await prisma.vod.update({ where: { id: vodId }, data: { thumbnail: s3Key } }); // * [x] done }