---
title: "Hệ thống video - upload, transcode, livestream HLS, WebRTC và các tradeoffs"
description: "Khóa học về thiết kế app có upload video, xem video, livestream và gọi video, xoay quanh câu hỏi: byte của video kẹt, trễ hay tốn băng thông ở chặng nào. Bạn sẽ cho app gửi tiếp phần thiếu khi upload đứt, xử lý video mà không kẹt hay phát hai bộ file, chọn ladder và độ trễ cho livestream, nối cuộc gọi qua TURN và SFU."
canonical: "https://200lab.io/courses/he-thong-video-upload-transcode-livestream-hls-webrtc-va-cac-tradeoffs"
type: "course"
course_type: "course"
learning_type: "free_form"
published: "2026-10-13T08:03:38Z"
students: 0
---

## Khóa học này mang lại gì cho bạn

Mỗi tối, người bán trên nền tảng livestream bán hàng của bạn mở phiên live, cầm điện thoại vừa quay vừa giới thiệu sản phẩm. Họ còn đăng video quay sẵn, gọi video tư vấn 1-1 cho người mua và mời khách cùng lên sóng với mình. Agent đã dựng các tính năng đó trên Postgres và object storage. Object storage là nơi lưu file tách khỏi database, và mỗi file được tải về qua HTTP.

Khi người dùng đông lên, video **2 GB** upload qua 4G đứt ở **90%** rồi phải gửi lại từ đầu, hình người xem thấy trễ **30 giây** so với lúc người bán nói, còn cuộc gọi tư vấn nối được ở văn phòng mà kẹt khi người mua dùng 4G. Agent đề xuất chuyển hết sang HLS hoặc thêm server cho từng tính năng. Nhưng chưa ai chỉ ra video kẹt, trễ hay tốn băng thông ở chặng nào, và số đo nào cho thấy điều đó.

- **Cho file video lớn đi thẳng lên bucket** — Upload là việc thiết bị gửi file lên server qua network, còn bucket là nơi chứa file của app trong object storage. Bạn cho trình duyệt hay điện thoại upload thẳng lên bucket bằng URL do backend ký, để file không còn đi qua backend. Khi upload đứt, app chỉ gửi tiếp phần bucket chưa nhận, và upload bị bỏ dở không âm thầm chiếm dung lượng của bucket.
- **Theo dõi mỗi video từ lúc upload tới lúc phát được** — Encode là bước nén hình và tiếng thành dữ liệu video gọn hơn, còn transcode là việc encode lại video gốc thành các bản mà player cần. Bạn theo dõi mỗi video qua chuỗi trạng thái, để job transcode chạy lại không làm video có hai bộ file cùng được phát, video không kẹt mãi ở “đang xử lý”, và xóa video thì mọi bản transcode cùng poster của video đó cũng bị dọn theo.
- **Chọn MP4 hay HLS cho từng loại video** — HLS là cách phát video bằng nhiều file nhỏ gọi là segment, để player tải lần lượt qua HTTP và đổi chất lượng ngay giữa lúc phát. Bạn chọn được khi nào file MP4 phát thẳng vẫn đủ và khi nào cần nhiều bản với độ phân giải, bitrate khác nhau; bạn cũng đặt keyframe cách đều nhau để player đổi bản mượt, và đo ngay tại player xem người xem có phải dừng chờ tải hay không.
- **Phát livestream mà vẫn giữ được độ trễ thấp** — Encoder encode kịp khi mỗi giây video encode xong trong không quá **1 giây**. Bạn giữ cho encoder của phiên live encode kịp, tách độ trễ theo từng chặng (segment, playlist, player) để kéo nó từ vài chục giây xuống vài giây, và đặt thời gian giữ riêng cho playlist và segment trên CDN.
- **Cho cuộc gọi 1-1 nối được khi hai thiết bị không nối thẳng được với nhau** — WebRTC là chuẩn có sẵn trong trình duyệt để gửi audio, video thẳng giữa hai thiết bị, độ trễ thường dưới **1 giây** khi network tốt. Bạn dựng đủ các thành phần giúp hai thiết bị tìm được đường tới nhau (signaling, ICE, STUN và TURN), và biết mỗi giờ gọi qua TURN tốn bao nhiêu băng thông.
- **Chọn cách chạy phòng gọi nhóm và mức độ trễ** — Ở kiểu mesh, mỗi máy gửi video thẳng tới từng người còn lại, còn SFU là server nhận luồng của mỗi người rồi chuyển tiếp cho những người cần xem. Bạn chọn giữa hai cách đó, dùng simulcast (người gửi encode cùng lúc vài bản) để người dùng 4G không nhận bản quá nặng, và chọn mức độ trễ (dưới **1 giây**, vài giây hay vài chục giây) theo việc người xem cần làm và chi phí băng thông.
- **Yêu cầu agent đo trước khi sửa** — Khi agent đưa ra phương án cho phần video, bạn yêu cầu nó đo trước số byte phải gửi lại mỗi lần upload đứt, số video đang kẹt ở “đang xử lý”, phần trăm thời gian người xem phải dừng chờ tải, phần trăm cuộc gọi đi qua TURN và số GB băng thông của mỗi giờ xem hay gọi.

Cách làm trong khóa học áp dụng được với bất kỳ nền tảng video, CDN hay thư viện player nào; trong bài chỉ có vài đoạn TypeScript, SQL, playlist và log ffmpeg ngắn để đọc, còn lệnh ffmpeg, cấu hình encoder và cấu hình TURN để agent dựng hệ thống thì nằm trong demo kit.

## Khóa học cung cấp những nền tảng nào

Cả khóa học lần theo đường đi của byte video, từ điện thoại của người quay tới màn hình của người xem. Ở từng chặng trên đường đó, bạn hỏi xem khi chặng ấy đứt, chạy chậm hay chạy hai lần thì cái gì mất, cái gì trùng, cái gì kẹt. Các nhóm bài dưới đây xếp theo bậc thang scale gồm sáu nấc kiến trúc, và app chỉ lên nấc sau khi nấc đang dùng không còn đủ.

Ở nấc đầu, app có vài trăm video ngắn, trình duyệt phát thẳng file MP4 lưu trên bucket, và trên server chỉ có Postgres cùng backend. Backend là phần code của bạn, nhận request từ trình duyệt hay app điện thoại để đọc và ghi database.

Hai nấc cuối là nhánh gọi video, bắt đầu khi người dùng cần nói chuyện hai chiều chứ không theo số video. Cuộc gọi tư vấn 1-1 có thể cần nấc 5 ngay khi app mới có vài trăm video, còn nấc 6 với SFU chỉ cần khi phòng gọi có khoảng **4 người** trở lên.

Theo tài liệu [Uploading and copying objects using multipart upload](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html) của AWS, upload chưa hoàn tất vẫn bị tính chi phí lưu trữ cho tới khi được hoàn tất hoặc hủy. Tài liệu [HLS authoring specification for Apple devices](https://developer.apple.com/documentation/http-live-streaming/hls-authoring-specification-for-apple-devices) của Apple là bộ khuyến nghị cấu hình ladder và keyframe mà nhiều team làm video dùng làm điểm xuất phát. Trang [Introduction to WebRTC protocols](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols) của MDN giải thích vai trò của STUN và TURN khi hai thiết bị gọi video cho nhau.

### 1. Upload thẳng lên bucket, chia file thành part và gửi tiếp khi đứt

Bài mở đầu nhóm này được lấy từ khóa học về các thành phần nền của hệ thống. Trong bài đó, ảnh sản phẩm của cửa hàng được chuyển ra khỏi database, sang object storage và CDN. CDN là hệ thống server đặt ở nhiều nơi, giữ bản sao của file ở gần người dùng.

Sang phần upload video, file đi thẳng từ trình duyệt lên bucket, không qua backend nữa. Backend ghi dòng `uploading` vào Postgres rồi ký presigned URL cho trình duyệt. Presigned URL là URL có chữ ký, chỉ cho ghi đúng một file vào đúng một key trong vài phút. Với file **30 giây** nặng **12 MB**, số byte upload từ thiết bị phải đi qua backend giảm từ **12 MB** xuống **0**, và backend chỉ còn nhận hai request nhỏ.

Khi trình duyệt báo upload xong, backend vẫn tự kiểm tra file trước khi cho video sang bước xử lý. Ngay lúc nhận file, bucket đã từ chối request có header `Content-Type`, tức loại file do trình duyệt tự khai báo, khác với giá trị đã ký vào URL. Nhưng header khớp chưa chứng minh nội dung đúng là video. Checksum phải khớp với giá trị trình duyệt tính trước khi gửi, và chỉ probe mới đọc ra định dạng thật của file.

Khi URL còn hiệu lực, ai có URL vẫn ghi đè được file vừa kiểm tra. Vì vậy job xử lý phải đọc đúng object đã kiểm tra, và best practice là chỉ ký URL có hạn vài phút, dù S3 cho đặt tới **7 ngày**.

Vì sao upload **2 GB** đứt ở **90%** lại phải gửi lại từ byte đầu tiên? Vì app xin một URL cho cả file. Multipart là cách chia file thành nhiều part, gửi mỗi part bằng một request riêng, rồi bucket ghép lại khi client báo hoàn tất. Với file **80 MB** chia thành **8** part **10 MB**, khi upload đứt lúc part 6 mới gửi được một nửa, client hỏi bucket những part nào đã nhận rồi chỉ gửi tiếp phần còn thiếu.

Upload dở là lượt upload chưa hoàn tất mà các part đã nằm trên bucket; các part này vẫn bị tính dung lượng dù không hiện thành file nào. Backend hủy upload khi dòng `uploading` hết hạn, ví dụ sau **24 giờ**, còn rule lifecycle của bucket là lớp dự phòng khi backend bỏ sót.

Kích thước part và hạn của dòng `uploading` đều có tradeoff: part lớn thì mỗi lần đứt mất nhiều byte hơn, còn hạn quá ngắn thì hủy cả những upload qua 4G tuy chậm nhưng vẫn đang chạy.

### 2. Job transcode: mỗi video chỉ có một bộ file được phát và không kẹt ở “đang xử lý”

Sau khi upload xong, video được xử lý ở background. Backend trả lời request ngay để người dùng không phải đứng chờ, còn phần việc nặng do worker làm sau đó; worker là process chạy tách khỏi backend. Mỗi việc worker nhận gọi là job, và job được lưu thành dòng trong bảng `jobs` của Postgres.

Lời báo “upload xong” có thể tới hai lần, và worker có thể tự chạy lại cùng một job. Recipe là bộ thiết lập encode, gồm những bản nào, độ phân giải và bitrate bao nhiêu. Ở bài đầu tiên của nhóm, constraint `UNIQUE (video_id, recipe_version)` bảo đảm mỗi cặp video và phiên bản recipe chỉ có một job.

Mỗi lần chạy ghi output vào thư mục riêng trong bucket, và chỉ con trỏ trong database quyết định bộ file nào được phát, nên lần chạy lại không ghi đè được bộ file đang được phát. Bộ file của lần chạy không được chọn thì nằm lại trong bucket cho tới khi được dọn. Video gốc hỏng thì job chuyển sang `failed` ngay, kèm lý do, còn lỗi hạ tầng như mất kết nối tới bucket thì job được chạy lại, tối đa **3** lần tính cả lần đầu.

Khi worker chết giữa chừng lúc đang chạy job, video nằm im ở “đang xử lý” mà không thành phần nào báo lỗi. Lease là thời hạn worker được giữ job, và worker còn sống thì tự gia hạn. Job đối chiếu là việc chạy định kỳ, đối chiếu bucket với database để tìm job kẹt, upload dở và object mồ côi.

Với lease **10 phút** và job đối chiếu chạy mỗi **5 phút**, job của worker đã chết quay lại hàng đợi chậm nhất sau khoảng **15 phút**. Còn với file trong bucket, job đối chiếu chỉ coi file là object mồ côi khi không dòng nào của bảng `videos` trỏ tới, lần chạy ghi ra file đã kết thúc, và file không thuộc version cũ mà app còn giữ.

Bài thứ ba mở bằng ticket “đã xóa video mà khách vẫn mở được link bản 720p và ảnh poster”. Bộ biến thể là mọi file sinh ra từ cùng một video theo cùng một recipe, gồm các bản transcode và poster. Con trỏ `current_version` chỉ đổi khi version mới đã đủ mọi file, nên người xem luôn thấy trọn một version.

Version cũ được giữ thêm **7 ngày** kể từ lúc thôi được phục vụ, cho người đang xem dở. Bài cũng nói rõ link đã cấp và bản sao trên CDN không biến mất cùng lúc với file trong bucket.

Ở bài tình huống cuối nhóm, bạn chạy thử upload qua network 4G giả lập chập chờn và tắt worker giữa chừng. Sau lần chạy thử, **31%** lượt upload không bao giờ xong, **12** video có hai bộ file được phát, còn **9** video vẫn kẹt sau khi worker bật lại. Agent đề xuất retry cả file và đặt URL hết hạn sau **7 ngày**, nhưng với file **80 MB** đứt ở **90%**, cách đó vẫn gửi lại **72 MB** mỗi lần đứt.

Thứ tự sửa trong bài bắt đầu từ chỗ gây hại nhiều nhất:

- multipart có gửi tiếp;
- lease cùng job đối chiếu;
- một job cho mỗi cặp video và phiên bản recipe;
- hạn của dòng `uploading` kèm rule lifecycle.

Sau khi sửa, với các part được gửi lần lượt, mỗi lần upload đứt chỉ phải gửi lại không quá một part, còn số video kẹt và số video có hai bộ file được phát về **0**. Tradeoff là code upload trên điện thoại phức tạp hơn và backend có thêm việc phải vận hành. Tỉ lệ upload không xong cũng chỉ giảm về gần **0**, không bảo đảm về đúng **0**.

### 3. Phát video có sẵn qua HLS: ladder, keyframe và player tự đổi bản

Clip **15 giây** nặng **9 MB** phát ngay trên wifi, nhưng người xem trên 4G phải chờ khoảng **8 giây** mới thấy frame đầu tiên. Frame là từng hình tĩnh trong chuỗi hình tạo nên video. Player phải đọc phần mô tả của file để biết frame nào nằm ở đâu, mà phần này lại nằm ở cuối file. Backend thì chỉ biết trả cả file từ byte đầu, nên player phải tải gần trọn file mới hiện được frame đầu tiên.

Faststart là cách đưa phần mô tả lên đầu file, còn Range request là request xin đúng một đoạn byte của file. Clip ngắn chỉ cần một chất lượng thì giữ MP4 có faststart, gửi qua bucket hay CDN có hỗ trợ Range request. HLS dành cho video dài cần đổi chất lượng theo network, hoặc cho app có livestream.

Bitrate là lượng dữ liệu video dùng cho mỗi giây phát, còn ladder là danh sách các bản của cùng một video, mỗi bản có độ phân giải và bitrate riêng. Ladder ba bản 720p **3.000 kbps**, 480p **1.200 kbps** và 360p **600 kbps** chiếm khoảng **2,2 GB** cho mỗi giờ video, còn ladder chín bản mà Apple gợi ý chiếm khoảng **11,5 GB**. Bitrate của mỗi bản phải có trần, kể cả khi encode theo mức chất lượng cố định.

Keyframe là frame đầy đủ, decode được mà không cần frame trước nó. Khi worker encode từng bản riêng, segment của bản 720p và 480p bị cắt ở hai chỗ khác nhau, nên mỗi lần player đổi bản thì hình giật hoặc đứng một lúc.

Encoder đặt keyframe cố định mỗi **2 giây** ở mọi bản và không chèn thêm khi chuyển cảnh, rồi bước đóng gói cắt segment **6 giây** đúng tại các keyframe đó. Nhờ vậy ranh giới segment của mọi bản trùng nhau. Keyframe đặt dày hơn thì player bắt đầu phát nhanh hơn, nhưng video tốn nhiều bitrate hơn.

Trong bài cuối của nhóm này, trải nghiệm của người xem được đo ngay tại player. Buffer là phần video player đã tải về trước, chờ tới lượt phát. Bạn so sánh quy tắc chọn bản theo băng thông với quy tắc chọn bản theo buffer.

Với người xem trên 4G ở **1 Mbps**, bắt đầu từ bản cao thì phải chờ **5 giây** mới có **2 giây** video đầu tiên, còn bắt đầu từ bản thấp thì chỉ chờ **1,6 giây**. Beacon là request nhỏ mà player gửi về backend kèm số đo, để bạn biết người xem phải chờ bao lâu.

Trước khi khóa học chuyển sang livestream, bài này đi kèm demo kit đầu tiên của chương để bạn đo lại những gì bốn bài về video có sẵn đã trình bày:

- faststart;
- Range request;
- trần bitrate;
- thời điểm đặt keyframe;
- hai quy tắc chọn bản của player.

### 4. Livestream: encoder encode kịp, độ trễ từng chặng và CDN cho playlist live

Livestream là video được phát tới người xem ngay trong lúc người bán đang quay. Server ingest nhận luồng hình từ phần mềm phát của người bán qua RTMP, SRT hay WHIP. Transcoder dùng encoder để encode luồng đó thành các bản của ladder ngay trong lúc phát.

Để encoder encode kịp, transcoder chỉ encode những bản người xem thật sự cần, với preset nhanh hơn. Preset là mức thiết lập có sẵn của encoder, chọn giữa encode nhanh và nén tốt. Khi encode thử clip có sẵn với cùng cấu hình và cho encoder đọc file nhanh hết mức, không theo nhịp phát live, encoder còn dư khoảng **20-30%** sức encode (`speed` khoảng **1,2-1,3**).

Trong phiên live, log chỉ ghi `speed` quanh 1, vì video tới đúng nhịp người bán quay và encoder không encode trước được phần chưa tới. Encoder vẫn encode kịp, nên hình người xem thấy không trễ thêm theo thời gian.

Bài tiếp theo bắt đầu từ phép thử giơ đồng hồ trước camera: hình trên màn hình người xem lệch **20 giây** dù network rất nhanh. Live edge là đoạn mới nhất của phiên live, và player HLS cố ý đứng lùi lại **3** segment sau live edge. Với segment **6 giây**, riêng phần đứng lùi đó đã chiếm **18** trên **20 giây**.

Rút độ dài segment xuống **2 giây** thì độ trễ trong ví dụ còn khoảng **8 giây**. LL-HLS chia segment thành part dài **0,2-1 giây** để độ trễ còn khoảng **3-5 giây**, nhưng player phải gửi nhiều request hơn và dễ phải dừng chờ tải hơn.

Giữa nhóm có bài dùng chung từ khóa học về hệ thống chịu tải cao, lấy ví dụ trang sale của cửa hàng. Trong bài đó, mỗi loại nội dung có header `Cache-Control` riêng, và bạn chọn cách đưa nội dung mới tới khách mà server gốc không bị quá tải.

Sau bài đó, nhóm bài quay lại với phiên live. Edge là server của CDN đặt gần người xem, còn origin là server gốc giữ segment và playlist. Edge chỉ giữ playlist live trong khoảng nửa độ dài segment, nên playlist trên edge cũ đi tối đa khoảng **1 giây**, còn segment đã ghi xong thì không đổi nên được giữ lâu.

Khi edge gộp được các request trùng, với **20** edge và **3** bản, origin chỉ nhận tối đa khoảng **60** request playlist mỗi giây, dù phiên live có thêm bao nhiêu người xem. Riêng với LL-HLS, cache key của edge phải gồm cả hai tham số `_HLS_msn` và `_HLS_part` của request playlist.

Trong phiên live **1.200** người xem của bài tình huống cuối nhóm, người xem nghe câu hỏi của người bán trễ khoảng **25 giây**, còn người xem trên 4G cứ phải dừng chờ tải. Trước khi sửa, bạn tách **25 giây** đó theo từng chặng, vì agent hay đề xuất ladder bốn bản encode bằng preset chất lượng cao, trong khi encoder có thể không encode kịp ladder đó.

Bạn sửa ladder trước, rồi rút độ dài segment xuống **2 giây** và cho edge giữ playlist khoảng **1 giây**. Sau các bước đó, độ trễ xuống dưới **10 giây**, còn player trên 4G có bản vừa với băng thông để chọn.

Ở giá ví dụ **0,12 USD** mỗi GB, với cấu hình cũ, nếu mọi người xem đều tải được bản 1080p thì chi phí CDN cho phiên live **1 giờ** có thể lên tới khoảng **389 USD**. Sau khi sửa, với bitrate trung bình giả định **1,5 Mbps**, chi phí đó còn khoảng **97 USD**.

Muốn có bản phát lại ngay từ các segment đã phát, app cần playlist liệt kê mọi segment từ đầu phiên, trong khi playlist live chỉ giữ vài segment mới nhất. Cách được chọn trong bài là cho transcoder ghi thêm playlist dạng event song song với playlist live; lưu lại lịch sử playlist cũng là hướng làm được.

### 5. Gọi video 1-1: signaling, ICE, TURN và trạng thái của cuộc gọi

Hai tab trình duyệt vào cùng phòng gọi nhưng tab nào cũng đứng ở “đang chờ”. Peer là thiết bị hay tab trình duyệt tham gia cuộc gọi, còn offer là lời mời kết nối mà một peer gửi cho peer kia. Mỗi tab đã tạo offer rồi để nó nằm yên trong tab.

Signaling là kênh do app tự làm để hai peer gửi cho nhau thông tin trước khi nối, và WebRTC không quy định kênh này. Signaling server chuyển offer, answer và địa chỉ network của từng tab sang tab còn lại, nhờ vậy mỗi peer nhận được SDP của peer kia.

SDP là đoạn văn bản mô tả codec, độ phân giải, chiều gửi và nhận media, cùng các địa chỉ network mà peer có thể dùng để nối tới; mỗi địa chỉ như vậy gọi là candidate. Khi đã nối, mỗi frame của cuộc gọi WebRTC được gửi đi ngay khi encode xong nên độ trễ thường thấp, nhưng WebRTC không dùng được cache của CDN như HLS.

Phần khó hơn là tìm đường nối giữa hai peer. NAT là cách thiết bị trong network nội bộ ra internet qua địa chỉ chung, nên thiết bị bên ngoài không gọi thẳng vào được nếu chưa có đường mở sẵn. ICE là thủ tục thử từng cặp địa chỉ để tìm đường nối hai peer.

STUN cho thiết bị biết địa chỉ công khai của nó, còn TURN server nhận rồi chuyển tiếp toàn bộ media khi hai peer không nối thẳng được. ICE vẫn ưu tiên thử các cặp nối thẳng. Relay là đường đi vòng qua TURN server, và media thường chỉ đi qua relay khi không còn cặp nào nối thẳng được.

Với người dùng thật, TURN cần credential ngắn hạn để người ngoài không dùng nhờ, và thường cần thêm TURN qua TLS port **443** cho wifi công ty chặn UDP. Cuộc gọi **1 giờ**, mỗi chiều **1,5 Mbps**, làm TURN server gửi ra khoảng **1,35 GB**. Tỉ lệ cuộc gọi đi qua relay thì không có số chung, nên bạn đếm trên chính app của mình, dựa vào loại cặp candidate mà `getStats()` báo.

Tình huống cuối nhóm bắt đầu từ thống kê cuộc gọi tư vấn trong tuần. Có **38** trong **270** cuộc kẹt ở “đang kết nối”, còn **21** cuộc khác rớt khi người bán cầm điện thoại đi ra kho và máy chuyển từ wifi sang 4G. App đang lấy thẳng trạng thái mà trình duyệt báo làm nhãn, nên hình đã đứng mà màn hình vẫn hiện “đã kết nối”.

Agent đề xuất cho app tự gọi lại từ đầu mỗi khi cuộc gọi rớt. Thay vào đó, bạn dựng state machine cho cuộc gọi, tức danh sách trạng thái cùng điều kiện chuyển giữa chúng, rồi suy ra cuộc gọi đang ở trạng thái nào từ trạng thái ICE và việc app còn nhận được frame hay không. Khi điện thoại đổi network, app chuyển cuộc gọi sang “đang nối lại” rồi chạy ICE restart qua signaling.

ICE restart là chạy lại thủ tục tìm đường mà không gọi lại từ đầu. Sau khi sửa, hai thiết bị không nối thẳng được với nhau vẫn gọi được nhờ relay, và gần như cuộc gọi nào đổi network cũng giữ được kết nối, hình chỉ mất trong vài giây.

### 6. Gọi video nhóm qua SFU và chọn mức độ trễ theo chi phí

Trong phòng gọi **5 người** chạy theo kiểu mesh, laptop của bạn gửi đi **4** luồng video, tổng cộng khoảng **6 Mbps**, chỉ để bốn người kia thấy bạn. Vì máy nào cũng phải tự gửi video tới từng người còn lại, điện thoại 4G là máy đứng hình đầu tiên.

Qua SFU, mỗi người chỉ gửi một luồng lên server, và SFU chuyển tiếp luồng đó tới những người cần xem mà không phải decode. MCU decode mọi luồng, ghép thành một hình rồi encode lại, nên tốn CPU và hiếm khi được chọn. Dùng SFU thì team phải vận hành thêm server và trả thêm chi phí băng thông gửi ra.

Có buổi họp đã chạy qua SFU mà điện thoại 4G của chủ shop vẫn đứng hình. Simulcast là cách trình duyệt của người gửi encode cùng lúc vài bản cho video của mình, mỗi bản gọi là layer. SFU chọn cho từng người nhận layer vừa với khung video và băng thông của người đó.

Người nhận được lợi, còn người gửi upload nhiều hơn và tốn thêm CPU. Khi nhiều người vào phòng cùng lúc, SFU phải xin keyframe từ trình duyệt của người gửi, và bitrate của người gửi có thể tăng **2-3 lần** trong chốc lát.

Bài về cách chọn mức độ trễ lấy ví dụ phiên live **2.000** người xem, mỗi người **3 Mbps**. Dù chọn tầng độ trễ nào (HLS, LL-HLS hay WebRTC), mỗi giờ vẫn có khoảng **2,7 TB** phải gửi tới màn hình người xem. Agent đề xuất cho mọi người xem vào SFU qua WebRTC.

Với HLS hay LL-HLS, CDN gửi phần đó thay server của bạn, tốn khoảng **324 USD** ở giá ví dụ. Với SFU, server của bạn phải tự gửi ra **6 Gbps**, và bạn trả theo giá egress của cloud. Egress là băng thông từ server hay cloud đi ra internet, tính tiền theo GB. Benchmark công khai của SFU cũng không đủ để suy ra số máy cần chạy.

Từ đó bạn chọn tầng độ trễ rẻ nhất mà vẫn đáp ứng được việc người xem cần làm. Người xem cần hỏi đáp trong vài giây thì xem qua LL-HLS, còn vài người lên sóng nói chuyện hai chiều với người bán thì vào SFU. Bài cũng chỉ ra khi nào mô hình giá khác làm thứ tự chi phí giữa các tầng đảo lại.

Bài tình huống cuối khóa diễn ra trong phiên “review chung”, khi **20** người cùng lên sóng, tính cả người bán. Agent khuyên giữ mesh và hạ mọi video xuống 360p, nhưng mỗi người vẫn phải upload **9,5 Mbps** vì số luồng phải gửi đi không đổi.

Qua SFU có simulcast, mỗi người chỉ còn upload khoảng **2,15 Mbps**. Với người nhận có network yếu, SFU chọn layer vừa với băng thông của họ. Người xem phiên live thì nhận hình đã ghép của cả phòng qua đường LL-HLS hay HLS sẵn có.

## Vì sao 200Lab tạo ra khóa học này

Người bán đứng trong kho hàng, upload video vài GB qua 4G, và app báo lỗi upload khi đã gần xong. Trước đó, bạn đã thử endpoint upload, worker gọi ffmpeg, player HLS và trang gọi video mà agent dựng trong vài giờ, nhưng chỉ với vài video ngắn trên wifi văn phòng. Lần thử ấy không cho thấy chuyện gì xảy ra khi worker chết giữa chừng, khi phiên live kéo dài cả tiếng, hay khi người mua gọi tư vấn từ network di động.

Khi lỗi đã lộ ra, đề xuất đầu tiên của agent thường là retry cả file hay chuyển hết sang HLS với ladder chín bản. Đề xuất nào nghe cũng hợp lý. Nhưng khi chưa biết chặng nào đang hỏng, cách sửa nào cũng có thể để nguyên chặng đó, trong khi số byte phải gửi lại, số server phải vận hành hay chi phí băng thông lại tăng thêm.

Những ticket sau đó đều tới tay bạn. Người bán báo upload tới **90%** thì lỗi, hỏi vì sao video đăng từ hôm qua vẫn “đang xử lý”, rồi kể khách bỏ đi khi cuộc gọi tư vấn cứ đứng ở “đang kết nối”. Người bán mất đơn, còn bạn mất cả ngày đọc log mà chưa biết nên bắt đầu từ chặng nào.

Những chuyện này không riêng gì app của bạn. Nghiên cứu của Krishnan và Sitaraman trên dữ liệu năm 2012 ghi nhận người xem bắt đầu bỏ đi khi video mất quá **2 giây** mới phát, và mỗi giây chờ thêm làm tỉ lệ bỏ đi tăng khoảng **5,8%**. Năm 2018, Discord kể rằng họ cho mọi cuộc gọi nhóm đi qua SFU, vì để các máy gửi video thẳng cho nhau quá tốn băng thông khi cuộc gọi đông người.

AWS IVS công bố chế độ độ trễ thấp, dưới **5 giây**, trong khi cách phát truyền thống qua internet có thể trễ tới **30 giây**. Các nền tảng như IVS hay YouTube có nhiều chế độ độ trễ như vậy, vì không mức nào hợp với mọi việc người xem cần làm.

Trên app của bạn, sự cố nào cũng để lại số đo. Chẳng hạn, file **80 MB** đứt ở **90%** vẫn phải gửi lại **72 MB**. Lần theo số đo đó, bạn tìm ra quyết định thiết kế nằm sau chặng upload: app xin một URL cho cả file.

Bằng cách ấy, bạn tìm ra quyết định nằm sau những chặng khác, như worker ghi output ra thư mục tên ngẫu nhiên, edge giữ playlist live như file tĩnh hay app chưa có TURN server. Cách sửa vì thế nhắm thẳng vào chặng đang hỏng, và cách nào cũng đi kèm tradeoff mà bạn cần nói rõ với team. Chẳng hạn, chuyển sang multipart thì code upload trên điện thoại sẽ phức tạp hơn.

Có khi số đo lại cho thấy app chưa cần làm gì thêm. File MP4 có faststart trên bucket vẫn đủ cho clip ngắn, livestream trễ hai ba chục giây vẫn ổn khi người xem chỉ ngồi xem mà không cần hỏi đáp với người bán, và cuộc gọi 1-1 chưa cần SFU. Nhờ vậy team chưa phải vận hành transcoder live, LL-HLS hay cụm SFU, và bạn dành công sức cho đúng chặng đang có triệu chứng.

## Những vấn đề thường gặp khi app có tính năng video

Chỗ người dùng thấy lỗi video thường không phải chặng gây ra lỗi. Người mua thấy API chậm trong lúc người bán upload, người xem thấy hình đứng dù log của CDN vẫn báo ổn. Nếu chỉ nới timeout của reverse proxy hay nâng cấu hình server ngay chỗ lỗi hiện ra, chặng gây lỗi vẫn còn nguyên. Trong mỗi gạch dưới đây, vế đầu là điều bạn thấy được, vế sau dấu gạch ngang là chặng thật sự gây ra nó.

- Mỗi khi vài người bán cùng upload video, p99 thời gian trả lời của mọi API lên **9 giây**, và file nào nặng hơn khoảng **150 MB** cũng gặp lỗi 504 — file đi qua backend, nên mỗi lượt upload giữ một chỗ xử lý của backend tới khi reverse proxy cắt request ở giây thứ **60**, và request của người mua mở trang sản phẩm phải xếp hàng chờ.
- Thư mục output trong bucket nặng **1,9 TB** trong khi các video đang có chỉ cần khoảng **1,1 TB**, có video có tới **ba** thư mục output cùng nội dung — lời báo upload xong tới hai lần và worker tự chạy lại job, mỗi lần chạy ghi ra thư mục tên ngẫu nhiên, mà database không có constraint nào chặn job thứ hai.
- Hàng đợi job đã trống mà **140** video vẫn nằm ở “đang xử lý” quá **6 tiếng** — worker chết giữa chừng khi đang chạy job, và không thành phần nào kiểm tra lại job sau khi worker đã nhận nó.
- Log của CDN cho thấy **99,9%** request lấy segment trả về 200 và p99 thời gian trả segment dưới **300 ms**, vậy mà ticket “video cứ đứng hình” vẫn tới đều — log của CDN chỉ ghi nhận tới lúc bắt đầu gửi byte đầu tiên, còn buffer của player cạn dần trên 4G thì chỉ player mới thấy.
- Tới phút **20** của phiên live, hình người xem thấy đã trễ thêm khoảng **2 phút**, và tới phút **50** thì luồng đứt hẳn — log ghi `speed=0.9x` suốt buổi, nghĩa là mỗi giây encoder chỉ encode được **0,9 giây** video, nên phần video chờ encode cứ dồn lên theo thời gian.
- Bật CDN cho livestream thì player đứng hình khoảng một phút rồi nhảy cóc tới đoạn mới hơn — edge giữ playlist live **60 giây** theo rule chép từ trang sale, trong khi playlist ở origin đổi mỗi **2 giây**.
- Gọi thử **40** cuộc qua 4G thì **9** cuộc kẹt ở “đang kết nối” rồi tự hủy, dù qua wifi cuộc nào cũng nối được — app mới có STUN mà chưa có TURN server, nên khi hai thiết bị không nối thẳng được với nhau, cuộc gọi không còn đường nào khác để gửi media.
- Phòng họp **12 người** đã chạy qua SFU mà điện thoại 4G vẫn nhận khoảng **16 Mbps**, phần lớn cho những khung video nhỏ ở góc màn hình — trình duyệt của mỗi người chỉ gửi lên SFU một bản 720p, nên SFU không có bản nhẹ hơn để gửi tới điện thoại.

Với từng triệu chứng, khóa học có bài đi từ số đo tới đúng chặng gây lỗi, kèm câu hỏi bạn nên đặt cho agent trước khi sửa.

## Bạn sẽ học theo cách nào

- **Bắt đầu từ tín hiệu thật** — Bài nào cũng bắt đầu từ thứ bạn tự đo hay đọc được, chẳng hạn ticket của người bán, báo cáo dung lượng bucket, dòng `speed=` trong log của ffmpeg, log của CDN, số đo player gửi về, lần gọi thử giữa hai thiết bị hay đề xuất của agent.
- **Ví dụ thu nhỏ trước, nghiệp vụ sau** — Cơ chế được dựng trước trên ví dụ nhỏ, như file video ngắn gửi từ trình duyệt lên bucket hay hai tab trình duyệt nối qua signaling server, rồi mới áp dụng vào nền tảng livestream bán hàng. Byte của video đi qua thành phần nào thì bạn thấy qua bảng theo mốc thời gian, phép tính có đơn vị và sơ đồ ngắn gọn.
- **Best practice trong từng bài cơ chế** — Mỗi bài cơ chế có ít nhất một cách làm mà các team làm video đang dùng, như ladder có trần bitrate, keyframe đặt cố định, thời gian giữ riêng cho playlist live hay credential TURN ngắn hạn, kèm nguồn, tradeoff và cho biết app ở mức scale nào thì chưa cần tới cách làm ấy.
- **Quiz yêu cầu bạn dự đoán** — Ở nhiều bài cơ chế, quiz ngắn hỏi bạn trước khi đưa ra đáp án, chẳng hạn presigned URL đã ký header `Content-Type` là `video/mp4` mà trình duyệt gửi `text/plain` thì bucket trả gì, hay hai tab cùng tạo offer mà không tab nào gửi đi thì kết nối đứng ở trạng thái nào.
- **Demo kit chạy bằng Docker** — Demo kit viết bằng Markdown đi kèm bốn bài tình huống và bài cuối của phần video có sẵn; coding agent của bạn đọc kit, dựng hệ thống ngay trên máy và chạy cùng kịch bản cả trước lẫn sau khi sửa để bạn so sánh số đo. Kit có bước kiểm tra máy trước khi chạy và chạy được bằng CPU, GPU chỉ là tùy chọn, còn chạy kit hay không là tùy bạn.

## Khóa học dành cho ai

1. **Junior developer hoặc vibe coder** — Bạn đang cùng AI agent xây app có upload video, livestream hay gọi video trên Postgres và object storage, và đã thấy upload đứt khi gần xong, video nằm mãi ở “đang xử lý”, livestream trễ vài chục giây hay cuộc gọi chạy tốt ở localhost mà người dùng 4G không nối được. Bạn cần biết lỗi nằm ở chặng nào và nên yêu cầu agent đo gì trước khi sửa.
2. **Developer phải duyệt đề xuất kiến trúc video** — Agent hoặc team muốn chuyển hết sang HLS, dựng transcoder live, thêm TURN server hay đưa mọi người xem vào SFU, và bạn cần số đo trên app để biết có nên đồng ý hay không.
3. **Developer đang cân nhắc dùng nền tảng video có sẵn hay tự xây** — Bạn đang xem các nền tảng như Cloudflare Stream hay Mux, vốn đã làm sẵn upload, transcode và phát qua HLS, và cần hiểu rõ nền tảng đang làm thay mình những việc gì để chọn giữa hai hướng, rồi kiểm tra được hướng đã chọn.

## Những điều cần lưu ý

- **Kiến thức nền cần có** — Bạn học được khóa này nếu đã từng có app ghi dữ liệu vào Postgres, gọi API bằng `fetch` từ trình duyệt (code do agent viết cũng được), và đọc được TypeScript, SQL cùng dòng báo lỗi trong log, kể cả log của ffmpeg. Encoder, SFU hay TURN server thì bạn không cần từng tự viết, và cũng không cần biết vận hành Kubernetes.
- **Nên học thêm các khóa System Design liên quan** — Hai bài dùng chung trong khóa học này lấy từ khóa “Thiết kế hệ thống — ước lượng tải, load balancer, cache, queue, replication và partitioning” và khóa “Thiết kế hệ thống chịu tải cao — rate limiting, load shedding và circuit breaker”, mỗi bài được xếp ngay trước bài dùng tới nó. Bạn chưa cần học trước khóa “Xử lý timeout, retry và crash — idempotency, outbox, lease và reconciliation” hay khóa “Thiết kế ứng dụng realtime và offline-first — WebSocket, SSE, đồng bộ dữ liệu và CRDT”, vì lease, job đối chiếu và signaling qua WebSocket được giải thích đủ ngay trong những bài cần tới chúng. Hai khóa đó giúp bạn nắm thêm phần nền chung như idempotency key, reconciliation hay connection WebSocket, và cách làm bình luận, thả tim trong phiên live.
- **Không hướng dẫn cấu hình một nền tảng video cụ thể** — Khóa học không gắn với nền tảng video, CDN hay SFU cụ thể nào, và không đi vào thuật toán nén bên trong codec; trong demo kit, ffmpeg, LiveKit và coturn chạy từ Docker image có sẵn. Bù lại, khi hiểu cơ chế, bạn nhận ra những quyết định mà agent, nền tảng và thư viện đang âm thầm đưa ra thay bạn.
- **Số liệu trong ví dụ do bài tự đặt** — Số liệu trong các tình huống minh họa, từ số người xem, bitrate tới đơn giá CDN hay TURN, chỉ là số ví dụ đặt ra cho các phép tính khớp với nhau, không phải số đo lấy từ production của công ty nào. Trên app của bạn, hãy nhờ agent đo số byte phải gửi lại sau mỗi lần upload đứt hay độ trễ từ camera tới màn hình người xem, rồi thay vào chính các phép tính ấy.

## Bắt đầu từ đâu

Bài mở đầu đặt bốn tình huống của nền tảng livestream bán hàng cạnh đề xuất của agent là chuyển hết sang HLS. Mỗi tình huống bắt nguồn từ chặng riêng của nó trên đường đi của video, còn HLS chỉ thay đổi một chặng. Bài được đặt ở đầu vì ba câu hỏi nó đưa ra dùng được cho mọi lần thay đổi kiến trúc video về sau, bắt đầu bằng câu: upload đứt ở **90%** thì phải gửi lại bao nhiêu byte?

## Chương trình

- Upload đứt ở 90%, livestream trễ 30 giây: app video lỗi ở đâu? (miễn phí)
- Sáu nấc của app video: khi nào mới cần HLS, khi nào cần WebRTC? (miễn phí)

### Nhận video từ người dùng rồi xử lý ở background

- Đưa file và ảnh ra khỏi database bằng object storage và CDN · quiz
- Mỗi lượt upload video qua API làm mọi request khác chậm theo (miễn phí) · quiz
- Vì sao upload đứt ở 90% mà phải gửi lại cả 2 GB? · quiz
- Retry khiến cùng một video có ba bộ file trong bucket · quiz
- Video kẹt ở “đang xử lý” từ hôm qua dù worker vẫn chạy · quiz
- Đã xóa video mà link bản 720p vẫn mở được · quiz
- Tình huống: 31% lượt upload video qua 4G không bao giờ xong · kit cho agent

### Phát video qua HLS: từ video có sẵn tới livestream

- Clip 15 giây phải tải gần hết file mới bắt đầu phát
- Video chỉ có bản 1080p nên người xem trên 4G phải dừng chờ tải · quiz
- Vì sao đổi từ 720p xuống 480p mà hình lại giật? · quiz
- Server và CDN đều trả 200 nhưng video vẫn dừng chờ tải · quiz · kit cho agent
- Livestream càng phát lâu càng trễ, tới phút 50 thì đứt hẳn · quiz
- Vì sao livestream HLS trễ 20 giây dù network rất nhanh? · quiz
- Cập nhật nội dung trên CDN mà không làm origin quá tải
- Bật CDN cho livestream thì player đứng hình cả phút · quiz
- Tình huống: livestream trễ 25 giây và người xem 4G phải chờ tải · kit cho agent

### Gọi video 1-1 khi hai thiết bị không nối thẳng được với nhau

- Hai người vào cùng phòng gọi nhưng không ai thấy video của ai · quiz
- Cuộc gọi kẹt ở “đang kết nối” khi có người gọi qua 4G · quiz
- Tình huống: cuộc gọi 1-1 không nối được hoặc rớt khi đổi network · kit cho agent

### Gọi video nhóm và chọn mức độ trễ theo chi phí

- Phòng gọi 5 người, máy nào cũng phải gửi đi 4 luồng video · quiz
- Điện thoại 4G vẫn nhận bản 720p cho cả những khung video nhỏ · quiz
- Độ trễ dưới 1 giây hay 5 giây là đủ, và mỗi mức tốn bao nhiêu?
- Tình huống: 20 người cùng lên sóng nên chạy mesh hay SFU? · kit cho agent
