Skip to main content

Project

Streamgeek

How I built a low-cost, self-owned adaptive video streaming service.

video · straming · cloudflare · r2 · hls

Steaming video is tough!

Why?

The key problem is this:

Uploading a thicc video file directly and then letting your users play that bad boi via a simple <video> tag is a slow/bad experience.

“But Tom” you say, “there are many many services that solve this already”.

To that I say:

  • Paid services often cost more money than they’re worth (I’m looking at you Vimeo 🤨)
  • Free services like YouTube are untrustworthy (tracking yo users, adding banners, sneakying in a lil sneaky ad 🤭) - its a NO from me 😅

You can use tool’s like the media-chome web component to render a custom youtube video player but you still get the yucky Youtube UI under it.

This kinda yucky, also probably voilates some TOCs and could break if youtube changes something
This kinda yucky, also probably voilates some TOCs and could break if youtube changes something

Streaming protocols

So how do these peeps make video chunk streaming possible? Enter the poorly named HTTP Live Streaming protocol (HLS) … well suited for VOD (which aint “live” lol).

It enables adaptive streaming - your users starts watching the vid ASAP (no matter the quality) and, based on their connection, chunks of different resolutions can get streamed in as they go - enabling a lekker smooth experience. There’s a few other procotocols that do simmilair things like DASH (less popular but supports more codecs) and WebRTC (low-latency for live streaming).

HLS remains very popular for the VOD (video-on-demand) style of video streaming tho.

Solution

I wondered how hard it would be to spin up my own sneaky Vimeo/Youtube/Mux alternative using HLS streams without breaking the BANK or the BRAIN 💆💸

Turns out… a little brain breaking - but IT WORKS BABAAY!

Introducing Streamgeek ! Have a look at a fun lil life vid I made and uploaded to my own streamgeek deployment:

The Idea

Video streams that meet user expectations by being fast and reliable are traditionally something web devs would outsource to a third-party service (often just by embedding Youtube 🤮).

Up to now, the excuse has been that rolling your own video hosting is “hard” or “expensive” - however I think that’s not true in 2026 🤓

I built streamgeek as a POC so that you (yes you 🫵) can do this too.

The main pillars I had for this solution were as follows:

  1. highly available reliable adaptive videos served from a server in a close location:
    Cloudflare R2 and their edge network
  2. very customisable hls video player that looks and works good:
    media-chrome’s hls-video-element
  3. less highly available uploading and generation of HLS playlists. I’m uploading WAY LESS then viewers would be streaming. If this dies sometimes it would be irritating but chill:
    A lightweight hono app in nodejs piping through commands to ffmpeg on my own hardware
  4. tying everything together with a nice ui and embed links (so you can share anywhere):
    rwsdk + hono + react + tailwind on cloudflare workers

The architechture for this bad boi
The architechture for this bad boi

Here’s the upload flow in action:

Storage

Inspired by this awesome article by Screencasting.com - storing the hls video playlists on Cloudflare R2 means free egress (yeah you heard that right dawg!) - you only pay for the amount of videos you store and not the bandwidth (ie: views) your videos get!

That’s literally the main gotcha most of these paid services hit you with - bandwidth fees!
If your site’s vids get lots of traffic… RIP yo bank account 🪦

Adaptive Video

Adapting to different resolutions based on the user’s available bandwidth with adaptive bitrate streaming.

This HLS video starts with a 720p video chunk but then jumps up to 1080p chunks after realising the user's connection is more capable - awesome!
This HLS video starts with a 720p video chunk but then jumps up to 1080p chunks after realising the user's connection is more capable - awesome!

The anatomy of an HLS stream

This lovely article breaks it down in more detail but its basically:

  • A bunch of .ts (transport stream) files - these are the video chunks of different resolutions (not typescript files hehe).
  • Each resolution has a m3u8 media playlist that points to each of the corresponding resolutions’s ts video chunks.
  • There’s a master m3u8 file, this serves as the entrypoint for the HLS stream - pointing to each of the different resolutions. Users will generally start at a lower bitrate and then bump up as a your speed is established.

Here’s an example of what that master playlist looks like:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=10000000,RESOLUTION=2160x1440
1440p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1620x1080
1080p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1080x720
720p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=720x480
480p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=540x360
360p.m3u8

DASH is a competing standard for streaming video - with wider codec support and more customisability. I ultimately opted to use HLS instead due to its broader adoption. Mux has a great write up on the pros and cons of each. So you’re loosing out on VP9 codec support (Google’s awesome codec for web video) but winning with simplicity. I’ll probably looks into adding it at some point 🔍

Encoding HLS (the hard bit 🌶️)

Being able to create these optimised HLS streams requires some horsepower. There are various ways about this… Some more attractive than others depending on how often you need post videos and (more importantly) how lazy or impatient you are.

On your computer

In the past I’ve used a script like this to spit out HLS playlists. This works but is quite a labour intensive process… I have to open a terminal, run the script, upload files and then reference the uploaded playlist manually - I’m too lazy for that shit 🫃

In the browser

While you could do this in the browser - its SLOW.

Trying ffmpeg-wasm was something I looked into early on. The ffmpeg.wasm speed comparison shows a 0.04x slow down compared to ffmpeg on baremetal

compiling ffmpeg to web-assembly makes it super damn slow and its viable 🐌

For client-side transcoding, there’s now also Mediabunny which provides an awesome api around the WebCodecs API and canvas using hardware acceleration! Honestly, this is a freaking solid option since they added HLS support last month 🥳

Perfomance is great on my 2021 M1 Macbook
Perfomance is great on my 2021 M1 Macbook

However, there’s still a limitation here, not everyone has a nippy machine and client-side encoding of a video many times over at different resolutions is still gonna absolutely CHONK on many devices.

I’m still very very keen to add mediabunny at some point tho, since its an awesome value proposition if you’re ONLY gonna be uploading from a decent computer (and it would scope down deployment to just cloudflare workers instead of needing something to fun ffmpeg on - nice and simple!). Watch this space 🤓

In the cloud

Expenny 💸

AWS Elastic Transcoder , Cloudflare Stream , Mux and various other services offer this to varying degrees of complexity and cost - but compute costs can blow out of proportion quite quickly.

And while something like Cloudflare containers suggests this as a use case , I didnt want to blow a hole through my bank account by mistake. This comparison from cloudcompare.xyz further drives home these pricing concerns, highlighting gnarly scalable cloud compute costs for “serverless” containers compared to VPS hosting (ol’ fashioned long running virtual servers).

I do want to investigate cloudflare containers further though, would be sick to keep everything in the cf stack - allowing for much easier “one-click” deployments 😉

Something in between!

Since transcoding the videos doesn’t need as much availability and scaling as the actual consumption of videos, spinning up an encoding server on existing hardware felt like the move 🤝

With these two I have all I need to encode my vids.

  • the orchestrator: cloudflare as the control plane
  • the transcoding agent: ffmpeg on existing hardware:
    like my old Lenovo Thinkcentre, Gaming PC or literally my laptop!
  • the client: your browser that uploads the vid straight to the agent.

This is what the upload flow looks like:

The architechture for this bad boi
The architechture for this bad boi

For the client-to-agent upload bit, I use the tus protocol to handle resumable uploads:

I hit reload on my browser around 71%. When I started re-uploading the same file, the magical tus was able to let us pick up where we left off 🤝

And then I use Server sent events (SSE) to stream realtime transcode status updates from ffmpeg:

This is the SSE connection in my network tab, sending over yummy updates about everything that’s going down (which is allows me to render a fun ui to watch instead of a lame % bar)

The code

I could yap on about code specifics but I’d rather say cloning the repo and have a poke around.

But here’s a little summary of some of the main technologies I used to make this cat meow:

Core tech used

Conclusion

This was a super fun endevour and was quite the learning experience 🤓

Absolutely stoked I got to share this with ya!

Thanks for reading up to now homie 🫂
Thanks for reading up to now homie 🫂

This hand-written article was incredibly enjoyable to cook up, if you enjoyed it too - you should stick around and give more of my stuff a look ;)

Tom Radford Tom Radford Tom Radford Tom Radford Tom Radford Tom Radford