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
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)
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 chopad.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ống | Khuyế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 + variant | Cloudflare Images | ~$10/tháng |
| Blog 100 cover + OG, R2 hoặc ảnh public | Image Resizing on Zone | ~$0.50/tháng |
| Upload ảnh UGC, avatar, nhiều kích thước | Cloudflare 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ĩnh | Image 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: Stream và Images 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, immutablecho 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.