Stream và Images: media pipeline ở edge, khi nào dùng product nào

3 cách xử lý media của Cloudflare: Stream cho video (HLS/DASH), Images cho upload-transform-deliver, Image Resizing / cf.image. Pipeline, giá, responsive, OG động.

· 8 phút đọc · Read in English
Pipeline media ở edge của Cloudflare: Stream cho video (upload → transcode đa bitrate → HLS/DASH), Images cho lifecycle upload-transform-deliver, và Image Resizing/cf.image cho nguồn R2/S3/web, kèm OG image động

TL;DR

Cloudflare có 3 sản phẩm cho media, bổ sung nhau:

  • Stream: video đầu-cuối. Upload → transcode multi-bitrate → HLS/DASH → nhúng player. Tính phí theo phút lưu trữ + phút phân phối.
  • Images: vòng đời đầy đủ cho ảnh. Upload → cấu hình variant → phân phối qua imagedelivery.net. Đăng ký $5/100k lưu + $1/100k phân phối.
  • Image Resizing on Zone hoặc Worker cf.image: biến đổi ảnh từ bất kỳ origin nào (R2, S3, web). Trả theo dùng $0.50/1k biến đổi duy nhất, cache CDN miễn phí.

Luận điểm chính:

Không phải mọi tình huống cần sản phẩm Images. Blog công nghệ với cover + ảnh OG động → R2 + Image Resizing đủ + rẻ hơn. Thương mại điện tử với 100k ảnh sản phẩm + nhiều variant → sản phẩm Images xứng với phí đăng ký. Video bất kỳ khối lượng nào → Stream là lựa chọn hợp lý duy nhất (phương án tự host FFmpeg là cái hố không đáy).

Bài này đi qua: 2 pipeline (Stream, Images) chi tiết, 3 cách biến đổi ảnh với đánh đổi, pattern responsive + lazy, ảnh OG động thực tế, URL ký trước, và khi nào mỗi sản phẩm đúng.

Bài này đóng Block 4 (AI + Media). Block 5 (Production) bắt đầu Part 17 với Observability.


Dành cho ai

  • Dev xây ứng dụng có upload / phục vụ ảnh, video.
  • Chủ blog muốn tối ưu cover, ảnh OG tự động.
  • Team thương mại điện tử cần ảnh sản phẩm responsive nhiều kích thước.
  • Ai đang tự host FFmpeg muốn thoát.

Nên đọc trước: Part 7 (R2), Part 13 (Workers AI cho Flux).

Sau bài này bạn sẽ:

  • Upload + phục vụ video qua Stream trong < 30 phút.
  • Biết khi nào chọn Images vs R2 + biến đổi.
  • Triển khai ảnh responsive với srcset + format=auto.
  • Sinh ảnh OG động từ Worker.

Bài này không nói về gì

  • Đi sâu live streaming: Stream Live có nhưng khác luồng (ingest WebRTC/RTMP). Bài này tập trung VOD.
  • Video editor / hậu kỳ: DAW, NLE không phải lĩnh vực Cloudflare.
  • Sinh ảnh bằng AI: Flux/SDXL đã cover ở Part 13. Bài này tập trung biến đổi + phân phối, không sinh.

Bức tranh tổng

Pipeline media: Stream (upload tus → transcode multi-bitrate → đóng gói HLS/DASH → CDN 330+ PoP → nhúng player). Images (multipart POST hoặc pull URL → Cloudflare Images lưu → biến đổi variant → cache CDN → img client). Phương án khác: R2 + Image Resizing on Zone hoặc tuỳ chọn Worker cf.image cho kiểm soát linh hoạt.


Cloudflare Stream: video

Video là cái hố không đáy nếu tự host: transcode, lưu trữ, ABR, DRM, CDN, player. Stream đóng gói mọi thứ vào 1 gói đăng ký.

Upload

Upload trực tiếp từ creator (client-đến-Cloudflare, không qua Worker):

// Worker tạo upload URL
async function getUploadURL(request: Request, env: Env): Promise<Response> {
  const r = await fetch(
    `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/stream/direct_upload`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${env.STREAM_API_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        maxDurationSeconds: 3600,
        expiry: new Date(Date.now() + 3600_000).toISOString(),
      }),
    }
  );

  const { result } = await r.json();
  return Response.json({ uploadURL: result.uploadURL, uid: result.uid });
}

Client dùng tus (giao thức upload có thể tiếp tục):

import * as tus from "tus-js-client";

const file = document.querySelector("input[type=file]").files[0];
const upload = new tus.Upload(file, {
  endpoint: uploadURL,  // từ Worker
  chunkSize: 50 * 1024 * 1024,  // 50MB
  onProgress: (sent, total) => {
    console.log(`${(sent / total * 100).toFixed(1)}%`);
  },
  onSuccess: () => console.log("Done"),
});
upload.start();

Có thể tiếp tục: mất mạng, tiếp tục đúng chỗ. Quan trọng cho mobile / kết nối không ổn định.

Transcode

Tự động. Cloudflare transcode video đã upload thành nhiều rendition:

Đầu vào: 4K MP4 upload
Đầu ra:
  → 240p (băng thông thấp)
  → 360p
  → 480p
  → 720p
  → 1080p
  → (không transcode lên cao hơn nguồn)

ABR (Adaptive Bitrate) — player chọn rendition phù hợp băng thông của user.

Thời gian transcode: ~ thời gian thực. 10 phút video = ~10 phút transcode.

Đóng gói

HLS (hệ sinh thái Apple + browser hiện đại) + DASH (Android + phương án dự phòng). Tự sinh ra.

URL manifest:

https://customer-<code>.cloudflarestream.com/<uid>/manifest/video.m3u8
https://customer-<code>.cloudflarestream.com/<uid>/manifest/video.mpd

Nhúng

Phương án 1: iframe player của Cloudflare.

<iframe
  src="https://customer-<code>.cloudflarestream.com/<uid>/iframe"
  style="border: none; aspect-ratio: 16/9; width: 100%;"
  allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
  allowfullscreen>
</iframe>

Phương án 2: player tùy biến HLS.js / Video.js.

<video id="player" controls></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
const video = document.getElementById("player");
if (Hls.isSupported()) {
  const hls = new Hls();
  hls.loadSource("https://customer-<code>.cloudflarestream.com/<uid>/manifest/video.m3u8");
  hls.attachMedia(video);
}
</script>

URL ký trước (tùy chọn)

Bảo vệ video riêng tư:

async function getSignedURL(uid: string, env: Env): Promise<string> {
  const r = await fetch(
    `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/stream/${uid}/token`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${env.STREAM_API_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        exp: Math.floor(Date.now() / 1000) + 3600,  // 1h
      }),
    }
  );

  const { result } = await r.json();
  return `https://customer-<code>.cloudflarestream.com/${result.token}/manifest/video.m3u8`;
}

Token hết hạn sau 1h. User không chia sẻ URL vĩnh viễn được. Đủ cho nội dung trả phí, nền tảng khóa học.

Giá Stream

  • Lưu trữ: $5 mỗi 1000 phút lưu mỗi tháng.
  • Phân phối: $1 mỗi 1000 phút phân phối.

Ví dụ: 100 video, trung bình 10 phút = 1000 phút lưu = $5/tháng. 10k view/tháng, trung bình xem 5 phút = 50k phút phân phối = $50/tháng. Tổng ~$55/tháng.

So với AWS + MediaConvert + CloudFront: tương đương hoặc rẻ hơn, nhưng công sức vận hành ít hơn nhiều.


Cloudflare Images: vòng đời ảnh

Images là sản phẩm dành cho ứng dụng upload + phục vụ nhiều ảnh (UGC, thương mại điện tử, nhiếp ảnh).

Upload

3 cách:

A. Upload trực tiếp (client → CF):

// Worker tạo upload URL
async function getImageUploadURL(env: Env) {
  const r = await fetch(
    `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/images/v2/direct_upload`,
    {
      method: "POST",
      headers: { Authorization: `Bearer ${env.IMAGES_API_TOKEN}` },
    }
  );
  const { result } = await r.json();
  return { uploadURL: result.uploadURL, id: result.id };
}

Client POST file:

const formData = new FormData();
formData.append("file", fileInput.files[0]);
await fetch(uploadURL, { method: "POST", body: formData });

B. Upload phía server:

const formData = new FormData();
formData.append("file", fileBlob);

await fetch(
  `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${token}` },
    body: formData,
  }
);

C. Kéo từ URL public:

await fetch(
  `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ url: "https://example.com/photo.jpg" }),
  }
);

Variant

Thay vì biến đổi mỗi request, Images dùng cấu hình “variant” định trước.

Dashboard → Images → Variants:

variant: "thumb"
  width: 200
  height: 200
  fit: cover
  format: auto

variant: "hero"
  width: 1920
  height: 1080
  fit: contain
  format: auto
  quality: 85

URL:

https://imagedelivery.net/<account-hash>/<image-id>/thumb
https://imagedelivery.net/<account-hash>/<image-id>/hero

Không cho param tùy biến trong URL (khác Image Resizing). Phải định nghĩa variant trước.

Variant linh hoạt

Bật tùy chọn trong dashboard → URL cho phép param inline:

https://imagedelivery.net/<account-hash>/<image-id>/w=800,h=600,fit=cover,format=auto

Linh hoạt nhưng không có validate schema.

URL ký trước

Bảo vệ ảnh riêng tư:

const SIGNING_KEY = env.IMAGES_SIGNING_KEY;

function signImageURL(imageId: string, variant: string, expiry: number) {
  const url = `https://imagedelivery.net/<hash>/${imageId}/${variant}?exp=${expiry}`;
  const hmac = crypto.subtle.sign(
    "HMAC",
    importKey(SIGNING_KEY),
    new TextEncoder().encode(url)
  );
  return `${url}&sig=${toHex(hmac)}`;
}

Giá Images

  • Lưu trữ: $5 mỗi 100k ảnh lưu mỗi tháng.
  • Phân phối: $1 mỗi 100k ảnh phân phối.
  • Biến đổi: bao gồm (không tính phí riêng).

Đơn giản: $10/tháng cho 100k ảnh lưu + 100k view. Mở rộng tuyến tính.


Image Resizing on Zone (không cần Images)

3 phương án biến đổi: Cloudflare Images (upload + variant), Image Resizing on Zone (đường dẫn URL /cdn-cgi/image/...), Worker cf.image (bằng code). Đánh đổi: có quản lý vs trả theo dùng vs linh hoạt.

Nếu đã có ảnh ở R2, S3, hoặc URL web, không cần sản phẩm Images. Bật Image Resizing trên zone:

Dashboard → Speed → Optimization → Image Resizing → Enable

Pattern URL:

https://<zone>/cdn-cgi/image/width=800,format=auto/<original-path>

Ví dụ:

<!-- Gốc -->
https://cloudsecop.net/images/cover.jpg (2000×1500 PNG, 2MB)

<!-- Biến đổi -->
https://cloudsecop.net/cdn-cgi/image/width=800,format=auto/images/cover.jpg
(800×600 WebP, ~80KB — nhỏ hơn 25 lần)

Tùy chọn

  • width, height: đổi kích thước.
  • fit: scale-down, contain, cover, crop, pad.
  • format: auto (WebP/AVIF theo browser), webp, avif, json.
  • quality: 1-100 (mặc định 85).
  • gravity: auto, left, right, top, bottom, center.
  • background: màu cho pad.
  • blur, sharpen, brightness, contrast: bộ lọc.
  • trim: cắt viền.
  • dpr: device pixel ratio.

Yêu cầu

  • Zone phải có custom domain.
  • Cloudflare workers.dev không hỗ trợ (phải qua zone).
  • Bật trong dashboard.

Giá

  • $0.50 mỗi 1000 biến đổi duy nhất.
  • Cache hit miễn phí (quy tắc CDN thông thường).

Ví dụ: 100 bài, mỗi bài 3 kích thước (1x, 2x, 3x) × 2 format (WebP, AVIF) = 600 biến đổi duy nhất. 100k view/tháng với 90% cache hit = 10k miss = chỉ ~600 biến đổi duy nhất thực (miss lại cùng variant chỉ tính một lần). Chi phí ~$0.30/tháng.


Worker cf.image: bằng code

Cần logic tùy biến (auth, param động, watermark)? Worker fetch với cf.image:

export default {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
    const imagePath = url.pathname.slice(1);  // trim leading /
    const width = parseInt(url.searchParams.get("w") || "800");

    // Auth check
    if (url.searchParams.get("private") === "true") {
      const user = await authenticate(request);
      if (!user) return new Response("Unauthorized", { status: 401 });
    }

    // Fetch với image resize
    const imageRequest = new Request(`https://origin.com/${imagePath}`, request);
    return fetch(imageRequest, {
      cf: {
        image: {
          width,
          format: "auto",
          fit: "scale-down",
          quality: 85,
        },
      },
    });
  },
};

Mạnh nhất khi:

  • Ảnh ở origin có bảo vệ (bucket R2 private).
  • Xác thực theo từng user.
  • Watermark động.
  • Log mỗi view vào Analytics Engine.

Pattern: ảnh responsive

HTML <img srcset> cho browser chọn kích thước phù hợp:

<img
  src="https://cloudsecop.net/cdn-cgi/image/width=800,format=auto/images/cover.jpg"
  srcset="
    https://cloudsecop.net/cdn-cgi/image/width=400,format=auto/images/cover.jpg 400w,
    https://cloudsecop.net/cdn-cgi/image/width=800,format=auto/images/cover.jpg 800w,
    https://cloudsecop.net/cdn-cgi/image/width=1600,format=auto/images/cover.jpg 1600w
  "
  sizes="(max-width: 640px) 100vw, 800px"
  alt="..."
  loading="lazy"
/>

Browser tự chọn:

  • Mobile viewport 400px → tải 400w.
  • Desktop retina 1600px → tải 1600w.

loading="lazy": browser defer nạp ảnh dưới fold. Tăng hiệu năng miễn phí.

Với Astro

Astro có component <Image />, nhưng URL biến đổi phải tùy biến. Helper:

---
// src/components/CFImage.astro
interface Props {
  src: string;
  alt: string;
  widths?: number[];
  sizes?: string;
  loading?: "lazy" | "eager";
}

const {
  src,
  alt,
  widths = [400, 800, 1200, 1600],
  sizes = "(max-width: 640px) 100vw, 800px",
  loading = "lazy",
} = Astro.props;

const srcset = widths.map((w) => 
  `/cdn-cgi/image/width=${w},format=auto,quality=85${src} ${w}w`
).join(", ");

const defaultSrc = `/cdn-cgi/image/width=${widths[Math.floor(widths.length / 2)]},format=auto${src}`;
---

<img src={defaultSrc} srcset={srcset} sizes={sizes} alt={alt} loading={loading} />

Dùng:

<CFImage src="/images/cover.jpg" alt="Cover" />

Pattern: ảnh OG động

Ảnh OG (1200×630) hiện khi chia sẻ link trên Twitter, LinkedIn, Slack. File tĩnh cho 100 bài = nhàm. Sinh OG động từ Worker:

Cách 1: SVG sang PNG

// worker/og.ts
export async function generateOG(title: string, env: Env) {
  const svg = `
    <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630">
      <rect width="1200" height="630" fill="#0f172a"/>
      <text x="60" y="120" fill="#f48120" font-family="system-ui" font-size="32" font-weight="700">
        CloudSecOp
      </text>
      <text x="60" y="200" fill="#fff" font-family="system-ui" font-size="54" font-weight="700">
        ${escapeXml(title.slice(0, 60))}
      </text>
    </svg>
  `;

  // Convert SVG → PNG qua Worker cf.image
  const pngResponse = await fetch(
    `data:image/svg+xml;base64,${btoa(svg)}`,
    {
      cf: {
        image: {
          width: 1200,
          height: 630,
          format: "png",
        },
      },
    }
  );

  return pngResponse;
}

Mẹo: Worker không tự chuyển SVG → PNG. Dùng cf.image với format: "png" đẩy việc chuyển xuống hạ tầng Cloudflare.

Cách 2: Sinh bằng AI với Flux

const image = await env.AI.run("@cf/black-forest-labs/flux-1-schnell", {
  prompt: `Minimal geometric cover for: "${title}". Dark theme, orange accent, editorial.`,
  num_steps: 4,
});

return new Response(image, { headers: { "Content-Type": "image/png" } });

Đẹp hơn template SVG, nhưng chi phí cao hơn (~$0.005/ảnh). Cache mạnh vào R2:

// Check cache first
const cached = await env.R2.get(`og/${slug}.png`);
if (cached) return new Response(cached.body, { headers: { "Content-Type": "image/png" } });

// Generate
const image = await env.AI.run(...);

// Cache
await env.R2.put(`og/${slug}.png`, image);

return new Response(image, { headers: { "Content-Type": "image/png" } });

Sinh 1 lần, phục vụ mãi mãi. 100 bài = chi phí một lần ~$0.50.


So sánh 3 phương án cho ảnh

Tình huốngKhuyến nghịƯớc tính giá (quy mô nhỏ)
Thương mại điện tử 10k+ ảnh sản phẩm, vòng đời + variantCloudflare Images~$10/tháng
Blog 100 cover + OG, R2 hoặc ảnh publicImage Resizing on Zone~$0.50/tháng
Upload ảnh UGC, avatar, nhiều kích thướcCloudflare Images~$10/tháng
Ảnh cần xác thực (nội dung trả phí)Worker cf.image + R2 private~$1-2/tháng
Ảnh marketing tĩnhImage Resizing on Zone~$0.10/tháng

Quyết định lớn:

  • Cần quy trình upload? → sản phẩm Images hoặc Worker API đẩy vào R2.
  • Ảnh public có zone? → Image Resizing on Zone đủ + rẻ.
  • Logic tùy biến phức tạp? → Worker cf.image.

Gotcha

① Image Resizing không chạy trên workers.dev

*.workers.dev không phải zone → không có /cdn-cgi/image/. Phải có custom domain. Vòng lặp dev: cấu hình custom domain hoặc test trên production.

② Cache key với query param

/cdn-cgi/image/width=800/images/cover.jpg?v=1  → cache key 1
/cdn-cgi/image/width=800/images/cover.jpg?v=2  → cache key 2

Cache bust bằng query param. Nhưng đừng dùng ?t=Date.now() ngẫu nhiên → miss mỗi request.

③ Format auto phụ thuộc header Accept

format=auto trả:

  • AVIF nếu browser hỗ trợ (Chrome 85+, Safari 16+, Firefox 93+).
  • WebP nếu không có AVIF.
  • JPEG là phương án dự phòng.

Test với curl:

curl -H "Accept: image/avif,image/webp" https://... -o test.avif

④ Định dạng URL ký trước

Images: HMAC trong param ?sig=. Stream: token trong path.

Không thay đổi được cho nhau. Đọc docs kỹ trước khi code.

⑤ Deadline upload Stream

URL upload trực tiếp có expiry. User upload qua deadline = 410 Gone. Set expiry đủ dài cho kết nối chậm (1h hợp lý).

⑥ Tải MP4 từ Stream

Mặc định Stream phục vụ HLS/DASH, không cho tải MP4. Bật downloadable: true khi tạo video:

{ "requireSignedURLs": false, "allowedOrigins": [...], "downloadable": true }

URL tải MP4: https://videodelivery.net/<uid>/downloads/default.mp4

⑦ Cache variant Images

Cập nhật cấu hình variant → URL cũ phục vụ bản cache cũ đến khi TTL hết (hoặc purge). Dùng tên variant mới cho breaking change (hero-v2 thay vì chỉnh hero).

⑧ PNG trong suốt → JPEG

format=auto với PNG có alpha → nếu browser chỉ hỗ trợ JPEG, alpha mất (điền đen/trắng). Giữ PNG tường minh:

/cdn-cgi/image/width=800,format=png/...

⑨ Giá lưu trữ Stream không miễn phí

Xóa video khi không cần. Video 1GB lưu = 1 phút nếu bitrate 8Mbps → $0.005/tháng/video. 1000 video bị quên = $5/tháng chi phí ẩn.


Observability

Dashboard: StreamImages có analytics riêng từng sản phẩm:

  • Stream: phút video phân phối, băng thông, quốc gia người xem.
  • Images: số lần biến đổi, số lần phân phối, tỉ lệ cache hit.

Log qua Logpush → R2 cho dài hạn (mỗi request truy cập).

Cảnh báo:

  • Số biến đổi tăng đột biến (DDoS qua lạm dụng /cdn-cgi/image/).
  • Chi phí lưu trữ tăng đột biến (upload bị quên).
  • Băng thông tăng đột biến.

Production checklist

  • Upload Stream trực tiếp qua Worker (không lộ token ra client).
  • Upload Images qua URL upload trực tiếp (không đẩy file qua Worker — phí CPU).
  • URL ký trước cho nội dung riêng (Stream: token, Images: HMAC).
  • Ảnh responsive: srcset + sizes + loading="lazy".
  • format=auto để browser nhận AVIF/WebP.
  • Ảnh OG động được cache trong R2, không sinh mỗi request.
  • Tên variant ổn định, không chỉnh variant đã có (breaking change).
  • Custom domain bật Image Resizing (workers.dev không hỗ trợ).
  • Giám sát chi phí: lưu trữ, biến đổi, phân phối.
  • Dọn media mồ côi (video/ảnh không còn tham chiếu).
  • Header cache Cache-Control: public, max-age=31536000, immutable cho asset có version.

Kết

Stream là câu trả lời hợp lý duy nhất cho video. Sản phẩm Images phù hợp cho ứng dụng UGC/thương mại điện tử với quy trình upload. Còn lại — blog, site nội dung, marketing — Image Resizing on Zone hoặc Worker cf.image rẻ hơn và đủ linh hoạt.

Block 4 (AI + Media) đóng lại với 4 part: Workers AI, Vectorize RAG, Durable Objects, Stream + Images. Nền tảng Cloudflare giờ gần như full-stack.

Block 5 (Production) bắt đầu Part 17: Observability với Logs + Tail Workers + Analytics Engine — làm sao biết Worker chạy đúng, debug production, và cảnh báo khi lỗi.


Tham khảo