🎬 E-commerce / Media / SaaS Marketing

Cloudflare Stream — Marketing Team Ships Video Without Engineering

Marketing uploads a product demo, promo clip, or trailer. Stream encodes it automatically at every quality level, delivers via adaptive bitrate from 330+ cities, and generates an embeddable player with captions — no engineering ticket required. $1 per 1,000 minutes delivered, $5 per 1,000 minutes stored, $0 egress.

The Problem

"Your marketing team has product demos, launch trailers, and promo clips ready — but they wait weeks for engineering to set up encoding pipelines, configure CloudFront, and build a player. Every video update needs another ticket. Momentum dies."

The Outcome

Zero

engineering tickets to publish a video. Marketing uploads, Stream handles encode, adaptive bitrate, captions, and player automatically. Update a product video in minutes, not sprints.

Live demo below

Marketing uploads → Stream handles everything else

Product demo · Promo clip · Trailer · How-to video — live in minutes

1

👩‍💼 Marketing

Uploads video

Dashboard drag-and-drop or CMS webhook. No engineering ticket.

2

⚙️ Stream

Auto-encodes

360p · 480p · 720p · 1080p · 4K — all generated automatically in ~5 min.

3

🌍 CDN

Delivers globally

Adaptive bitrate from 330+ Cloudflare cities. Viewer gets best quality for their connection. $0 egress.

4

▶️ Player

Embeds anywhere

One <iframe> — product page, landing page, email, CMS. Quality selector + captions built-in.

Live — real Cloudflare Stream videos · one <iframe> tag each

uid: 99c0c73f…

↑ Quality selector · Adaptive bitrate · Captions — all built-in. Zero engineering.

Everything built-in — no plugins, no config

🔄
Adaptive bitrate Auto-switches 360p→1080p based on viewer bandwidth
📝
Captions & subtitles Upload SRT/VTT or auto-generate via Workers AI
🖼️
Custom thumbnail Set poster image — product shot, promo frame
🔒
Access control Signed URLs — gate to subscribers or embed domains
📊
Analytics Views, watch time, engagement — no extra service
Stream Player SDK React / JS SDK if you need a custom branded player

Before vs After — what marketing's video workflow looks like

Before — marketing files a ticket

👩‍💼 Marketing

Has new product video ready to launch

🎫 Ticket

Files request to engineering: "Please add this video to the site"

Sprint queue: 2–4 weeks

👨‍💻 Engineering

Sets up S3 bucket + IAM + MediaConvert job + CloudFront + custom player

3 services, complex config

💸 AWS Bill

S3 storage + MediaConvert encoding + CloudFront egress on every view

~$1,500/mo per 1M min

📝 Change request

"Can you add Thai captions and a new thumbnail?"

Another ticket, another sprint

Marketing velocity blocked by engineering queue. Every update = a ticket.

After — marketing ships independently

👩‍💼 Marketing

Uploads product video via Stream dashboard or CMS integration

Day 1, 10 minutes

⚙️ Stream

Auto-encodes 360p / 720p / 1080p. Webhook fires when ready.

~5 min

📋 Copy

Marketing copies the <iframe> embed code — pastes into product page, landing page, or CMS

30 seconds

▶️ Live

Player live globally — adaptive bitrate, captions, quality selector — all built-in

Same day

✏️ Update

New thumbnail? Add Thai captions? Done in the Stream dashboard — no engineering involved

Self-service, always

Zero engineering tickets. Marketing ships video as fast as they create it.
worker.js — upload video and get embeddable player URL
// CMS/marketing integration — connect your CMS to Cloudflare Stream.
// When marketing publishes a video in the CMS, this Worker webhook
// uploads it to Stream, gets the embed URL, and writes it back.
// Marketing team never touches engineering infrastructure.
// Bindings: STREAM_ACCOUNT_ID, STREAM_API_TOKEN (Cloudflare secrets)

export default {
  async fetch(request, env) {
    const url = new URL(request.url)

    // ── POST /api/video/upload ─────────────────────────────────────────
    // Client sends a video file — Worker proxies to Stream API.
    // Stream auto-encodes at 360p / 720p / 1080p / 4K.
    if (url.pathname === '/api/video/upload' && request.method === 'POST') {
      const formData = await request.formData()
      const file     = formData.get('file')        // Blob from browser

      const upload = await fetch(
        `https://api.cloudflare.com/client/v4/accounts/${env.STREAM_ACCOUNT_ID}/stream`,
        {
          method:  'POST',
          headers: { Authorization: `Bearer ${env.STREAM_API_TOKEN}` },
          body:    file,
        }
      )
      const { result } = await upload.json()

      // result.uid = Stream video ID
      // result.playback.hls = HLS manifest URL (adaptive bitrate)
      // Embed: https://customer-XXX.cloudflarestream.com/{uid}/iframe
      return Response.json({
        uid:      result.uid,
        embedUrl: `https://customer-XXX.cloudflarestream.com/${result.uid}/iframe`,
        hls:      result.playback.hls,
        status:   result.status.state,  // "inprogress" → "ready"
      })
    }

    // ── GET /api/video/:uid/token ──────────────────────────────────────
    // Generate a signed token — gates video to authenticated users only.
    // Expires in 1 hour. Only viewers with this token can watch.
    if (url.pathname.startsWith('/api/video/') && url.pathname.endsWith('/token')) {
      const uid   = url.pathname.split('/')[3]
      const token = await fetch(
        `https://api.cloudflare.com/client/v4/accounts/${env.STREAM_ACCOUNT_ID}/stream/${uid}/token`,
        {
          method:  'POST',
          headers: { Authorization: `Bearer ${env.STREAM_API_TOKEN}` },
          body:    JSON.stringify({ exp: Math.floor(Date.now() / 1000) + 3600 }),
        }
      ).then(r => r.json())

      // Signed embed: /iframe?token={token}  — only works for 1 hour
      return Response.json({ token: token.result.token })
    }

    return fetch(request)
  }
}

Video types marketing manages without engineering

🛍️

Product demos

New product launch? Upload, embed on the product page. Done in minutes.

🎬

Trailers & teasers

Campaign launch clips, event teasers. Auto-encoded at every quality.

📣

Promo videos

Seasonal campaigns, sale announcements. Swap videos without touching the site.

🎓

How-to & tutorials

Product onboarding, feature walkthroughs. Captions added from the dashboard.

📺

Live product launches

RTMPS live stream — same player, same embed, zero extra infrastructure.

🔒

Gated content

Premium demos for leads. Signed URLs expire automatically — no engineering.

🌏

Localised captions

Upload Thai, Indonesian, Vietnamese SRT files directly. Instant caption switching.

📱

Mobile-first delivery

Adaptive bitrate serves 360p on 4G, 1080p on WiFi. Same embed code.

Productionising this

What changes when you ship this for real

Two costs, surface both

$1 per 1,000 minutes delivered + $5 per 1,000 minutes stored. Always quote both — finance teams care about steady storage cost more than spiky delivery cost.

Signed playback for paywalled video

Generate signed URLs server-side via the Stream API for premium content. Token TTL 5–30min. Public videos can use plain UID embeds.

Captions via Workers AI

Run uploaded audio through @cf/openai/whisper-large-v3-turbo, save VTT, attach via Stream Captions API. The /demo/subtitles flow demonstrates this end-to-end.

Marketing-friendly upload

Skip the dashboard for marketing: build a tiny Worker that exposes POST /upload-video, gates with a CMS auth token, returns the playback UID. Marketing pastes UID into the CMS.

Webhooks for "ready"

Configure Stream webhooks (video.live.connected, video.ready) → Worker → notify CMS / Slack. Marketing sees the status without polling.

Lifecycle / archival

For ad campaigns with a fixed run, set Stream's requireSignedURLs + a TTL on the asset. Stale launch videos shouldn't accumulate storage cost forever.