Build Vimeo
KINDAreplaces $41/mosaves $492/yrback to the verdict
A pipeline that transcodes a source video into an adaptive bitrate ladder with ffmpeg, uploads the segments to object storage behind a content delivery network, and serves unlisted embed pages with your own player, so playback adapts to the viewer's connection and the bandwidth bill is one you can read.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why It produces the entire ladder. A build without the encoders you need fails at the first transcode.
Get it brew install ffmpeg, your package manager, or a static build. Confirm the encoder is present rather than assuming. open ↗
Verify
ffmpeg -encoders lists libx264 and ffmpeg -formats lists hls - API keystorage is cheap; egress is the number that matters and varies enormously between providers
Why Holds segments and playlists. Providers differ in egress pricing by a large factor, which matters more here than in any other build.
Get it Create a bucket and credentials scoped to it. Compare egress rates before choosing, because video makes that number the dominant cost.
Verify
A test upload appears and can be fetched - accountvaries widely; check whether your bucket and network providers waive egress between them
Why Video served straight from a bucket is slow for distant viewers and usually the most expensive delivery option available.
Get it Choose a network that can use your bucket as an origin, configure it, then block direct bucket access. Some providers waive egress between their own bucket and network, which changes the arithmetic substantially.
Verify
A segment fetch shows a cache hit and the bucket refuses direct requests - have readyfree
Why Keyframe alignment, caption drift and cost estimates all need a video long enough to reveal them. A thirty second clip proves nothing.
Get it Use your own footage. One short for fast iteration and one over twenty minutes for the alignment and caption checks.
Verify
Both are available and the long one exceeds twenty minutes - installfree, but slow on long videos
Why Phase 4 only, and only for draft transcripts. Everything else works without it.
Get it Build whisper.cpp and download a model. open ↗
Verify
It transcribes a 16 kHz mono WAV
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- videos(id, title, description, source_key, duration_ms, width, height, state, created_at) - renditions(video_id, height, bitrate_kbps, playlist_key, segment_prefix, bytes) - jobs(id, video_id, state, started_at, finished_at, error) · transcoding is slow and fails, so it needs a state machine - captions(video_id, language, vtt_key, kind) · kind is subtitles or captions, and the distinction matters for accessibility - views(video_id, day, plays, watch_seconds) · derived from delivery logs rather than from a tracking script - Segments are immutable and content-addressed by rendition and index, so a cache never needs invalidating.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
BUCKET_NAME | required | video-segments | Where playlists and segments are stored. |
BUCKET_ACCESS_KEYsecret | required | replace-with-key | Scoped to this bucket only. |
BUCKET_SECRET_KEYsecret | required | replace-with-secret | Kept out of the repository. |
CDN_BASE_URL | required | https://cdn.example.com | The hostname viewers actually fetch from. The bucket hostname must never appear in a playlist. |
SIGNING_KEYsecret | optional | replace-with-a-long-random-value | Signs playback URLs for private videos. Required only if you use private mode. |
SIGNED_URL_TTL_SECONDS | optional | 3600 | How long a signed playback URL lasts. Long enough for the video, short enough to matter. |
LADDER | required | 360:800,720:2500,1080:5000 | Height and bitrate pairs. Each rung costs transcoding time and storage. |
The build, in order
A ladder that actually adapts
One source produces several renditions and a master playlist a player can switch between.
Three or four rungs covering a phone on mobile data through to a desktop. More rungs cost transcoding time and storage for diminishing benefit.
Files
docs/ladder.mdA fixed group of pictures size with scene change detection off, applied identically to every rendition.
terminalffmpeg -i in.mp4 -g 48 -keyint_min 48 -sc_threshold 0 -c:v libx264 -b:v 2500k -maxrate 2675k -bufsize 3750k -hls_time 4 -hls_playlist_type vod -hls_segment_filename '720p_%03d.ts' 720p.m3u8
Extract keyframe timestamps per rendition and assert they match. Do not judge this by watching.
Files
scripts/verify_keyframes.shterminalffprobe -select_streams v -skip_frame nokey -show_entries frame=pkt_pts_time -of csv 720p.m3u8
done when · tick each as it passeswatch out- If keyframes are not at identical timestamps in every rendition, the player cannot switch cleanly and viewers see a stutter or a black frame at each change. Force a fixed keyframe interval and disable scene-change keyframes, so every rendition has a keyframe at exactly the same points.
Storage and delivery
Segments are served from a cache close to the viewer, and the bucket is not directly reachable.
Segments get a long cache lifetime because they never change. Playlists get a short one because they can.
Files
src/upload.pyOrigin is the bucket, and the bucket only accepts requests from the network. Test the direct path is closed.
Request a segment twice and check for a cache hit. A network that misses every time is costing you origin egress anyway.
terminalcurl -sI https://cdn.example.com/v/abc/720p_001.ts | grep -i cache
done when · tick each as it passeswatch out- Serving video directly from a bucket means paying that provider's egress rate for every byte of every view, which is usually several times what a delivery network in front costs and scales badly the moment anything is popular. Put the network in front before you share a single link, not after the first bill.
The player and the embed page
A link plays anywhere, including on a phone, and can be embedded on another site.
If the browser can play the stream natively, let it, and do not attach the JavaScript player at all.
Files
static/player.jsEmbed is minimal and frameable; watch has the title, description and a share control.
Files
templates/Open Graph tags with a generated thumbnail, and controls that work without a mouse.
Files
templates/watch.html
done when · tick each as it passeswatch out- Safari plays HTTP Live Streaming natively and will fight a JavaScript player attached to the same element, producing double playback or none. Detect native support and use it, loading the JavaScript player only where it is needed.
Captions, which are not optional
Every video can carry captions, and they display correctly.
Standard timed text files, validated on upload, served as separate tracks.
Files
src/captions.pywhisper.cpp for a first pass, stored as a draft with a clear state.
Files
src/transcribe.pyA draft cannot become a published track without a deliberate action. Enforce it in the state machine, not in the interface only.
Files
src/captions.py
done when · tick each as it passeswatch out- Automatic transcription is a draft, not captions. Publishing an unreviewed machine transcript as captions produces confidently wrong text for exactly the viewers who depend on it most, and it is worse than none because it looks authoritative. Require the review step in the workflow rather than trusting people to remember.
Unlisted, private and expiring links
A video can be shared with some people and not with everyone.
Enough entropy that enumeration is impractical, and no sequential identifiers anywhere in URLs.
Files
src/ids.pyThe delivery network validates the signature. Test by requesting a segment directly without one.
Referrer restriction is weak but useful; signed embeds are stronger. Say which you chose and what it does not stop.
Files
src/embed.py
done when · tick each as it passeswatch out- An unguessable URL is obscurity rather than access control, which is fine for unlisted and useless for private. If a video is genuinely private, the segments must require signed requests, because otherwise anyone with the playlist can fetch every segment directly and the player was never the gate.
Costs, analytics and operationsproduct builder
You know what a view costs and what it would cost if a video did well.
Segment requests grouped by video give plays and watch time without a tracking script and without consent complications.
Files
src/analytics.pyEgress bytes times your rate, plus storage and transcoding. Publish it in your own documentation so the number is not a surprise.
Files
docs/costs.mdLower the threshold deliberately and confirm the alert arrives. An untested alert is a hope.
done when · tick each as it passeswatch out- A single video going unexpectedly wide can produce a bandwidth bill an order of magnitude above your normal month, and you find out afterwards. Set a spend alert at your provider and a hard limit if one is available, before publishing anything you expect to travel.
That is the whole plan for Vimeo. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Live streaming, webinars and events.
- Publishable-quality automatic captions and chapters.
- Deep engagement analytics from an instrumented player.
- Digital rights management.
- live streaming, webinars, and events
- auto captions, chapters, and the accessibility tooling around them
- viewer analytics and engagement graphs
- OTT, pay-per-view, and the monetization stack
- a player QA'd against every browser, smart TV, and embed surface
- traffic spikes absorbed by someone else's bandwidth bill
- Per-title encoding, choosing bitrates from the content rather than a fixed ladder
- Thumbnail sprites for scrub previews, which are cheap and much missed when absent
- A second delivery network as a failover origin, once the first one has an outage
Need the files? The project pack on the verdict page hands your agent the whole brief · more audio & video.