Skip to main content

@happyvertical/video

License: MIT

Video processing utilities with adapter pattern for composition and transcoding in the HAVE SDK.

Overview

The @happyvertical/video package provides a unified interface for video processing operations using FFmpeg. It supports video composition, transcoding, overlay placement, thumbnail extraction, and lower-third generation for news-style video production.

Features

  • Video Composition: Combine video, audio, and overlays
  • Transcoding: Convert between formats and adjust quality
  • Thumbnail Extraction: Extract frames at specific timestamps
  • Lower-Third Overlays: News-style name/title graphics
  • Image Overlays: Position logos and watermarks
  • Audio Mixing: Combine multiple audio tracks
  • Metadata Extraction: Get duration, resolution, codec info
  • Format Conversion: Support for MP4, WebM, MOV, and more
  • Type-Safe: Full TypeScript support

Installation

# Install with bun (recommended)
bun add @happyvertical/video

# Or with npm
npm install @happyvertical/video

# Or with pnpm
pnpm add @happyvertical/video

Prerequisites

FFmpeg must be installed and available in your system PATH:

# macOS
brew install ffmpeg

# Ubuntu/Debian
sudo apt install ffmpeg

# Windows (with Chocolatey)
choco install ffmpeg

Quick Start

Basic Usage

import { FFmpegProcessor } from '@happyvertical/video';

const processor = new FFmpegProcessor();

// Get video metadata
const metadata = await processor.getMetadata('input.mp4');
console.log(`Duration: ${metadata.duration}s`);
console.log(`Resolution: ${metadata.width}x${metadata.height}`);
console.log(`FPS: ${metadata.fps}`);

// Extract thumbnail
const thumbnail = await processor.extractFrame('input.mp4', 2.0, {
format: 'jpg',
width: 1280,
quality: 90,
});
fs.writeFileSync('thumbnail.jpg', thumbnail);

Video Composition

const output = await processor.compose({
baseVideo: 'input.mp4',
audio: 'narration.mp3',
overlays: [
{
type: 'image',
content: 'logo.png',
position: { x: 'right', y: 'top', padding: 20 },
opacity: 0.8,
},
{
type: 'text',
content: 'BREAKING NEWS',
position: { x: 'center', y: 'bottom', padding: 50 },
style: {
fontSize: 48,
fontColor: 'white',
backgroundColor: 'red',
},
},
],
outputFormat: 'mp4',
quality: 20,
});

fs.writeFileSync('output.mp4', output);

Lower-Third Graphics

// Add news-style lower-third with name and title
const output = await processor.addLowerThird('input.mp4', {
title: 'John Smith',
subtitle: 'Senior Correspondent',
style: 'news',
primaryColor: '#1a1a1a',
accentColor: '#cc0000',
duration: 5, // seconds
startTime: 0,
fadeIn: 0.5,
fadeOut: 0.5,
});

Transcoding for Social Platforms

// YouTube Shorts (9:16 vertical)
const youtubeShort = await processor.transcode('input.mp4', {
format: 'mp4',
width: 1080,
height: 1920,
video: {
codec: 'h264',
preset: 'medium',
crf: 23,
},
audio: {
codec: 'aac',
bitrate: '128k',
},
});

// Web-optimized (16:9 horizontal)
const webVideo = await processor.transcode('input.mp4', {
format: 'mp4',
width: 1920,
height: 1080,
video: {
codec: 'h264',
preset: 'slow',
crf: 20,
},
audio: {
codec: 'aac',
bitrate: '192k',
},
});

API Reference

FFmpegProcessor

The main processor class for video operations.

Constructor

new FFmpegProcessor(options?: VideoProcessorOptions)
OptionTypeDefaultDescription
ffmpegPathstring'ffmpeg'Path to FFmpeg binary
ffprobePathstring'ffprobe'Path to FFprobe binary
tempDirstringsystem tempDirectory for temp files
timeoutnumber300000Processing timeout (ms)

Methods

getMetadata

Get video file metadata.

async getMetadata(video: Buffer | string): Promise<VideoMetadata>
extractFrame

Extract a single frame as an image.

async extractFrame(
video: Buffer | string,
timestamp: number,
options?: ExtractFrameOptions
): Promise<Buffer>
OptionTypeDefaultDescription
format'jpg' | 'png' | 'webp''jpg'Output format
widthnumberoriginalOutput width
heightnumberoriginalOutput height
qualitynumber85JPEG quality (1-100)
compose

Compose video with overlays and audio.

async compose(options: ComposeOptions): Promise<Buffer>
addLowerThird

Add lower-third graphic overlay.

async addLowerThird(
video: Buffer | string,
config: LowerThirdConfig
): Promise<Buffer>
transcode

Convert video format and adjust quality.

async transcode(
video: Buffer | string,
options: TranscodeOptions
): Promise<Buffer>
addOverlay

Add image or text overlay to video.

async addOverlay(
video: Buffer | string,
overlay: OverlayConfig
): Promise<Buffer>

Types

interface VideoMetadata {
duration: number; // seconds
width: number;
height: number;
fps: number;
codec: string;
bitrate?: number;
audioCodec?: string;
audioBitrate?: number;
audioSampleRate?: number;
}

interface ComposeOptions {
baseVideo: Buffer | string;
audio?: Buffer | string;
overlays?: OverlayConfig[];
outputFormat?: VideoFormat;
quality?: number; // CRF value (lower = better)
}

interface OverlayConfig {
type: 'image' | 'text';
content: Buffer | string;
position: OverlayPosition;
opacity?: number; // 0-1
startTime?: number;
duration?: number;
fadeIn?: number;
fadeOut?: number;
style?: TextOverlayOptions;
}

interface OverlayPosition {
x: number | 'left' | 'center' | 'right';
y: number | 'top' | 'center' | 'bottom';
padding?: number;
}

interface LowerThirdConfig {
title: string;
subtitle?: string;
style?: 'news' | 'minimal' | 'modern';
primaryColor?: string;
accentColor?: string;
textColor?: string;
duration?: number;
startTime?: number;
fadeIn?: number;
fadeOut?: number;
}

interface TranscodeOptions {
format: VideoFormat;
width?: number;
height?: number;
video?: VideoCodecOptions;
audio?: AudioCodecOptions;
}

interface VideoCodecOptions {
codec: 'h264' | 'h265' | 'vp9' | 'av1';
preset?: 'ultrafast' | 'fast' | 'medium' | 'slow' | 'veryslow';
crf?: number; // 0-51 (lower = better quality)
bitrate?: string; // e.g., '5M'
}

interface AudioCodecOptions {
codec: 'aac' | 'mp3' | 'opus';
bitrate?: string; // e.g., '128k'
sampleRate?: number; // e.g., 48000
}

type VideoFormat = 'mp4' | 'webm' | 'mov' | 'avi' | 'mkv';

Use Cases

News Video Production Pipeline

import { FFmpegProcessor } from '@happyvertical/video';

async function processNewsVideo(rawVideo: string, anchorName: string) {
const processor = new FFmpegProcessor();

// 1. Add lower-third with anchor name
const withLowerThird = await processor.addLowerThird(rawVideo, {
title: anchorName,
subtitle: 'News Correspondent',
style: 'news',
primaryColor: '#1a237e',
accentColor: '#c62828',
duration: 5,
startTime: 0,
});

// 2. Add station logo
const withLogo = await processor.addOverlay(withLowerThird, {
type: 'image',
content: 'station-logo.png',
position: { x: 'right', y: 'top', padding: 20 },
opacity: 0.9,
});

// 3. Create multiple formats for distribution
const youtubeShort = await processor.transcode(withLogo, {
format: 'mp4',
width: 1080,
height: 1920,
video: { codec: 'h264', preset: 'medium', crf: 23 },
});

const webVersion = await processor.transcode(withLogo, {
format: 'mp4',
width: 1920,
height: 1080,
video: { codec: 'h264', preset: 'medium', crf: 20 },
});

// 4. Extract thumbnail
const thumbnail = await processor.extractFrame(withLogo, 2.0, {
format: 'jpg',
width: 1280,
quality: 90,
});

return { youtubeShort, webVersion, thumbnail };
}

Batch Thumbnail Generation

async function generateThumbnails(videos: string[], timestamps: number[] = [2.0]) {
const processor = new FFmpegProcessor();
const thumbnails = [];

for (const video of videos) {
for (const timestamp of timestamps) {
const thumbnail = await processor.extractFrame(video, timestamp, {
format: 'webp',
width: 1280,
quality: 85,
});
thumbnails.push({
video,
timestamp,
data: thumbnail,
});
}
}

return thumbnails;
}

Best Practices

Memory Management

// For large videos, use file paths instead of buffers
const output = await processor.transcode('/path/to/large-video.mp4', {
format: 'mp4',
// ...options
});

// Write output to file instead of keeping in memory
fs.writeFileSync('/path/to/output.mp4', output);

Quality vs Size Trade-offs

// High quality (larger file)
{ video: { codec: 'h264', preset: 'slow', crf: 18 } }

// Balanced (recommended)
{ video: { codec: 'h264', preset: 'medium', crf: 23 } }

// Fast encoding (lower quality)
{ video: { codec: 'h264', preset: 'fast', crf: 28 } }

Platform-Specific Encoding

// YouTube recommended settings
{
format: 'mp4',
video: { codec: 'h264', preset: 'slow', crf: 18 },
audio: { codec: 'aac', bitrate: '384k' },
}

// Twitter/X recommended settings
{
format: 'mp4',
video: { codec: 'h264', preset: 'medium', crf: 23 },
audio: { codec: 'aac', bitrate: '128k' },
}

License

This package is part of the HAVE SDK and is licensed under the MIT License - see the LICENSE file for details.