이번에 어떤 프로젝트에 참여하면서 라이브 스트리밍을 구현해야 할 일이 생겼습니다. 어떤 카메라로부터 영상을 받아서 그것을 웹브라우저에서 확인할 수 있게 해야 하는 건데요. 구글링을 하면서 찾아보니, 이걸 깔끔하게 구현해주는 마땅한 솔루션이 없었습니다. 그래서 이걸 직접 구현해보기로 했습니다.
영상, 특히 라이브 스트리밍을 할 때 생기는 가장 큰 문제점은 도대체 어떻게 영상을 받을지, 그리고 어떻게 스트리밍할지입니다. 다행스럽게도 저는 이전에 비슷한 경험을 몇 번 해 보아서 두 가지 솔루션을 알고 있었습니다.
- OpenCV등을 이용, 카메라 영상을 이미지로 변환 후 클라이언트에게 전송. 클라이언트는 이미지를 계속 업데이트하여 보여줌. 그러므로 실은 영상이 아니라 빠르게 바뀌는 이미지에 불과함.
- FFmpeg를 사용.
언듯 생각하기에는 1번 솔루션은 상당히 단순무식해보입니다. 그러나 실제로 해 보면 꽤 영상 퀄리티와 FPS가 괜찮은데다, 구현하기도 편하고(인코딩 관련 고생할 필요가 없습니다), 특히 영상에 어떤 처리를 한 후 전송해야 할 때 유용합니다. 그러나 소리를 전송할 수 없는 단점이 있습니다. 그래서 이번에는 2번 솔루션을 구현해보기로 했습니다.
이전에 했던 것
예전에 FFmpeg를 사용할 때에는 실시간으로 영상 전송이 가능한 프로토콜을 잘 몰랐습니다. 그래서 상당히 로우레벨로 접근해야만 했었는데, 다음과 같은 방법을 사용했습니다.
- FFmpeg를 사용하여 웹캠 영상을 webm형식으로 변환, 출력 스트림을
stdout
으로 pipe하면 Node.js 서버에서 그것을 받음. - Node.js서버에서는 받은 스트림을 버퍼에 담음.
- 버퍼가 다 차면 버퍼를 큐에 넣음.
- 만약 클라이언트가 영상을 요구할 경우, 서버는 큐에서 버퍼를 꺼내서 클라이언트에게 전송함.
- 클라이언트는 HTML5의 Media Source API를 이용하여 버퍼를 video tag에 추가함.
커밋 기록을 보니 이걸 구현한 게 19년 2월이었으니까, 대충 고등학교 졸업하기 직전이네요. 지금 와서 보니 상당히 비효율적인 구현입니다. 애초에 두 사람이 동시에 스트리밍을 받을 수 없는 구조네요.
이번에 한 것
이번에는 저렇게 하지 않고, FFmpeg의 기능을 충분히 이용하기로 했습니다. 코드가 정말 간단해서, 어쩌면 직접 소스코드를 보시는 편이 더 이해가 잘 될 수도 있습니다.
- FFmpeg는 출력을 HTTP Live Streaming(HLS)로 할 수 있는데, 이렇게 하면 출력으로
.m3u8
파일과 여러.ts
파일들이 생깁니다..ts
파일은 영상을 조각으로 나눠놓은 segment파일이며,.m3u8
파일은.ts
파일들의 정보를 담고 있는 메타 파일입니다. - Node.js서버는 단지 저 파일들을 정적으로 서비스하기만 합니다.
- 클라이언트는 hls.js 라이브러리를 사용하여 영상을
video
태그로 볼 수 있도록 합니다. 물론 일부 브라우저는 외부 라이브러리 없이video
태그의src
에.m3u8
파일을 넘겨줘도 잘 재생해줍니다. 그런데 가장 메이저한 브라우저인 크롬 데스크톱에서 이게 안 돼서 그냥 라이브러리를 쓰기로 했습니다.
아래는 Node.js에서 실행하는 FFmpeg 커맨드를 그대로 옮겨 온 것입니다. FFmpeg가 설치되어있을 경우 터미널에 바로 입력하면 작동합니다.
ffmpeg -f v4l2 -i /dev/video0 -c:v libx264 -crf 23 -pix_fmt yuv420p -hls_time 2 -hls_list_size 5 -hls_delete_threshold 1 -hls_flags delete_segments -f hls public/video.m3u8
인자에 대해 하나씩 알아보도록 하겠습니다. 소스코드에도 설명이 있습니다.
-f v4l2
: 입력 포맷을 v4l2(웹캠)으로 한다.-i /dev/video0
:/dev/video0
(웹캠)에서 영상을 읽어온다.-c:v libx264
: 출력 비디오 포맷을libx264
로 한다.-crf 23
: Constant Rate Factor (CRF)를 23으로 한다. CRF는 영상의 품질과 관련된 인자로, 낮을수록 화질이 좋아지며, 23이 기본값이다.-pix_fmt yuv420p
: 픽셀 포맷을 Y:U:V = 4:2:0으로 설정한다. 이는 어떤 플레이어들이 이 포맷밖에 지원하지 않는 경우가 있기 때문으로, 실제로 이렇게 하지 않으면 크롬에서 비디오가 깨진다. 설명 참조.-hls_time 2
: ts파일의 길이를 2초로 한다. (이유는 알 수 없지만 제 컴퓨터에서는 이 옵션이 제대로 작동하지 않고, 파일 길이가 약 8초였습니다.)-hls_list_size 5
:.m3u8
파일에 포함할.ts
파일 리스트의 길이를 최대 5로 설정한다. 이는 실시간 스트리밍이기 때문에 필요한 옵션으로, 오래 전에 생성된 것들까지 포함해서 모든 segment들을.m3u8
파일에 포함하면 비효율적이므로 최근 5개의 segment만 포함하는 것이다.-hls_delete_threshold 1
:.m3u8
에 포함되지 않은 segment를 1개까지 허용한다. 추후 설명.-hls_flags delete_segments
: 오래된 segment를 삭제한다.-f hls public/video.m3u8
: 출력 파일 이름. 이렇게 하면.ts
파일들은video.ts
와 같이 생성된다.
위의 옵션 중 -hls_list_size
, -hls_delete_threshold
, -hls_flags delete_segments
는 세 개가 한 세트인 옵션입니다. 약간 설명이 복잡한데, -hls_flags delete_segments
옵션은 위에서 언급했듯 오래된 segment들을 지우는 옵션이고, -hls_delete_threshold
옵션은 .m3u8
에 포함되지 않은 segment들의 경우, 몇 개까지를 오래된 것으로 생각할 것인지를 지정하는 옵션입니다.
예를 들어, hls_list_size
가 7이고, hls_delete_threshold
가 3이라고 가정합니다. 그러면 .m3u8
에는 n, n+1, n+2...n+6까지 총 7개의 segment에 대한 정보가 있을 것입니다. 이때 hls_delete_threshold
가 3이기 때문에, n-1, n-2, n-3까지의 segment는 오래된 것으로 간주하지 않고, n-4부터를 오래된 것으로 간주합니다. 그러므로 .ts
파일은 항상 10개가 존재하게 됩니다.
hls_delete_threshold
옵션이 필요한 이유는, 비록 .m3u8
의 segment 리스트에서는 특정 segment가 지워졌지만, 인터넷 속도 등의 문제로 해당 segment를 계속 다운받고 있는 클라이언트가 있을 수도 있기 때문입니다.
이렇게 옵션을 주면 파일들이 실시간으로 업데이트되며, 클라이언트는 일반 HLS파일을 재생하듯 실시간 영상을 재생할 수 있습니다. 아래는 스크린샷입니다.
웹캠에서 찍은 영상을 HTML로 스트리밍하여 볼 수 있습니다. (URL은 개인 서버를 사용 중이라 지웠습니다.)
참고문헌
-
-
첫 번째 답변이 도움이 많이 되었습니다. 단, 아래 옵션 중
-hls_wrap
은 deprecated되었습니다. 대신 제가 사용한 것처럼-hls_delete_threshold 1 -hls_flags delete_segments
를 사용하시면 됩니다.ffmpeg -v verbose -i rtmp://host:port/stream -c:v libx264 -c:a aac -ac 1 -strict -2 -crf 18 -profile:v baseline -maxrate 400k -bufsize 1835k -pix_fmt yuv420p -flags -global_header -hls_time 10 -hls_list_size 6 -hls_wrap 10 -start_number 1 pathToFolderYouWantTo/streamName.m3u8
-