H.264 스트리밍을 JPEG 스크린샷으로 대체했더니 더 잘 작동했다

1 month ago 12

  • Helix는 클라우드 상에서 자율 코딩 에이전트가 작동하는 화면을 사용자에게 보여주는 AI 플랫폼으로, 안정적인 원격 화면 전송이 핵심임
  • 기업 네트워크의 UDP 차단과 방화벽 제약으로 WebRTC 기반 스트리밍이 실패하자, 팀은 WebSocket 기반 H.264 파이프라인을 구축했으나 불안정한 Wi-Fi 환경에서 지연이 심각하게 발생함
  • 복잡한 인코딩·디코딩 구조 대신, 단순히 JPEG 스크린샷을 HTTP로 주기적으로 전송하는 방식이 훨씬 안정적이고 효율적임을 발견
  • 이 방식은 대역폭 사용량이 적고, 손상된 프레임 복구가 필요 없으며, 네트워크 품질에 따라 자동으로 화질과 프레임 속도를 조정
  • 결과적으로 Helix는 좋은 연결에서는 H.264, 나쁜 연결에서는 JPEG 폴링으로 전환하는 하이브리드 구조를 채택해, 단순하지만 실용적인 원격 스트리밍 시스템을 완성함

Helix의 스트리밍 문제와 제약

  • Helix는 클라우드 샌드박스에서 작동하는 AI 코딩 에이전트의 화면을 실시간으로 공유해야 하는 플랫폼
    • 사용자는 마치 원격 데스크톱처럼 AI가 코드를 작성하는 과정을 시청함
  • 초기에는 WebRTC를 사용했으나, 기업 네트워크의 UDP 차단으로 연결이 실패함
    • TURN 서버, STUN/ICE, 커스텀 포트 등은 모두 방화벽 정책에 의해 차단됨
  • 이에 따라 HTTPS(443 포트)만 사용하는 WebSocket 기반 H.264 스트리밍 파이프라인을 직접 구현
    • GStreamer + VA-API로 하드웨어 인코딩, WebCodecs로 브라우저 디코딩
    • 60fps, 40Mbps, 100ms 미만의 지연을 달성

네트워크 지연과 성능 저하

  • 커피숍 등 불안정한 네트워크 환경에서 영상이 멈추거나 수십 초 지연되는 문제가 발생
    • TCP 기반 WebSocket은 패킷 손실 시 프레임이 순차적으로 지연되어 실시간성이 붕괴
  • 비트레이트를 낮춰도 지연은 해결되지 않고, 화질만 저하됨
  • 키프레임만 전송하는 방식도 시도했으나, Moonlight 프로토콜이 P-프레임을 요구해 실패

JPEG 스크린샷 방식의 발견

  • 디버깅 중 /screenshot?format=jpeg&quality=70 엔드포인트를 호출하자 즉시 선명한 이미지가 로드
    • 150KB 크기의 JPEG 한 장이 지연 없이 표시됨
  • 단순히 HTTP 요청을 반복해 스크린샷을 갱신하자 5fps 수준의 부드러운 화면 갱신이 가능
  • 결국 복잡한 비디오 파이프라인 대신, 주기적 JPEG 요청(fetch loop) 방식으로 전환

JPEG 방식의 장점

  • H.264 대비 주요 비교 항목
    • 대역폭: H.264는 40Mbps 고정, JPEG는 100~500Kbps로 변동
    • 상태 관리: H.264는 상태 의존적, JPEG는 완전한 독립 프레임
    • 복구성: H.264는 키프레임 대기 필요, JPEG는 다음 프레임으로 즉시 복구
    • 복잡도: H.264는 수개월 개발, JPEG는 fetch() 루프 몇 줄로 구현
  • 네트워크 품질이 나쁠수록 단순한 JPEG 방식이 더 안정적이고 효율적

하이브리드 전환 구조

  • Helix는 두 방식을 RTT(왕복 지연 시간) 기준으로 자동 전환
    1. RTT < 150ms → H.264 스트리밍
    2. RTT > 150ms → JPEG 폴링
    3. 연결 복구 시 사용자가 클릭해 재전환
  • 입력 이벤트(키보드·마우스)는 WebSocket으로 계속 전송되어 상호작용성 유지
  • 서버는 {"set_video_enabled": false} 메시지로 비디오 전송을 중단하고 스크린샷 모드로 전환

전환 불안정(oscillation) 문제와 해결

  • 전송 중단 후 WebSocket 트래픽이 줄어들면 지연이 낮아져 자동으로 다시 비디오 모드로 전환되는 무한 루프 발생
  • 해결책: 스크린샷 모드 진입 후에는 사용자 클릭 전까지 고정 유지
    • UI에 “대역폭 절약을 위해 비디오 일시 중지됨” 메시지 표시

JPEG 지원 문제와 빌드 과정

  • Wayland용 스크린샷 도구 grim이 Ubuntu 기본 패키지에서 JPEG 지원이 비활성화되어 있음
    • grim -t jpeg 실행 시 “jpeg support disabled” 오류 발생
  • 이를 해결하기 위해 Dockerfile에서 libjpeg-turbo8-dev를 포함해 grim을 소스에서 직접 빌드

최종 아키텍처

  • 좋은 연결: 60fps H.264, 하드웨어 가속
  • 나쁜 연결: 2~10fps JPEG 폴링, 완전한 신뢰성
  • 스크린샷 품질은 전송 시간에 따라 자동 조정
    • 500ms 초과 시 품질 -10%, 300ms 미만 시 +5%, 최소 2fps 유지

주요 교훈

  1. 단순한 해법이 복잡한 시스템보다 낫다 — 3개월의 H.264 개발보다 2시간의 JPEG 해킹이 실용적
  2. 우아한 성능 저하(graceful degradation) 가 사용자 경험의 핵심
  3. WebSocket은 입력 전송에 최적, 영상 전송에는 필수 아님
  4. Ubuntu 패키지는 기능 누락 가능성 — 필요 시 직접 빌드
  5. 최적화 전 측정 필수 — 복잡한 스트리밍이 유일한 해법은 아님

오픈소스 공개

  • Helix는 오픈소스로 제공되며, 핵심 구현은 다음과 같음
    • api/cmd/screenshot-server/main.go — 스크린샷 서버
    • MoonlightStreamViewer.tsx — 적응형 클라이언트 로직
    • websocket-stream.ts — 비디오 전환 제어
  • Helix는 실제 환경에서도 작동하는 AI 인프라를 목표로 개발 중임

Read Entire Article