diff --git a/app/api/video-overlay/route.ts b/app/api/video-overlay/route.ts index 32c28d4..dd5dd4f 100644 --- a/app/api/video-overlay/route.ts +++ b/app/api/video-overlay/route.ts @@ -8,6 +8,10 @@ import { tmpdir } from 'os'; const execAsync = promisify(exec); +// Constants for video dimensions +const TARGET_WIDTH = 1080; +const TARGET_HEIGHT = 1920; + export async function POST(request: Request) { try { const { text, videoUrl } = await request.json(); @@ -28,14 +32,65 @@ export async function POST(request: Request) { const videoBuffer = await videoResponse.arrayBuffer(); await writeFile(inputPath, Buffer.from(videoBuffer)); - // Process video with ffmpeg - const ffmpegCommand = `ffmpeg -i ${inputPath} -vf "drawtext=text='${text}':fontcolor=white:fontsize=72:box=1:boxcolor=black@0.5:boxborderw=5:x=(w-text_w)/2:y=(h-text_h)/2:font=Arial:enable='between(t,0,20)'" -codec:a copy ${outputPath}`; + // First, get video dimensions + const { stdout: videoInfo } = await execAsync( + `ffprobe -v error -select_streams v:0 -show_entries stream=width,height,duration -of json ${inputPath}` + ); + const { streams: [{ width, height, duration }] } = JSON.parse(videoInfo); + + // Calculate crop parameters + const videoAspectRatio = width / height; + const targetAspectRatio = TARGET_HEIGHT / TARGET_WIDTH; + let cropW, cropH, x, y; + + if (videoAspectRatio > targetAspectRatio) { + // Video is wider than target - crop sides + cropH = height; + cropW = Math.round(height * (TARGET_WIDTH / TARGET_HEIGHT)); + x = Math.round((width - cropW) / 2); + y = 0; + } else { + // Video is taller than target - crop top/bottom + cropW = width; + cropH = Math.round(width * (TARGET_HEIGHT / TARGET_WIDTH)); + x = 0; + y = Math.round((height - cropH) / 2); + } + + // Calculate font size based on video width + const fontSize = Math.floor(TARGET_WIDTH / 20); // Responsive font size + // Prepare text for FFmpeg (escape special characters) + const escapedText = text.replace(/'/g, "'\\''"); + + // Build FFmpeg command with crop and text overlay + const ffmpegCommand = `ffmpeg -i ${inputPath} ` + + `-vf "` + + `crop=${cropW}:${cropH}:${x}:${y},` // Crop to 9:16 + + `scale=${TARGET_WIDTH}:${TARGET_HEIGHT},` // Scale to target dimensions + + `drawtext=` + + `text='${escapedText}':` + + `fontcolor=white:` + + `fontsize=${fontSize}:` + + `box=1:` + + `boxcolor=black@0.5:` + + `boxborderw=5:` + + `x=(w-text_w)/2:` + + `y=h-h/4:` // Position in lower third + + `font=Arial:` + + `line_spacing=10:` + + `textfile_enable=0:` // Disable text file mode + + `text_wrap=1:` // Enable text wrapping + + `text_width=w-100" ` // Width for text wrapping (50px padding each side) + + `-c:a copy ` + + outputPath; + + // Process the video await execAsync(ffmpegCommand); - // Upload to Vercel Blob with a more structured filename + // Upload to Vercel Blob const outputBuffer = await readFile(outputPath); - const filename = `videos/${Date.now()}-overlay.mp4`; + const filename = `videos/${Date.now()}-overlay-vertical.mp4`; const blob = await put(filename, outputBuffer, { access: 'public', contentType: 'video/mp4' diff --git a/components/VideoOverlay.tsx b/components/VideoOverlay.tsx index fdde2e3..7d0384b 100644 --- a/components/VideoOverlay.tsx +++ b/components/VideoOverlay.tsx @@ -15,15 +15,37 @@ export default function VideoOverlay({ videoUrl, overlayText }: VideoOverlayProp const [videoLoaded, setVideoLoaded] = useState(false) const [videoDimensions, setVideoDimensions] = useState({ width: 0, height: 0 }) - // Handle video metadata loaded to get dimensions + // Constants for target aspect ratio + const TARGET_WIDTH = 1080 + const TARGET_HEIGHT = 1920 + const TARGET_ASPECT_RATIO = TARGET_HEIGHT / TARGET_WIDTH + + // Updated text constants for better vertical video display + const TEXT_PADDING = 40 + const BASE_FONT_SIZE = Math.floor(TARGET_WIDTH / 20) // Responsive font size + const LINE_HEIGHT = Math.floor(BASE_FONT_SIZE * 1.5) // Proportional line height + + // Handle video metadata loaded to get dimensions and calculate crop useEffect(() => { const video = videoRef.current if (!video) return const handleMetadataLoaded = () => { + const videoWidth = video.videoWidth + const videoHeight = video.videoHeight + + // Calculate dimensions that maintain aspect ratio + let cropWidth = videoWidth + let cropHeight = videoWidth * TARGET_ASPECT_RATIO + + if (cropHeight > videoHeight) { + cropHeight = videoHeight + cropWidth = videoHeight / TARGET_ASPECT_RATIO + } + setVideoDimensions({ - width: video.videoWidth, - height: video.videoHeight, + width: TARGET_WIDTH, + height: TARGET_HEIGHT, }) setVideoLoaded(true) } @@ -41,8 +63,8 @@ export default function VideoOverlay({ videoUrl, overlayText }: VideoOverlayProp const canvas = canvasRef.current if (!video || !canvas || !videoLoaded) return - canvas.width = videoDimensions.width - canvas.height = videoDimensions.height + canvas.width = TARGET_WIDTH + canvas.height = TARGET_HEIGHT const ctx = canvas.getContext("2d") if (!ctx) return @@ -50,22 +72,55 @@ export default function VideoOverlay({ videoUrl, overlayText }: VideoOverlayProp let animationId: number const drawFrame = () => { - // Draw video frame - ctx.drawImage(video, 0, 0, canvas.width, canvas.height) + // Calculate source dimensions for cropping + const videoAspectRatio = video.videoWidth / video.videoHeight + let sourceX = 0 + let sourceY = 0 + let sourceWidth = video.videoWidth + let sourceHeight = video.videoHeight + + if (videoAspectRatio > 9/16) { + // Video is wider than target ratio - crop sides + sourceWidth = video.videoHeight * (9/16) + sourceX = (video.videoWidth - sourceWidth) / 2 + } else { + // Video is taller than target ratio - crop top/bottom + sourceHeight = video.videoWidth * (16/9) + sourceY = (video.videoHeight - sourceHeight) / 2 + } + + // Clear canvas and draw cropped video frame + ctx.clearRect(0, 0, canvas.width, canvas.height) + ctx.drawImage( + video, + sourceX, sourceY, sourceWidth, sourceHeight, // Source dimensions + 0, 0, TARGET_WIDTH, TARGET_HEIGHT // Destination dimensions + ) - // Draw text overlay + // Draw text overlay with updated measurements ctx.fillStyle = "rgba(0, 0, 0, 0.5)" - const textBoxHeight = calculateTextHeight(ctx, overlayText, canvas.width - 40) - ctx.fillRect(0, (canvas.height - textBoxHeight) / 2, canvas.width, textBoxHeight) + const textWidth = TARGET_WIDTH - (TEXT_PADDING * 2) + const textBoxHeight = calculateTextHeight(ctx, overlayText, textWidth) + + // Position the text box in the lower third of the video + const textBoxY = (TARGET_HEIGHT * 0.7) - (textBoxHeight / 2) + ctx.fillRect(0, textBoxY, TARGET_WIDTH, textBoxHeight) // Text styling ctx.fillStyle = "white" - ctx.font = "bold 32px Arial" + ctx.font = `bold ${BASE_FONT_SIZE}px Arial` ctx.textAlign = "center" ctx.textBaseline = "middle" // Draw wrapped text - drawWrappedText(ctx, overlayText, canvas.width / 2, canvas.height / 2, canvas.width - 40, 40) + drawWrappedText( + ctx, + overlayText, + TARGET_WIDTH / 2, + textBoxY + textBoxHeight / 2, + textWidth, + LINE_HEIGHT + ) animationId = requestAnimationFrame(drawFrame) } @@ -98,10 +153,12 @@ export default function VideoOverlay({ videoUrl, overlayText }: VideoOverlayProp // Function to calculate text height based on wrapping const calculateTextHeight = (ctx: CanvasRenderingContext2D, text: string, maxWidth: number): number => { const words = text.split(" ") - const lineHeight = 40 let lines = 1 let currentLine = words[0] + // Set font here to ensure accurate measurements + ctx.font = `bold ${BASE_FONT_SIZE}px Arial` + for (let i = 1; i < words.length; i++) { const word = words[i] const width = ctx.measureText(currentLine + " " + word).width @@ -114,7 +171,7 @@ export default function VideoOverlay({ videoUrl, overlayText }: VideoOverlayProp } } - return lines * lineHeight + 40 // Add padding + return lines * LINE_HEIGHT + TEXT_PADDING * 2 // Padding top and bottom } // Function to draw wrapped text @@ -130,6 +187,9 @@ export default function VideoOverlay({ videoUrl, overlayText }: VideoOverlayProp const lines: string[] = [] let currentLine = words[0] + // Set font here to ensure consistent rendering + ctx.font = `bold ${BASE_FONT_SIZE}px Arial` + for (let i = 1; i < words.length; i++) { const word = words[i] const width = ctx.measureText(currentLine + " " + word).width