🟣 개념을 손으로 확인하려면
앞의 세 장은 스트리밍을 개념으로 정리했다. 프로토콜의 지연과 호환 사이 트레이드오프, HLS의 세그먼트와 적응형 비트레이트, WebRTC의 직접 연결. 머리로는 이해했지만, 카메라에서 나온 영상이 실제로 어떤 단계를 거쳐 브라우저에서 재생되는지는 직접 세워 보기 전까지 흐릿했다.
그래서 가장 고전적이면서 이해에 좋은 조합을 로컬에 만들어 봤다. RTMP로 영상을 받아 HLS로 변환해 내보내는 미디어 서버다. 방송용 도구(OBS 등)가 RTMP로 서버에 영상을 밀어 넣고, 서버가 그걸 잘게 잘라 HLS로 만들고, 브라우저가 그 HLS를 받아 재생한다. 앞 장들에서 나눠 본 프로토콜들이 하나의 파이프라인 안에서 어떻게 이어지는지 이 구성 하나로 확인된다.
🟣 왜 받을 때와 보낼 때 프로토콜이 다른가
가장 먼저 이상하게 느껴졌던 지점이다. 영상을 받을 때는 RTMP를 쓰고 내보낼 때는 HLS를 쓴다. 왜 하나로 통일하지 않을까.
이유는 두 구간의 요구가 다르기 때문이다. 방송 장비에서 서버로 올리는 구간은 지연이 낮고 연결이 안정적인 프로토콜이 유리하다. RTMP는 TCP 위에서 지속 연결을 유지하며 낮은 지연으로 영상을 밀어 넣는 데 오래 쓰여 왔다. 반면 서버에서 수많은 시청자로 내보내는 구간은 누구나 방화벽 문제 없이 받을 수 있고 CDN으로 퍼뜨리기 쉬운 프로토콜이 유리하다. HLS는 평범한 HTTP로 세그먼트 파일을 내려받는 방식이라 이 조건에 맞는다.
올리는 쪽(파랑)은 소수의 안정적 연결, 내보내는 쪽(초록)은 다수의 호환 우선 연결이다. 서버는 이 둘 사이에서 프로토콜을 바꿔 주는 변환기 역할을 한다. 이 그림이 왜 두 구간의 프로토콜이 다른지를 한눈에 설명한다.
🟣 nginx-rtmp: 받아서 자르는 서버
이 변환을 가장 적은 설정으로 해내는 것이 nginx의 rtmp 모듈이다. Nginx가 RTMP 입력을 받고, 들어온 영상을 HLS 세그먼트로 잘라 디렉터리에 쌓고, 그 디렉터리를 HTTP로 내보낸다. 앞 책에서 정리한 Nginx가 여기서 스트리밍 서버로 확장된다.
rtmp {
server {
listen 1935;
application live {
live on;
hls on;
hls_path /tmp/hls;
hls_fragment 3s;
}
}
}
http {
server {
location /hls {
types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; }
root /tmp;
}
}
}listen 1935가 RTMP 표준 포트로 방송 입력을 받는다. hls on과 hls_fragment 3s가 들어온 스트림을 3초짜리 세그먼트로 잘라 /tmp/hls에 쌓는다. 아래 http 블록은 그 디렉터리를 평범한 정적 파일처럼 내보낸다. 2장에서 본 m3u8 플레이리스트와 ts 세그먼트가 실제 파일로 여기 생성된다. 개념으로만 봤던 세그먼트가 디스크에 쌓이는 걸 직접 보면 HLS가 훨씬 구체적으로 잡힌다.
세그먼트 길이는 트레이드오프의 손잡이다. 짧게 자르면 시청자가 방송에 붙는 지연이 줄지만 요청 수와 파일 개수가 늘고, 길게 자르면 반대가 된다. 1장에서 본 지연-효율 트레이드오프가 이 설정값 하나로 손에 잡힌다.
🟣 Docker로 묶는 이유
이 구성을 직접 서버에 설치하려면 특정 버전의 Nginx에 rtmp 모듈을 붙여 다시 빌드해야 한다. 내 개발 머신에서 되던 게 배포 서버에서 안 되는 흔한 문제가 여기서 생긴다. 모듈 버전, 라이브러리, 경로가 환경마다 다르기 때문이다.
Docker는 이 환경 자체를 이미지로 고정한다. rtmp 모듈이 포함된 Nginx와 필요한 설정, 디렉터리 구조를 이미지 하나에 담아 두면, 어느 기계에서 실행하든 같은 환경이 뜬다. "내 컴퓨터에서는 됐는데"라는 말이 사라지는 지점이다.
docker run -d -p 1935:1935 -p 8080:80 \
-v $(pwd)/nginx.conf:/etc/nginx/nginx.conf \
tiangolo/nginx-rtmpRTMP 포트(1935)와 HLS를 내보낼 HTTP 포트(8080)를 열고, 위에서 만든 설정 파일을 이미지 안으로 연결해 실행한다. 이 한 줄로 방송 입력을 받아 HLS로 내보내는 서버가 뜬다. 뒤에 이어 공부한 클라우드 네이티브에서 이 컨테이너가 어떻게 격리되고 오케스트레이션되는지를 커널 수준까지 파고들게 되는데, 그 출발점이 이렇게 직접 이미지 하나를 실행해 본 경험이었다.
🟣 iOS 앱에서 이 스트림을 재생하면
내보낸 HLS는 iOS에서 특히 잘 맞는다. HLS는 애플이 만든 규격이고, AVPlayer가 m3u8 주소 하나만 넘기면 세그먼트 다운로드와 적응형 비트레이트 전환을 알아서 처리한다. 앞 장에서 본 적응형 비트레이트가 앱 코드에서는 그냥 URL 하나를 넘기는 것으로 끝난다. 네트워크가 나빠지면 플레이어가 알아서 낮은 화질 세그먼트로 내려간다.
여기서 서버 지식이 앱 개발로 이어졌다. 앱에서 영상 재생이 끊길 때, 그것이 플레이어 문제인지 세그먼트 생성이 늦는 서버 문제인지, 네트워크가 낮은 비트레이트로 못 내려가는 문제인지를 구분하려면 이 파이프라인 전체를 알아야 한다. 클라이언트만 보면 원인을 좁힐 수 없다. 미디어 서버를 직접 세워 본 경험이 앱의 재생 문제를 볼 때 어디를 의심할지 정하는 감각을 줬다.
🟣 정리
- 받을 때 RTMP, 내보낼 때 HLS를 쓰는 이유는 업링크(소수·안정·저지연)와 다운링크(다수·호환·CDN)의 요구가 다르기 때문이다.
- nginx-rtmp는 RTMP 입력을 받아 HLS 세그먼트로 잘라 HTTP로 내보낸다. 개념으로만 보던 세그먼트가 실제 파일로 생성된다.
- 세그먼트 길이는 지연과 효율 사이의 손잡이다. 짧으면 저지연, 길면 저부하.
- Docker로 모듈 포함 환경을 이미지로 고정하면 어느 기계에서나 같은 서버가 뜬다.
- HLS는 iOS
AVPlayer와 잘 맞는다. 서버 파이프라인을 알면 앱의 재생 문제에서 어디를 의심할지가 좁혀진다.
스트리밍 프로토콜의 선택에서 시작해 HLS와 WebRTC의 구조를 지나, 마지막으로 미디어 서버를 직접 세워 카메라 입력이 브라우저 재생까지 도달하는 길을 손으로 확인했다. 개념과 구축을 한 번씩 오가고 나니, 실시간 미디어라는 주제가 막연한 버즈워드가 아니라 각 구간의 트레이드오프로 나뉘어 보이게 됐다.