Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 59 additions & 4 deletions app/api/video-overlay/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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:[email protected]: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:`
+ `[email protected]:`
+ `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'
Expand Down
88 changes: 74 additions & 14 deletions components/VideoOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -41,31 +63,64 @@ 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

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)
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down