Skip to content

Instantly share code, notes, and snippets.

@tsemachh
Last active August 20, 2024 05:24
Show Gist options
  • Save tsemachh/f95532dfd9f88f0fa62a355fc620e165 to your computer and use it in GitHub Desktop.
Save tsemachh/f95532dfd9f88f0fa62a355fc620e165 to your computer and use it in GitHub Desktop.
PayloadCms 3.00 Thumbnail for video asset
import path from 'path'
import { fileURLToPath } from 'url'
import { type CollectionConfig } from 'payload'
import { videoCoverImage } from './hooks/videoCoverImage'
/* eslint-enable */
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const generateURL = ({ collectionSlug, config, filename }: GenerateURLArgs) => {
if (filename) {
return `${config.serverURL || '/'}${config?.routes?.api || 'api'}/${collectionSlug}/file/${filename}`
}
return undefined
}
/* eslint-disable @typescript-eslint/no-explicit-any */
const isVideo = (data: any) => {
return data?.mimeType?.toLowerCase().includes('video');
}
export const Media: CollectionConfig = {
slug: 'media',
upload: {
staticDir: path.resolve(__dirname, '../../../media'),
imageSizes: [
{
name: 'webp',
formatOptions: { format: 'webp' },
},
{
name: 'thumbnail',
width: 250,
formatOptions: { format: 'webp' },
},
{
name: 'medium',
width: 800,
formatOptions: { format: 'webp', options: { quality: 90 } },
},
{
name: 'large',
width: 1200,
formatOptions: { format: 'webp' },
},
],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
adminThumbnail: ({ doc: originalDoc }) => {
const config = {}
if (isVideo(originalDoc)) {
return originalDoc.thumbnailURL;
}
else
if (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
typeof adminThumbnail === 'string' &&
'sizes' in originalDoc &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
originalDoc.sizes?.[adminThumbnail]?.filename
) {
return generateURL({
collectionSlug: "media",
config,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
filename: originalDoc.sizes?.[adminThumbnail].filename as string,
})
}
},
},
fields: [
{
name: 'coverImage',
type: 'upload',
relationTo: 'media',
filterOptions: {
mimeType: { contains: 'image' },
},
admin: {
hidden: true,
},
},
],
hooks: {
beforeChange: [videoCoverImage],
}
}
import { withPayload } from '@payloadcms/next/withPayload'
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverSourceMaps: true,
},
productionBrowserSourceMaps: true,
output: 'standalone',
serverExternalPackages: ["ffmpeg-static"],
}
export default withPayload(nextConfig)
"ffmpeg-static": "^5.2.0",
"fluent-ffmpeg": "^2.1.3",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
import ffmpeg from 'fluent-ffmpeg'
import ffmpegStatic from 'ffmpeg-static'
import fs from 'fs'
import { CollectionBeforeChangeHook } from 'payload'
ffmpeg.setFfmpegPath(ffmpegStatic)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
async function extractFrame(tempFilePath, outputPath) {
return new Promise<void>((resolve, reject) => {
ffmpeg()
.input(tempFilePath)
.videoFilters('select=eq(n\\,0)') // Select first frame
.output(outputPath)
.on('end', () => {
resolve();
})
/* eslint-disable @typescript-eslint/no-explicit-any */
.on('error', (err: any) => {
reject(err);
})
.run();
});
}
export const videoCoverImage: CollectionBeforeChangeHook = async ({ data, req, collection }) => {
'use server'
if (req.context?.from || !data?.mimeType?.startsWith('video') || data.thumbnailURL) {
return data
}
console.log('<<<<<<<<<< Starting videoCoverImage hook')
try {
if (req.file) {
const tempVideoFilePath = `${collection.upload.staticDir}/tmp_${req.file.name}`
const outpuCoverImagetPath = `${collection.upload.staticDir}/coverImage_${req.file.name}.webp`
fs.writeFileSync(tempVideoFilePath, req.file.data)
// Extract the first frame
await extractFrame(tempVideoFilePath, outpuCoverImagetPath)
console.log('Finished processing')
const coverImageDoc = await req.payload.create({
collection: 'media',
data: { title: 'Auto generated cover image for ' + req.file.name },
filePath: outpuCoverImagetPath,
context: { from: 'hook' },
})
data.coverImage = coverImageDoc.id
data.thumbnailURL = coverImageDoc.sizes.thumbnail.url
data.sizes.thumbnail = {"url":data.thumbnailURL}
console.log('Cover image created and assigned to media:', coverImageDoc.id)
//Remove the temporary files after processing
fs.unlinkSync(tempVideoFilePath)
fs.unlinkSync(outpuCoverImagetPath)
}
console.log(data)
return data
} catch (error) {
console.error('<<<<<<<<<< Error processing video cover image:', (error as Error).message)
throw new Error('Video cover image processing failed.' + (error as Error).message)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment