Video Hosting for Next.js
Add video upload and playback to your Next.js app in minutes.
Installation
npm install @playvideo/playvideo-sdk
Setup
Environment Variables
# .env.local
BLOCKBUSTER_API_KEY=play_live_your_api_key
Initialize Client (Server-Side Only)
// lib/playvideo.ts
import PlayVideo from '@playvideo/playvideo-sdk';
export const bb = new PlayVideo(process.env.BLOCKBUSTER_API_KEY!);
Important: Only use the PlayVideo client in server-side code (API routes, Server Components, server actions). Never expose your API key to the browser.
Upload Videos
Option 1: Server Action (App Router)
// app/actions/upload.ts
'use server';
import { bb } from '@/lib/playvideo';
export async function uploadVideo(formData: FormData) {
const file = formData.get('video') as File;
// Convert File to Buffer
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
const result = await bb.videos.upload({
file: buffer,
collection: 'user-uploads',
filename: file.name
});
return result.video;
}
// app/upload/page.tsx
'use client';
import { uploadVideo } from './actions';
import { useState } from 'react';
export default function UploadPage() {
const [uploading, setUploading] = useState(false);
async function handleSubmit(formData: FormData) {
setUploading(true);
const video = await uploadVideo(formData);
console.log('Uploaded:', video.id);
setUploading(false);
}
return (
<form action={handleSubmit}>
<input type="file" name="video" accept="video/*" />
<button type="submit" disabled={uploading}>
{uploading ? 'Uploading...' : 'Upload Video'}
</button>
</form>
);
}
Option 2: API Route
// app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { bb } from '@/lib/playvideo';
export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get('video') as File;
if (!file) {
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
const result = await bb.videos.upload({
file: buffer,
collection: 'user-uploads',
filename: file.name
});
return NextResponse.json(result.video);
}
Display Videos
Server Component
// app/videos/page.tsx
import { bb } from '@/lib/playvideo';
import VideoPlayer from '@/components/VideoPlayer';
export default async function VideosPage() {
const { videos } = await bb.videos.list({
collection: 'user-uploads',
status: 'COMPLETED'
});
return (
<div>
<h1>My Videos</h1>
<div className="grid grid-cols-3 gap-4">
{videos.map(video => (
<div key={video.id}>
<VideoPlayer url={video.playlistUrl!} />
<p>{video.filename}</p>
</div>
))}
</div>
</div>
);
}
Video Player Component
// components/VideoPlayer.tsx
'use client';
import { useEffect, useRef } from 'react';
import Hls from 'hls.js';
interface VideoPlayerProps {
url: string;
poster?: string;
}
export default function VideoPlayer({ url, poster }: VideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(url);
hls.attachMedia(video);
return () => hls.destroy();
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari native HLS
video.src = url;
}
}, [url]);
return (
<video
ref={videoRef}
controls
poster={poster}
className="w-full aspect-video"
/>
);
}
Install hls.js:
npm install hls.js
Upload with Progress
// components/VideoUploader.tsx
'use client';
import { useState, useRef } from 'react';
export default function VideoUploader() {
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState<'idle' | 'uploading' | 'processing' | 'done'>('idle');
const [videoUrl, setVideoUrl] = useState<string | null>(null);
async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
setStatus('uploading');
// Upload to your API route
const formData = new FormData();
formData.append('video', file);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const video = await response.json();
setStatus('processing');
// Poll for completion
const checkStatus = async () => {
const res = await fetch(`/api/videos/${video.id}`);
const updated = await res.json();
if (updated.status === 'COMPLETED') {
setVideoUrl(updated.playlistUrl);
setStatus('done');
} else if (updated.status === 'FAILED') {
setStatus('idle');
alert('Processing failed: ' + updated.errorMessage);
} else {
setProgress(updated.progress || 0);
setTimeout(checkStatus, 2000);
}
};
checkStatus();
}
return (
<div>
{status === 'idle' && (
<input type="file" accept="video/*" onChange={handleUpload} />
)}
{status === 'uploading' && <p>Uploading...</p>}
{status === 'processing' && (
<div>
<p>Processing: {progress}%</p>
<progress value={progress} max={100} />
</div>
)}
{status === 'done' && videoUrl && (
<video src={videoUrl} controls className="w-full" />
)}
</div>
);
}
Webhooks for Real-Time Updates
Instead of polling, use webhooks for production:
// app/api/webhooks/playvideo/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const payload = await request.json();
switch (payload.event) {
case 'video.completed':
// Update your database, notify user, etc.
console.log('Video ready:', payload.video.playlistUrl);
break;
case 'video.failed':
console.error('Video failed:', payload.video.errorMessage);
break;
}
return NextResponse.json({ received: true });
}
Complete Example: Video Gallery
// app/gallery/page.tsx
import { bb } from '@/lib/playvideo';
import VideoPlayer from '@/components/VideoPlayer';
import VideoUploader from '@/components/VideoUploader';
export const revalidate = 60; // Revalidate every 60 seconds
export default async function GalleryPage() {
const { videos } = await bb.videos.list({
status: 'COMPLETED',
limit: 20
});
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Video Gallery</h1>
<div className="mb-8">
<h2 className="text-lg mb-2">Upload New Video</h2>
<VideoUploader />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{videos.map(video => (
<div key={video.id} className="border rounded p-4">
<VideoPlayer
url={video.playlistUrl!}
poster={video.thumbnailUrl!}
/>
<p className="mt-2 text-sm">{video.filename}</p>
<p className="text-xs text-gray-500">
{Math.round(video.duration || 0)}s • {video.resolutions?.join(', ')}
</p>
</div>
))}
</div>
</div>
);
}
Using with Next.js Image
Display video thumbnails with Next.js Image optimization:
import Image from 'next/image';
// In next.config.js, add the CDN domain
// images: { domains: ['cdn.playvideo.dev'] }
<Image
src={video.thumbnailUrl!}
alt={video.filename}
width={320}
height={180}
/>
Self-Hosted Setup
Point to your self-hosted instance:
// lib/playvideo.ts
import PlayVideo from '@playvideo/playvideo-sdk';
export const bb = new PlayVideo({
apiKey: process.env.BLOCKBUSTER_API_KEY!,
baseUrl: process.env.BLOCKBUSTER_URL || 'https://api.playvideo.dev'
});
# .env.local
BLOCKBUSTER_API_KEY=your-key
BLOCKBUSTER_URL=https://video.yourdomain.com