이전에 작성한 효율적인 스트리밍을 위한 HLS 기술 에서는 저장된 영상을 스트리밍하는 방법에 대해 알아봤다. 그렇다면 실시간 영상을 스트리밍하려면 어떻게 해야 할까?
RTMP(Real-Time Messaging Protocol)
RTMP는 Adobe Systems에서 개발한 실시간 스트리밍을 위한 프로토콜이다. TCP 기반으로 동작하며, 낮은 지연 시간을 보장하여 실시간 스트리밍에 적합하다. RTMP는 실시간 영상 데이터를 서버로 전송하고, 서버는 이를 다시 시청자들에게 전달하는 역할을 수행한다. 이 과정을 자세히 살펴보자.
- 인코딩: 먼저 전송하고자 하는 영상 데이터를 인코딩하는 과정을 거친다. 인코딩은 영상 데이터를 압축하고 디지털 형식으로 변환하는 과정이다. H.264, VP8과 같은 코덱이 주로 사용된다.
- RTMP 서버로 전송: 인코딩된 영상 데이터는 RTMP 프로토콜을 통해 스트리밍 서버로 전송된다.
- 배포: RTMP 서버는 수신된 영상 데이터를 다양한 형식으로 변환하여 여러 플랫폼의 시청자들에게 배포한다. 주로 HLS, DASH와 같은 HTTP 기반 스트리밍 프로토콜로 변환하여 다양한 기기에서 시청 가능하도록 한다.
- 재생 : 시청자들의 디바이스에서 영상 데이터를 압축 해제/디코딩하여 영상을 재생한다.
Node.js를 이용한 RTMP 스트리밍 서버 구현
이제, Node.js를 이용하여 간단한 RTMP 스트리밍 서버를 구축해보자.
1. 필요한 패키지 설치
node-media-server는 Node.js 기반의 RTMP 서버를 구축하기 위한 라이브러리이다.
$npm install node-media-server
2. 서버 설정:
여기서는 RTMP 서버를 1935 포트에서, HTTP 서버를 8043번 포트에서 실행하도록 설정하였다.
const NodeMediaServer = require('node-media-server'); const ffmpegPath = require('ffmpeg-static'); const config = { rtmp: { port: 1935, // rtmp 서버 포트번호. chunk_size: 60000, gop_cache: true, ping: 30, ping_timeout: 60 }, http: { port: 8043, // http 서버 포트번호. mediaroot: './media', // 방송 송출할 파일의 위치 allow_origin: '*' }, trans: { ffmpeg: ffmpegPath, //ffmpeg가 설치된 경로 tasks: [ { app: 'live', // 포트번호 뒤에 입력할 url hls: true, hlsFlags: '[hls_time=2:hls_list_size=3:hls_flags=delete_segments]', hlsKeep: true, // to prevent hls file delete after end the stream dash: true, dashFlags: '[f=dash:window_size=3:extra_window_size=5]', dashKeep: true // to prevent dash file delete after end the stream } ] } }; var nms = new NodeMediaServer(config); nms.run();
3. 스트리밍:
RTMP 프로토콜로의 방송 송출을 지원하는 프로그램을 사용하여 영상을 스트리밍할 수 있다. 여기서는 OBS Studio를 사용하여 방송을 송출해보도록 하겠다.
OBS Studio 설정에서 방송 서비스를 사용자 지정을 변경한 후 서버 주소와 스트림 키를 입력하면 된다. 스트림 키는 방송을 구별하기 위한 고유의 키이다. OBS Studio의 경우 스트림 키를 입력하지 않아도 송출이 가능하다.
설정을 마친 후 방송 시작을 눌러 스트리밍을 시작할 수 있다.
3. 재생:
인코딩된 영상 데이터는 위 코드 상에서 지정한 mediaroot 위치에 저장된다. 따라서 8043 포트에 index.m3u8 파일을 요청하여 hls 데이터를 받을 수 있다.
https://서버주소:8043/live/스트림키/index.m3u8
hls 재생을 기본으로 지원하는 브라우저에서는 바로 재생이 가능하지만, 그렇지 않은 브라우저에서도 영상을 재생할 수 있도록 하기 위해서는 별도 라이브러리 사용이 필요하다.
대표적인 라이브러리로는 hls.js가 있다.
여기까지 하면 이 글의 제목인 ‘RTMP 스트리밍 서버를 만들어보자’를 완료했다.
하지만 뭔가 아쉬우니 스트리밍 데이터를 재생할 수 있는 클라이언트를 그럴듯하게 만들어보자.
우선 방송 중 여부와 방송 제목, 영상 송출 여부를 저장할 데이터베이스를 supabase로 구축했다.
클라이언트에서는 아래와 같이 supabase의 데이터를 가져올 수 있다. supabase와의 연동 방법은 본 글에서는 생략하겠다.
const { data, error } = await supabase .from('streaming') .select('title, description, isLive, isRadio');
실시간으로 값이 변경될 수 있도록 Realtime 업데이트를 사용할 수도 있다.
const subscription = supabase .channel('live-streaming_changes') .on('postgres_changes', { event: '*', schema: 'public', table: 'streaming' }, (payload) => { if (payload.new) { setStreamData(payload.new); if (payload.new.isLive !== streamData?.isLive || payload.new.isRadio !== streamData?.isRadio) { window.location.reload(); } } }) .subscribe();
DB에서 가져온 isRadio 값이 true인 경우 hls.js를 사용해 audio를 로드하고 재생해보자.
if (Hls.isSupported()) { hlsRef.current = new Hls(); hlsRef.current.loadSource(streamUrl); hlsRef.current.attachMedia(audioElement); hlsRef.current.on(Hls.Events.MANIFEST_PARSED, () => { console.log("HLS manifest parsed"); }); hlsRef.current.on(Hls.Events.ERROR, (event, data) => { console.error("HLS error:", data); }); }
만약 isRadio 값이 false로 영상을 함께 재생해야 하는 경우에는 video-js 라이브러리를 사용해 영상을 로딩하도록 했다.
const videoElement = playerRef.current; const player = videojs(videoElement, { controls: true, autoplay: false, preload: 'auto', responsive: true, fluid: true, sources: [{ src: streamUrl, type: 'application/x-mpegURL' }] });
라디오 모드에서 스트리밍되는 음성에 따라 원이 움직이는 애니메이션을 구현하였다. 이를 위해 AudioContext와 AnalyserNode를 사용하여 음성 데이터를 실시간으로 분석하고, 분석된 데이터를 기반으로 원의 크기를 조절하는 방식을 택하였다.
1. AudioContext 및 AnalyserNode 설정:
useEffect
훅 내에서 AudioContext
를 생성하고, AnalyserNode
를 연결하여 음성 데이터를 분석할 수 있도록 준비하였다. createMediaElementSource
를 사용하여 <audio>
요소의 음성 데이터를 AudioContext
로 입력받도록 하였다.const setupAudioContext = () => { audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)(); analyserRef.current = audioContextRef.current.createAnalyser(); const source = audioContextRef.current.createMediaElementSource(audioElement); source.connect(analyserRef.current); analyserRef.current.connect(audioContextRef.current.destination); };
2. 애니메이션 함수 구현:
animate
함수에서는 analyserRef.current.getByteFrequencyData(dataArray)
를 통해 현재 음성 데이터를 가져온다. 이 데이터는 Uint8Array
형태이며, 각 값은 특정 주파수 대역의 강도를 나타낸다const animate = () => { if (!analyserRef.current) return; // ... const bufferLength = analyserRef.current.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); analyserRef.current.getByteFrequencyData(dataArray); // ... };
3. 음성 데이터 분석 및 원 크기 조절:
dataArray
의 평균값을 계산하여 원의 크기를 조절한다. 평균값이 높을수록 원이 커지도록 하였다. requestAnimationFrame
을 사용하여 애니메이션을 반복 실행한다.// ... const average = dataArray.reduce((sum, value) => sum + value, 0) / bufferLength; const maxSize = Math.min(window.innerWidth, window.innerHeight); const size = 150 + (average / 255) * maxSize; audioCircle.style.width = `${size}px`; audioCircle.style.height = `${size}px`; animationRef.current = requestAnimationFrame(animate); };
그럼 아래와 같이 스트리밍되는 음성의 소리에 맞춰 움직이는 원을 만들 수 있다.
댓글