본문 바로가기

CS연구소👨‍💻

[미디어 서버] ffmpeg 미디어 서버 프로젝트

4월부터 현재까지 RTMP 프로토콜과 DIY 미디어 서버를 활용한 스트리밍 사이트 프로젝트를 만들고 있는데 워낙 배우는 것들이 많아서 정리를 해야할 것 같았다.

왜 미디어 서버였나

일단 내가 다른 프로젝트에서 WebRTC 기술인 OpenVidu를 사용해 본 적이 있어 관련된 공부를 해놨고 스트리밍도 비슷한점이 있을 것이라 판단해 어려우면서도 많이 배울 수 있는 프로젝트겠다! 라는 결론이 들어 제작을 시작했다

처음 계획

1. kurento를 이용한 제작

처음 생각한 꽃밭 아이디어

일단 스트리밍 서비스 만들려고 인터넷에 구조나 실현방법을 검색해보면 미디어 서버가 많이 나온다. 스트리밍을 위해선 스트리머가 보내는 데이터를 인코딩등의 처리를 하는과정이 필요하고 이때문에 미디어 서버가 필요하기 때문이다. 그래서 이전에 kurento를 래핑한 Openvidu를 사용해본 경험으로 kurento가 rtp 요청도 받아서 미디어 서버 역할을 해준다는 것을 듣고 kurento를 사용하려했다.

그런데 이제 그 계획 자체는 실패했다 kurento는 webrtc를 사용할때는 자체적으로 연결만 해주면 되는데 rtp 요청을 보낼때는 gstreamer을 통한 추가 설정이 필요했다. 문제는 이게 생각보다 찾아볼 수 있는 자료도 없고 난이도도 높았다는 것 시간이 6주 밖에 없었기 때문에 다른 방법을 시도했다.

 

RTMP프로토콜을 이용한 DIY서버 만들기

kurento를 버리면서 생각보다 많은 대안들이 있다는 것을 알게 되었다. 일단 가장 많이 쓰이는 것은 nginx + ffmpeg 페어로, ffmpeg은 미디어 입출력을 처리하는 강력한 툴이고 nginx는 입력으로 들어오는 데이터를 내부 ffmpeg과 연결해주는 방식으로 많이 사용한다. 그런데 이런 예제들이 너무 많기도했고, 스트리밍 서비스이다 보니 방 시작 api와 동기화 하고싶다는 생각이 들어서 그냥 아예 api서버에서 ffmpeg을 실행하는 방식으로 해보자라는 결론을 내리게 되었다.

내가 찾아본 방식과 직접 구현하려했던 방식

그래서 구현은 일단 스트리머가 django에게 api로 요청을 보내면 장고가 ffmpeg에게 명령을 보내고 그 이후에 스트리머가 OBS를 통해 방송을 키는 방향으로 만들어보았다.

 

1. 장고에서 스트리밍을 실행하는법.

아예 아무것도 몰랐을 때는 http처럼 장고가 직접 rtmp를 받고 처리하는 미디어서버를 계획했었다. 장고는 웹서버 어플리케이션이기 때문에 기본적으로 rtmp포트를 입력받지 못한다. 다행이 OBS같은 잘만든 rtmp 송신 프로그램이 있기 때문에 나는 데이터를 수신하는 방법만 고안하면 되었다.

 

일단 여러 nginx + ffmpeg의 예시들을 보면서 ffmpeg이 rtmp요청을 받을 수 있다는 것을 확인했다. 그래서 그냥 도커를 씌우고 api 요청을 받으면 장고가 직접 ffmpeg 도커에 명령어를 보낸 뒤 그 후 OBS를 통해 데이터를 받는 방식을 썼다.

 

일단 os에 직접 명령어를 보낼 수 있는 장고의 subprocess를 사용했다. 그리고 docker 역시 간단하게 ffmpeg만을 담은 dockerfile을 통해서 1935 포트에 대해서 데이터를 받게 했다.

 

import subprocess
	
    	...
    
    command = [
        'docker', 'exec', container_name, 'ffmpeg',
        '-listen', '1',
        '-i', rtmp_url,
        '-g', '30',
        '-c:v', 'libx264',
        '-preset', 'fast',
        '-tune', 'zerolatency',
        '-c:a', 'aac',
        '-b:a', '128k',
        '-f', 'hls',
        '-hls_time', '1',
        '-hls_list_size', '10',
        output_path
    ]
    	
        ...
        
    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    # 로그 출력 (예시로 stderr만 출력)
    stdout, stderr = process.communicate()

 

이번에 특히 ffmpeg을 사용할 일이 많았는데 처음 써보는 입장으로썬 여간 어려운게 아니었다 그래도 하나하나 끊어서 보면 읽을 논리적으로 접근할 수 있으니 하나하나 정리해보자.

 


1-1. ffmpeg 명령어 정리

리스트에 담겨져있는 명령어를 다시 한줄로 만들면 이렇다.

ffmpeg -listen 1 -i rtmp://0.0.0.0:1935/live/stream -c:v libx264 -preset fast -tune zerolatency -c:a aac -b:a 128k -f hls -hls_time 10 -hls_list_size 10 -hls_wrap 10 /files/stream.m3u8

 

1. ffmpeg : 프로그램 시작 언어

2. -listen 1 : 직관적으로 뭔가를 들어라라는 의미일 것인데 서버모드를 의미한다. 서버 모드는 대기가 가능한가를 의미한다. 예를들어 누군가가 api요청을 보내고 이후에 슬금슬금 OBS를 킨다면, ffmpeg이 그 입력을 대기받는 시간이 필요하다. 만약 저기서 -listen 1이 없다면 ffmpeg은 바로 들어오는 입력에만 처리를 실행하고 입력이 없을 경우에는 에러만 띄울 수 있다.

3. -i rtmp://0.0.0.0:1935/live/stream 말그대로 입력을 어디서 받을지를 의미한다. 앞에 listen 이 있냐 없냐에 따라서 데이터가 바로들어올필요가 있는지 없는지가 정해진다.

4. -c:v libx264 c는 코덱이라고 생각하면 된다 한국말로하면 -{코덱은}:{비디오일 땐} libx264라는 의미이다.

5. preset, 이 설정은 기본적으로 ffmpeg이 인코딩과 압축의 정도/균형을 조절하는 옵션이라고한다. G쌤에 따르면 총 10가지가 있는데

  1. ultrafast
  2. superfast
  3. veryfast
  4. faster
  5. fast
  6. medium (기본값)
  7. slow
  8. slower
  9. veryslow
  10. placebo

ultrafast에 가까울 수록 인코딩은 빠르지만 압축효율이 별로라 품질이 낮고 파일이 커지지만 placebo에 가까울 수록 효율이 높지만 인코딩 속도가 느리다고한다.

6. tune, 이건 preset보다 좀더 맞춤형으로 내 상황에 따라서 레이턴시나 품질을 조절해주는 코드다.

  1. film: 고해상도 영화 소스를 위해 최적화되어 있습니다.
  2. animation: 애니메이션 콘텐츠에 최적화되어 있습니다.
  3. grain: 필름 그레인이나 노이즈가 많은 소스를 위해 최적화되어 있습니다.
  4. stillimage: 정적 이미지나 슬라이드쇼에 최적화되어 있습니다.
  5. fastdecode: 디코딩 속도를 빠르게 하기 위해 최적화되어 있습니다.
  6. zerolatency: 지연 시간을 최소화하기 위해 최적화되어 있습니다. 실시간 스트리밍에 유용합니다.

여기서 나는 실시간 스트리밍이기 때문에 zerolatency를 사용했다.

7, -c:a aac 비디오처럼 오디오 코덱을 aac로 정하는 내용이다.

8. b:a 128k 오디오의 비트레이트를 의미하는 코드다. 비트레이트는 1초당 흐르는 비트를 의미하고 128kbps(비트)는 16 kBps(바이트) 라고 할 수 있다.

9. f hls 출력 파일 형식을 hls로 설정한다.

10. hls_time hls는 서버가 전송하는 스트리밍 방식이 아니라 사용자가 요청하는 스트리밍 방식이다 그렇기 때문에 http통신을 통해서 영상 패킷을 받게되는데 그러다보니 한 패킷의 길이를 정해줘야한다. 길이가 길어질수록 한 패킷을 만드는데 드는 시간이 길어지는 것이나 마찬가이지기 때문에 레이턴시에 영향을 줄 수 있다.

11. hls_list_size 하나의 패킷이라고 할 수 있는 ts파일이 있다면 그것들을 총괄하는 m3u8이라는 파일이있다. 이 파일은 각각 다운받아야할 ts파일들을 정의해놓는다.

12. hls_wrap 이 파일은 ts파일이 늘어나면서 같은 파일을 순환하는지 물어보는 일을 한다고 한다. 실시간 영상에서는 효율적일 것 같은데 녹화영상을 저장해야하는 스트리밍 사이트 특성상 사용할일은 없을 것 같다.

13. /files/stream.m3u8 : 파일경로 

 

여기서 내가 말하지않은 설정인 -g가 있는데 -g는 hls에서 하나의 키프레임을 담당한다. 키프레임은 한마디로 n번째 프레임이 한 시간 단위가 하게 해주는 역할을 한다. 만약 이 옵션을 사용하지 않으면 시간을 의미하는 ffmpeg의 hls_time같은 커맨드들은 전혀 작동하지 않고 한 패킷의 디폴트 양을 채우는 것으로 보인다. 

 

하여튼 우여곡절 끝에 장고와 도커, ffmpeg을 이용해 스트리밍 데이터를 만드는 일을 성공했었다. 하지만 이게 끝이 아니고 더 많은 문제들이 남았었지만 나눠서 글을 써야할 것 같다.