- Helix là một nền tảng AI cho phép các tác nhân lập trình tự động chạy trên đám mây và hiển thị màn hình hoạt động của chúng cho người dùng, nên truyền màn hình từ xa ổn định là yếu tố cốt lõi
- Khi streaming dựa trên WebRTC thất bại do chặn UDP và ràng buộc tường lửa trong mạng doanh nghiệp, nhóm đã xây dựng pipeline H.264 dựa trên WebSocket, nhưng trong môi trường Wi‑Fi không ổn định thì độ trễ tăng nghiêm trọng
- Thay vì cấu trúc mã hóa/giải mã phức tạp, họ phát hiện rằng cách đơn giản là gửi định kỳ ảnh chụp màn hình JPEG qua HTTP lại ổn định và hiệu quả hơn nhiều
- Cách này dùng ít băng thông hơn, không cần khôi phục frame bị hỏng, đồng thời tự động điều chỉnh chất lượng hình ảnh và tốc độ khung hình theo chất lượng mạng
- Cuối cùng, Helix áp dụng kiến trúc hybrid chuyển sang H.264 khi kết nối tốt, và sang JPEG polling khi kết nối xấu, hoàn thiện một hệ thống streaming từ xa đơn giản nhưng thực dụng
Vấn đề streaming và các ràng buộc của Helix
- Helix là nền tảng cần chia sẻ theo thời gian thực màn hình của tác nhân AI viết code đang chạy trong sandbox trên đám mây
- Người dùng theo dõi quá trình AI viết mã giống như đang xem một desktop từ xa
- Ban đầu họ dùng WebRTC, nhưng kết nối thất bại do mạng doanh nghiệp chặn UDP
- TURN server, STUN/ICE, cổng tùy chỉnh đều bị chặn bởi chính sách tường lửa
- Vì vậy họ tự triển khai pipeline streaming H.264 dựa trên WebSocket chỉ dùng HTTPS (cổng 443)
- Mã hóa phần cứng bằng GStreamer + VA-API, giải mã trên trình duyệt bằng WebCodecs
- Đạt 60fps, 40Mbps, độ trễ dưới 100ms
Độ trễ mạng và suy giảm hiệu năng
- Trong môi trường mạng không ổn định như quán cà phê, xuất hiện vấn đề video bị đứng hình hoặc trễ hàng chục giây
- WebSocket dựa trên TCP khiến khi mất gói, các frame bị dồn tuần tự và tính thời gian thực sụp đổ
- Giảm bitrate cũng không giải quyết được độ trễ, chỉ làm giảm chất lượng hình ảnh
- Họ cũng thử cách chỉ gửi keyframe, nhưng thất bại vì giao thức Moonlight yêu cầu P-frame
Phát hiện ra cách dùng ảnh chụp màn hình JPEG
- Trong lúc debug, khi gọi endpoint
/screenshot?format=jpeg&quality=70, một hình ảnh rõ nét lập tức được tải lên
- Một ảnh JPEG 150KB hiển thị không có độ trễ
- Chỉ cần lặp lại yêu cầu HTTP để cập nhật ảnh chụp màn hình là đã có thể làm tươi màn hình mượt ở mức khoảng 5fps
- Cuối cùng họ chuyển khỏi pipeline video phức tạp sang cách yêu cầu JPEG định kỳ (fetch loop)
Ưu điểm của cách JPEG
- Các hạng mục so sánh chính với H.264
- Băng thông: H.264 cố định 40Mbps, JPEG dao động 100~500Kbps
- Quản lý trạng thái: H.264 phụ thuộc trạng thái, JPEG là các frame hoàn toàn độc lập
- Khả năng phục hồi: H.264 phải chờ keyframe, JPEG có thể phục hồi ngay ở frame tiếp theo
- Độ phức tạp: H.264 mất nhiều tháng phát triển, JPEG chỉ cần vài dòng vòng lặp
fetch()
- Mạng càng kém thì cách JPEG đơn giản lại càng ổn định và hiệu quả hơn
Cấu trúc chuyển đổi hybrid
- Helix tự động chuyển giữa hai cách dựa trên RTT (thời gian khứ hồi)
- RTT < 150ms → streaming H.264
- RTT > 150ms → JPEG polling
- Khi kết nối phục hồi, người dùng bấm để chuyển lại
- Sự kiện nhập liệu (bàn phím, chuột) vẫn tiếp tục được gửi qua WebSocket để duy trì tính tương tác
- Server dừng truyền video và chuyển sang chế độ ảnh chụp màn hình bằng thông điệp
{"set_video_enabled": false}
Vấn đề dao động chuyển đổi và cách khắc phục
- Khi ngừng truyền, lưu lượng WebSocket giảm làm độ trễ hạ xuống, dẫn tới vòng lặp vô hạn tự chuyển lại sang chế độ video
- Cách giải quyết: sau khi vào chế độ ảnh chụp màn hình thì giữ nguyên cho tới khi người dùng bấm
- UI hiển thị thông báo “Video đã tạm dừng để tiết kiệm băng thông”
Vấn đề hỗ trợ JPEG và quá trình build
- Công cụ chụp màn hình cho Wayland là grim bị tắt hỗ trợ JPEG trong gói Ubuntu mặc định
- Khi chạy
grim -t jpeg thì xuất hiện lỗi “jpeg support disabled”
- Để giải quyết, họ build grim trực tiếp từ source trong Dockerfile, kèm
libjpeg-turbo8-dev
Kiến trúc cuối cùng
- Kết nối tốt: H.264 60fps, tăng tốc phần cứng
- Kết nối xấu: JPEG polling 2~10fps, độ tin cậy hoàn toàn
- Chất lượng ảnh chụp màn hình được tự động điều chỉnh theo thời gian truyền
- Nếu vượt 500ms thì chất lượng -10%, nếu dưới 300ms thì +5%, duy trì tối thiểu 2fps
Bài học chính
- Giải pháp đơn giản có thể tốt hơn hệ thống phức tạp — 2 giờ “hack” với JPEG thực dụng hơn 3 tháng phát triển H.264
- Graceful degradation là cốt lõi của trải nghiệm người dùng
- WebSocket rất phù hợp để truyền input, nhưng không nhất thiết phải dùng để truyền video
- Gói Ubuntu có thể thiếu tính năng — khi cần thì tự build
- Phải đo đạc trước khi tối ưu — streaming phức tạp không phải lúc nào cũng là lời giải duy nhất
Công bố mã nguồn mở
- Helix được cung cấp dưới dạng mã nguồn mở, với các phần triển khai chính như sau
api/cmd/screenshot-server/main.go — server ảnh chụp màn hình
MoonlightStreamViewer.tsx — logic client thích ứng
websocket-stream.ts — điều khiển chuyển đổi video
- Helix đang được phát triển với mục tiêu xây dựng hạ tầng AI hoạt động được cả trong môi trường thực tế
1 bình luận
Ý kiến trên Hacker News
Khi mạng kém, việc JPEG co lại không phải do UDP mà do cách triển khai TCP
JPEG không giải quyết được vấn đề buffering hay kiểm soát tắc nghẽn. Nhiều khả năng nó được triển khai theo cấu trúc giảm tối đa việc truyền frame
h.264 có hiệu quả mã hóa cao hơn JPEG. Với cùng kích thước, frame IDR của h.264 có thể cho chất lượng tốt hơn
Vấn đề cốt lõi là thiếu ước lượng băng thông. Ngay cả trong môi trường TCP, vẫn có thể điều chỉnh bitrate thông qua probe băng thông ban đầu và phát hiện độ trễ truyền
Nếu có thể thì nên dùng WebRTC, còn để vượt tường lửa thì WebSocket là lựa chọn tốt
Bỏ qua vấn đề hình thức hay văn phong kiểu LLM, thì toàn bộ nội dung có rất nhiều chỗ sai
10Mbps lẽ ra là đủ cho màn hình tĩnh. Vấn đề là thiết lập mã hóa sai hoặc chất lượng encoder thấp
Cách tiếp cận “chỉ gửi keyframe” là không hiệu quả; thay vào đó chỉ cần đặt khoảng cách keyframe ngắn
Cuối cùng vấn đề nằm ở cấu trúc nhồi toàn bộ stream qua một kết nối TCP duy nhất. Đã có sẵn các giải pháp như DASH cho tình huống này
Có lẽ nên tham khảo cách VNC đã làm từ năm 1998
Nó giữ mô hình client pull, đồng thời chia framebuffer thành các tile, rồi chỉ truyền phần thay đổi
Với màn hình code tĩnh, có thể giảm băng thông rất nhiều. Nếu thêm phát hiện cuộn trang thì còn hiệu quả hơn
Tôi từng làm về mã hóa video, và 40Mbps là chất lượng cỡ Blu-ray
Với việc stream văn bản đơn giản thì quá mức. Sau khi trao đổi với Claude, kết luận là 30FPS, GOP 2 giây, trung bình khoảng 1Mbps là đủ
Ngay cả trong trường hợp xấu nhất thì 1.2Mbps cũng đủ để giữ chất lượng ổn định
Vấn đề chính của bài này là đặt băng thông tối thiểu cho h.264 quá cao
H.264 hiệu quả hơn JPEG rất nhiều. Đáng ra phải bắt đầu từ 1Mbps rồi điều chỉnh
Chỉ dùng keyframe thậm chí còn kém hiệu quả hơn
Nếu là tôi thì tôi sẽ tiếp cận hoàn toàn khác
10Mbps là quá mức, còn video code trên YouTube ở 1080p chỉ khoảng 0.6Mbps. Vẫn đủ sắc nét
Tôi nghĩ thà giảm xuống 1fps hoặc chỉnh khoảng cách keyframe còn hơn
Stream video thời gian thực vào trình duyệt đúng là một việc cực kỳ đau khổ
Nếu ảnh chụp màn hình JPEG hoạt động tốt thì cứ để vậy còn hơn
Những stack như gstreamer hay Moonlight sẽ thành địa ngục debug nếu không hiểu backpressure và truyền lỗi
Tổ hợp NVIDIA Video Codec SDK + WebSocket + MediaSource Extensions là một phương án thay thế thực tế
Nhưng nếu bài viết là do LLM tạo ra, thì có lẽ tác giả cũng không có ý định hiểu những cấu trúc bên trong này
Trước đây tôi từng dùng một chương trình chụp màn hình mỗi 5 giây, và ổ cứng nhanh chóng đầy lên
Tôi nhận ra phần lớn ảnh đều giống nhau, rồi bắt đầu nghĩ đến thuật toán chỉ lưu phần thay đổi
Cuối cùng mới nhận ra là mình đang tự phát minh lại nén video
Giải quyết bằng một dòng ffmpeg và tiết kiệm được 98% dung lượng lưu trữ
Việc stream cảnh LLM đang gõ phím ở mức 40Mbps là mức băng thông quá mức một cách bất thường
Cách duy nhất để nhận được câu trả lời hay trên HN là đăng một bài sai
Theo tôi, một bài sai nhưng thú vị chính là ví dụ hoàn hảo về sự cân bằng để kéo thảo luận đi lên