Skip to content

Design: Member Video Upload, Encoding & Delivery Pipeline

Status: DRAFT — parked until prioritized Author: gercek@raklet.org (with Claude) Created: 2026-06-13 Linear: ENG-86 Branch: docs/member-video-pipeline (doc only; implementation gets its own branches)

Problem Statement

Members can attach videos to feed posts (announcements). The upload path is already sane — the browser uploads straight to Azure Blob via a SAS token, so video bytes never transit our API servers. The encoding and serving side is where the cost and quality problems live: we store each member's raw file as-is and drop the raw blob URL into an HTML5 <video> tag for every viewer. We are, in effect, serving huge original files to everyone.

Current Implementation (grounded in code)

Stage Today File
Upload 1-hour SAS token, browser PUTs directly to Azure Blob V2OrganisationController.cs:4199, AzureStorageService.GenerateVideoUploadSas
Validation .mp4/.webm/.mov, 50 MB cap StaticVariables.cs:18
Encoding None — file stored exactly as uploaded
Storage Raw original in per-org blob container AzureStorageService
Serving Public blob URL in <video><source type="video/mp4">, no CDN post-create.html:133

Why this is bad (ranked)

  1. No CDN. Videos serve from the raw blob origin, even though a Cdn-Path config is already used for other assets. Every view pulls from origin → slow globally, and we pay blob egress on every play.
  2. No transcoding = playback roulette. An iPhone .mov is often H.265/HEVC, which does not play in Chrome/Firefox at all. The hardcoded type="video/mp4" makes mismatches worse. We are storing files a chunk of members literally cannot watch.
  3. Single giant file, no adaptive bitrate. A 50 MB clip is 50 MB whether the viewer is on fiber or 3G. No HLS/DASH ladder → no quality adaptation, full-file download before playback starts.
  4. No poster/thumbnail. Feeds can't show a still frame; browsers may prefetch video bytes just to render the first frame.

Goal

Replace "store raw original, serve raw blob" with a pipeline that: transcodes every upload to web-safe H.264, produces an adaptive HLS ladder, generates a poster thumbnail, and delivers via CDN — without standing up and operating our own encoding infrastructure.

Recommendation (summary)

Adopt a managed video APICloudflare Stream as the primary recommendation, with Mux as the close alternative. Do not build an in-house FFmpeg pipeline, and do not use a transcode-only service like Zencoder (see comparison below). The upload flow is already token-based, so the change is contained.

Today:   browser → SAS → Azure Blob → <video src=blob>          (no encode, no CDN)
Target:  browser → provider direct-upload URL → [Stream/Mux encodes + packages HLS]
                 → webhook "ready" → store playbackId on attachment
                 → <video>/hls.js → provider HLS URL (CDN, ABR, auto poster)

Options Considered

One product handles encode + adaptive HLS + thumbnail + global CDN delivery + player. Billed per-minute. Minimal infra for us to own.

B. Build on Azure (FFmpeg in a WebJob)

Note: Azure Media Services is retired (shut down mid-2024), so "native Azure" means we build it: blob upload → WebJob/Function triggered → FFmpeg transcodes to an HLS ladder → store renditions in blob → wire Azure Front Door / CDN → ship an hls.js player. We own the encoding ladders, thumbnailing, HLS packaging, retries, and CDN config forever. Only worth it if egress economics at very large scale dominate — and we have no evidence of that scale today, so this is not recommended now.

See the dedicated comparison below.

Pricing

All rates captured 2026-06-13 from vendor pricing pages. Rates change — re-verify before committing spend.

Cloudflare Stream

Two line items, flat regardless of resolution, and encoding + ingress are free:

Item Rate
Storage $5 per 1,000 minutes stored / month ($0.005/min/mo) — billed on source duration
Delivery $1 per 1,000 minutes delivered ($0.001/min watched) — bandwidth included, no separate egress
Encoding / ingress Free
Minimum A $5 plan bundles 1,000 storage + 5,000 delivery minutes; usage beyond is metered

Key property: delivery price does not change with resolution. A 4K minute delivered costs the same $0.001 as a 720p minute. For unpredictable member-generated content (members shoot whatever their phone shoots), this is a meaningful de-risking property.

Mux

Usage-based across three dimensions, priced by resolution and quality tier (rates below are the "Plus" tier, 1080p, first-tier volume; "Basic" encoding is free but lower quality):

Item 720p 1080p 1440p (2K) 2160p (4K)
Encoding (one-time per upload) $0.025/min $0.03125/min $0.05/min $0.10/min
Storage (per month) $0.0024/min $0.003/min $0.0048/min $0.0096/min
Delivery (per min watched) $0.0008/min $0.001/min $0.0016/min $0.0032/min
  • First 100,000 delivery minutes/month are free. (Big deal at low volume.)
  • "Basic" quality has free encoding but no per-title/advanced features; "Premium" tier is ~1.5× the Plus rates above.
  • Volume discounts kick in at higher tiers (next 10k/10k/25k encoding; 50k storage; 500k delivery).

Zencoder (Brightcove Zencoder) — transcode only

Item Rate
File transcoding $0.05/min pay-as-you-go, down to $0.02/min on monthly subscription (tiered 4¢/3¢/2¢)
Base subscription from ~$40/mo
Storage / CDN / delivery Not included — you supply blob storage, CDN, and player yourself

Zencoder bills only the encode step. Storage, delivery/egress, HLS packaging hosting, and the player are all still on us.

Illustrative cost model

These are assumptions, not measured production numbers — we have no instrumented data on video post volume or watch-minutes yet. Plug real numbers in once we have them. Model assumes 1080p, ~50% average watch completion (1 watch-minute per view of a 2-min clip).

Scenario A — small (early adoption): 200 videos/mo × 2 min = 400 new min/mo; ~5,000 min stored at year-1 steady state; 200 videos × ~100 views × 1 min ≈ 20,000 delivered min/mo.

Cloudflare Stream Mux (1080p Plus)
Encoding $0 400 × $0.03125 = $12.50
Storage 5,000 × $0.005 = $25 5,000 × $0.003 = $15
Delivery 20,000 × $0.001 = $20 20,000 − 100k free = $0
~Monthly ~$45 ~$28

Scenario B — moderate growth: 1,000 videos/mo × 2.5 min = 2,500 new min/mo; ~30,000 min stored at year-1 steady state; 1,000 × ~300 views × 1.25 min ≈ 375,000 delivered min/mo.

Cloudflare Stream Mux (1080p Plus)
Encoding $0 2,500 × $0.03125 = $78
Storage 30,000 × $0.005 = $150 30,000 × $0.003 = $90
Delivery 375,000 × $0.001 = $375 (375,000 − 100k) × $0.001 = $275
~Monthly ~$525 ~$443

Read of the model: - At low volume, Mux's 100k free delivery minutes makes it cheaper. - At moderate volume and 1080p, the two are within ~15% of each other. - The differentiator is resolution risk: if members upload 4K, Mux delivery is 3.2× ($0.0032/min) while Cloudflare stays flat ($0.001/min). For member-generated content of unpredictable resolution, Cloudflare's flat delivery is the safer bet on the bill.

Is Zencoder better than Cloudflare? (the explicit question)

No — for this use case Zencoder is the wrong category of tool.

  • It solves 1 of our 4 problems. Zencoder is a transcoding API only. It does not store, package-and-host HLS, run a CDN, generate the player, or handle delivery. Pick Zencoder and we still have to assemble blob storage + a CDN + thumbnailing + an HLS player ourselves — i.e. most of Option B's integration burden, which is exactly what we're trying to avoid.
  • It's not even cheaper on the one thing it does. Zencoder encoding is $0.02–0.05/min. Cloudflare encoding is free; Mux 1080p encoding is $0.03/min (and Basic is free). And with Zencoder you additionally pay separately for storage and CDN egress.
  • It's a previous-generation product. Zencoder (now Brightcove Zencoder) predates the all-in-one video-API category. In ~2012 it was the right call because there was no Stream/Mux to assemble encode+store+CDN+player into one product. Today, choosing it means re-assembling that stack by hand for no upside.

Zencoder (or AWS MediaConvert, Bitmovin, etc.) only makes sense if we deliberately want to own the pipeline and treat encoding as one swappable component — a deep-infra posture we have no reason to take for member feed videos.

Verdict: Cloudflare Stream (or Mux) over Zencoder, decisively. Zencoder would be a step backward.

Implementation Sketch (when prioritized)

The upload flow is already token-based, so the surface is contained:

  1. Endpoint swap. getVideoUploadSasgetVideoUploadUrl: return a provider direct-upload URL instead of an Azure SAS. (~1 endpoint.)
  2. Schema. Add VideoProvider, VideoAssetId/VideoPlaybackId, VideoStatus (uploading|processing|ready|errored) to AnnouncementAttachment. EF migration ships on its own branch first, released before the logic that depends on it (team rule).
  3. Webhook receiver. Provider posts asset.ready → flip VideoStatus to ready, store playbackId + thumbnail URL. Feed shows a "processing…" placeholder until then.
  4. Frontend. Replace the raw <source> with the provider HLS URL + hls.js (Safari plays HLS natively); use the auto-generated thumbnail as poster.
  5. Migration of existing videos. Backfill job: push already-uploaded raw blobs through the provider, or leave legacy posts on the old direct-blob path and only route new uploads through the provider (lower risk; recommended).
  6. Secrets/config. Provider API token in config; webhook signing secret; per-env (test/prod) accounts.

Open questions to resolve before building

  • Same-day CDN quick-win? Pointing the existing CDN at the video blob container fixes problem #1 (egress/latency) with a config + URL-builder change, no migration. It does not fix HEVC-won't-play or no-ABR. Decide whether to ship this as an interim step or go straight to the provider.
  • Cloudflare vs Mux final pick — lean Cloudflare (we're already moving eng-docs to Cloudflare Pages + Access, so a vendor/billing relationship exists; flat delivery pricing de-risks member 4K). Choose Mux if we want built-in per-video engagement analytics.
  • Access control. Some orgs gate member content. Stream/Mux both support signed playback URLs/tokens — confirm we wire signed URLs for private orgs rather than public playback IDs.
  • Real usage numbers. Instrument current video post count + sizes to replace the illustrative cost model with measured figures before committing spend.

Sources