Video Streaming

Transcode videos to HLS adaptive streaming and MP4 formats for optimal playback across all devices. Sources without an audio stream are supported and produce video-only renditions.

Transcode Video

Start a transcoding job to convert a video to HLS or MP4:

transcode.ts
import { Stack0 } from '@stack0/sdk'
const stack0 = new Stack0({
apiKey: process.env.STACK0_API_KEY!
})
// Start a transcoding job
const job = await stack0.cdn.transcode({
projectSlug: 'my-project',
assetId: 'video-asset-id',
outputFormat: 'hls', // 'hls' for adaptive streaming, 'mp4' for download
variants: [
{ quality: '720p', codec: 'h264' },
{ quality: '1080p', codec: 'h264' },
],
webhookUrl: 'https://your-app.com/webhook', // Optional
})
console.log(`Job ID: ${job.id}, Status: ${job.status}`)

Quality Variants

Available quality presets for video transcoding:

QualityResolutionUse Case
360p640x360Low bandwidth, mobile data
480p854x480Standard definition
720p1280x720HD, most common
1080p1920x1080Full HD
1440p2560x14402K / QHD
2160p3840x21604K Ultra HD

Check Job Status

job-status.ts
// Get job status
const job = await stack0.cdn.getJob('job_abc123')
console.log(`Status: ${job.status}`)
console.log(`Progress: ${job.progress}%`)
if (job.status === 'failed') console.error(job.errorMessage)
// Status values:
// 'pending' - Job created, waiting to be processed
// 'queued' - Job queued for processing
// 'processing' - Currently transcoding
// 'completed' - Finished successfully
// 'failed' - Transcoding failed
// 'cancelled' - Job was cancelled

List Jobs

list-jobs.ts
// List all transcoding jobs
const { jobs, total, hasMore } = await stack0.cdn.listJobs({
projectSlug: 'my-project',
assetId: 'video-asset-id', // Optional filter
status: 'processing', // Optional filter
limit: 20,
offset: 0,
})
for (const job of jobs) {
console.log(`${job.id}: ${job.status} (${job.progress}%)`)
}

Get Streaming URLs

After transcoding completes, get URLs for playback:

streaming-urls.ts
const urls = await stack0.cdn.getStreamingUrls('asset-id')
// HLS adaptive streaming (recommended for web/mobile)
console.log(`HLS: ${urls.hlsUrl}`)
// MP4 direct download by quality
for (const mp4 of urls.mp4Urls) {
console.log(`${mp4.quality}: ${mp4.url}`)
}
// Generated thumbnails
for (const thumb of urls.thumbnails) {
console.log(`Thumbnail at ${thumb.timestamp}s: ${thumb.url}`)
}

Video Player Integration

Use HLS.js for cross-browser HLS playback:

hls-player.tsx
import Hls from 'hls.js'
const urls = await stack0.cdn.getStreamingUrls('asset-id')
const video = document.getElementById('video') as HTMLVideoElement
if (Hls.isSupported() && urls.hlsUrl) {
const hls = new Hls()
hls.loadSource(urls.hlsUrl)
hls.attachMedia(video)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play()
})
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari native HLS support
video.src = urls.hlsUrl!
video.addEventListener('loadedmetadata', () => {
video.play()
})
}

Generate Thumbnails

thumbnails.ts
// Get the thumbnail at a specific timestamp. Works for any ready video
// asset (uploads included) — if the frame doesn't exist yet, generation is
// queued automatically and getThumbnailAndWait polls until it's ready.
const thumbnail = await stack0.cdn.getThumbnailAndWait({
assetId: 'video-asset-id',
timestamp: 10.5, // 10.5 seconds into the video
width: 320, // Optional: resize
format: 'webp', // 'jpg', 'png', 'webp'
})
console.log(`Thumbnail: ${thumbnail.url}`)
// Non-blocking variant: returns { status: 'pending', url: null } while
// the frame is being generated — poll until status is 'ready'
const maybe = await stack0.cdn.getThumbnail({
assetId: 'video-asset-id',
timestamp: 0,
})
// Force a fresh capture (e.g. after replacing the video), then wait
await stack0.cdn.regenerateThumbnail({ assetId: 'video-asset-id', timestamp: 5 })
const fresh = await stack0.cdn.getThumbnailAndWait({ assetId: 'video-asset-id', timestamp: 5 })

Extract Audio

extract-audio.ts
// Extract audio from video
const result = await stack0.cdn.extractAudio({
projectSlug: 'my-project',
assetId: 'video-asset-id',
format: 'mp3', // 'mp3', 'aac', 'wav'
bitrate: 192, // Optional: kbps
})
console.log(`Status: ${result.status}`)

Generate GIF

Create an animated GIF from a video segment:

generate-gif.ts
// Generate a GIF from video
const gif = await stack0.cdn.generateGif({
projectSlug: 'my-project',
assetId: 'video-asset-id',
startTime: 5, // Start at 5 seconds
duration: 3, // 3 second GIF (max 30s)
width: 480, // Output width (100-800px)
fps: 10, // Frames per second (5-30)
optimizePalette: true, // Better quality, smaller file
})
// Poll for completion
let result = await stack0.cdn.getGif(gif.id)
while (result?.status === 'pending' || result?.status === 'processing') {
await new Promise(r => setTimeout(r, 1000))
result = await stack0.cdn.getGif(gif.id)
}
console.log(`GIF URL: ${result?.url}`)
console.log(`Size: ${result?.sizeBytes} bytes`)

GIF Options

OptionDefaultDescription
startTime0Start position in seconds
duration5Duration in seconds (0.5-30)
width480Output width 100-800px
fps10Frames per second (5-30)
optimizePalettetrueTwo-pass palette for smaller files

Tip: For smaller file sizes, use lower fps (10 is usually enough), smaller width, and keep duration under 10 seconds.

Analysis Options

createVideoAnalysis probes a video or audio asset and finds its scene cuts, plus the outputs below.

OptionDefaultDescription
frameIntervalSeconds1Seconds between frames in the contact sheets (0.25-10)
framestrueJPEG contact sheets of the sampled frames. False by default when motion is on.
extractAudiotrueA mono 16 kHz MP3 of the soundtrack. False by default when motion is on.
motionfalseHow much the video moves: the mean frame-to-frame difference every 0.1 s, grayscale at 320 px, on a 0-255 scale. Returned as result.motion.
beatsfalseBeats, kicks, the drop and loudness of the soundtrack

Add Watermark

watermark.ts
// Transcode with an image (logo) watermark.
// Uses the same config shape as image watermarks, so you can reuse the
// same settings across photos and video. The original asset stays clean;
// every rendition is watermarked.
const job = await stack0.cdn.transcode({
projectSlug: 'my-project',
assetId: 'video-asset-id',
outputFormat: 'hls',
variants: [{ quality: '1080p', codec: 'h264' }],
watermark: {
assetId: 'logo-asset-id', // your logo uploaded to the CDN (or use `url`)
position: 'bottom-right',
scale: 15, // 15% of the video width
opacity: 80,
offsetX: 20,
offsetY: 20,
},
})
// Tile a watermark across the whole frame for copyright protection
const tiledJob = await stack0.cdn.transcode({
projectSlug: 'my-project',
assetId: 'video-asset-id',
outputFormat: 'mp4',
variants: [{ quality: '720p', codec: 'h264' }],
watermark: {
assetId: 'logo-asset-id',
tile: true,
opacity: 30,
rotation: -30,
},
})
// Text watermarks can use the same tiling controls
const textJob = await stack0.cdn.transcode({
projectSlug: 'my-project',
assetId: 'video-asset-id',
outputFormat: 'mp4',
variants: [{ quality: '720p', codec: 'h264' }],
watermark: {
type: 'text',
text: 'My Company',
fontSize: 32,
fontColor: '#ffffff',
tile: true,
tileSpacingX: 120,
tileSpacingY: 120,
rotation: -30,
opacity: 30,
},
})

Trim Video

trim.ts
// Transcode only a portion of the video
const job = await stack0.cdn.transcode({
projectSlug: 'my-project',
assetId: 'video-asset-id',
outputFormat: 'mp4',
variants: [{ quality: '720p', codec: 'h264' }],
trim: {
start: 30, // Start at 30 seconds
end: 120, // End at 2 minutes
},
})

Pricing

Transcoding

$0.005

per minute of output video

Streaming

$0.05

per GB bandwidth delivered

Audio

$0.002

per minute of audio processed