유동

[NodeJS] 서버에서 영상을 배달하는법 HTTP Live Streaming(HLS) 본문

node.js

[NodeJS] 서버에서 영상을 배달하는법 HTTP Live Streaming(HLS)

동 선 2024. 5. 25. 18:11

개요

  • 현재 진행중인 프로젝트에서 관리자가 동영상을 업로드 한 후(500MB 이하의), 학생이 업로드 된 영상을 시청하는 기능이 있었습니다.
  • 처음엔 단순히 업로드 되어 서버에 저장된 동영상을 그대로 내려주면 되겠다는 안일안 생각을 했지만. 거의 용량 최대치인 480mb의 동영상을 업로드 하고 내려주는데 postman은 뻗어버리고 브라우저에서는 로딩하는데 상당한 시간이 걸렸습니다..
  • 이로인해 서버에서 클라이언트로 동영상을 전달하는 많은 방법을 알게되었고, 이중에 HLS라는 HTTP기반의 비디오 스트리밍 프로토콜을 알게되었고, 해당 프로젝트에 적용해봤습니다.

비디오를 전송하는 여러가지 방법들은?

  1. RTSP (Real-Time Streaming Protocol)
  2. RTP (Real-time Transport Protocol)
  3. RTMP (Real-Time Messaging Protocol)
  4. HLS (HTTP Live Streaming)

등의 전통적인 라이브 스트리밍을 위한 프로토콜이 존재합니다.
각각에 대한 자세한 설명은 다른 글에서 하고, HLS에 대한 설명만 짤막하게 하고 넘어가겠습니다.

HLS?

HLS(Hypertext Transfer Protocol Live Streaming)는 HTTP 기반의 비디오 스트리밍 프로토콜로,
비디오를 작은 세그먼트로 나누어 전송하여 실시간 또는 온디맨드 비디오 스트리밍을 가능하게 합니다.
클라이언트는 .m3u8 플레이리스트 파일을 통해 비디오 세그먼트를 가져와 재생하고, 네트워크 상황에 따라 자동으로 비디오 품질을 조절할 수 있어 사용자 경험을 향상시켜줍니다.
여기서 HTTP기반의 프로토콜이라는게 중요한데, HLS는 HTTP 프로토콜의 한 종류라고 생각하면 될것같습니다.
RTSP, RTP, RTMP같은 프로토콜은 서로 다른 네트워크를 통해 연결하기 때문에 네트워크 리소스, 방화벽 같은 문제가 생깁니다.
고로 서로 다른 프로토콜이 연결되는 과정에서 생기는 네트워크 비용을 절약하고 HTTP의 캐싱같은 장점을 활용할 수 있어. 동영상 스트리밍 시 거의 표준으로 사용된다고 함니다.

HLS의 특징

  1. 위에서 말했듯이 HTTP 프로토콜을 사용함
  2. 클라이언트 네트워크 상황에 따라 영상 화질을 선택할수 있음
  3. 클라이언트에서 영상을 쪼개서 다운받기 때문에 부분 재생이 가능, 10분짜리 동영상에 1분만 보고 나간다면 리소스낭비가 심하겠죠?
  4. 때문에 네트워크 리소스와 서버의 자원을 절약할 수 있음

HLS 프로토콜은 아래와 같은 .m3u8과 .ts파일을 이용하여 스트리밍합니다.

"대략적인 흐름"

.m3u8파일은 무엇일까?

  • 동영상의 세그먼트 목록을 포함하는 재생 목록 파일입니다
  • 영상 재생을 위한 메타 정보들과 세그먼트 데이터(.ts)경로들이 담겨있습니다. 클라이언트는 서버로부터 .m3u8파일을 읽고 적혀있는 세그먼트 데이터들을 차례로 요청함니다

.ts파일

  • 동영상을 작은 세그먼트로 나눈 파일로 실제 비디오 데이터가 포함되어 있습니다.
  • ts 파일은 실제 스트리밍 영상 데이터이며, 시간 단위로 작게 쪼개져 있습니다.

잠시 생각

위의 정의들을 대략 보아하니 대충 머릿속에 서버 로직이 그려집니다.

  1. 클라이언트로부터 동영상을 업로드 하면, 서버는 받은 원본 mp4파일을 쪼개는 작업을 거친 후(아래서부터는 트랜스코딩이라고 지칭) 적절한 위치에 저장
  2. .m3u8파일이 저장된 위치(이름)를 데이터베이스에 저장
    예시
  3. 이후 클라이언트에서 동영상의 idx (105)을 호출하면 서버는 적절한 로직을 거친 후, 해당하는 row의 url을 가져오고 (video_length는 무시해주시길..)
  4. 해당하는 url을 가지고 .m3u8과 .ts파일을 적절히 내려주면 되겠다!

 

머리속의 생각을 따라서 코드를 작성해봅시다

사용된 기술 스택

  • FE: HTML5, VanilaJS, VideoJS
  • Language & Framework: NodeJS(Typescript) + Express.js
  • Database & ORM: Postgresql + Prisma

잠시 구조에 대해 짤막한 설명

학습 콘텐츠 : 강의 영상 (1 : 1관계)

 

백엔드 코드

일단, multer를 이용해 동영상을 서버 디스크에 저장하는 미들웨어는 다른 글에서 자세히 설명할 예정이기 때문에 생략하겠습니다.

 

1. 먼저 원본 파일을 트랜스코딩하는 미들웨어

 

  • NodeJS환경에서는 Fmpeg라는 라이브러리를 사용하여 동영상 파일을 트랜스코딩합니다.
    (fluent-ffmpeg)
  • 이처럼 미들웨어로 만들어 놓고, 라우터에서는 아래처럼 동영상을 업로드하는 미들웨어의 다음에 장착해주면 되겠습니다
  • 더 많은 옵션은 ffmpeg-options을 참고하시면 될것같습니다
  • -f hls옵션을 사용해서 포맷을 설정하고, 10초단위로 영상간격을 쪼갰습니다.
  • 저는 트랜스파일 진행 과정을 소켓통신을 이용해서 클라이언트에 내려줄겁니다 (아직구현중ㅎㅎ), 또 트랜스파일이 완료되면 원본 파일을 삭제할겁니다
  • 해당 과정은 github에서 확인 가능합니다. https://github.com/DongSeonYoo/node-streaming

2. 동영상 업로드 요청을 받는 라우터

 

  • 해당 라우터에선 원본(mp4)파일을 저장하는 작업과, 트랜스코딩하는 작업을 순차적으로 수행한 후, 데이터베이스에 row를 생성한 후, 클라이언트에 생성된 비디오 인덱스를 내려줍니다.

프론트엔드 코드

 

 

  • videoJS 라이브러리를 사용하여 video 폼을 구현하였습니다.

파일을 선택하고, 업로드 버튼을 누르면 요청은 우리가 만든 라우터를 타고, 서버에서는 로컬 디스크 스토리지에 원본 파일을 업로드 한 후, 트랜스코딩 과정을 거칠것입니다.

파일 업로드 수행 결과 확인

예로, 23분(480mb) 가량의 비디오 파일을 업로드 해보겠습니다

  • 성공적으로 변환된걸 확인할 수 있습니다. (약 8분가량 걸렸습니다) .m3u8파일을 한번 볼까요?

.m3u8파일

  • 쪼개진 .ts파일이 순차적으로 기록되어있는 걸 볼수있습니다.
  • 프론트엔드에서는 서버로부터 .m3u8파일을 받아서 읽고 해당 파일의 내용을 기반으로 서버에서 ts파일을 요청할겁니다.
  • 그렇다면 서버에서는 .m3u8과 .ts파일을 내려주는 라우터를 받아야겠죠?

 

서버에서 성공적으로 트랜스코딩 된것이 확인되었으니 클라이언트에서 재생 요청을 해봅시다.

  1. 동영상 인덱스를 입력하고 재생버튼을 누르면, 우리가 만든 GET /learning-content/lecture-video/:videoIdx 으로 fetch요청을 보냅니다.
  2. 해당하는 비디오 인덱스가 존재하면, 해당하는 비디오의 이름을 받아옵니다. (1716550832018-060188 이겠죠?)
  3. videojs의 src에 받아온 이름(1716550832018-060188)을 넣어주면 서버로 .m3u8파일을 달라는 요청을합니다.
  4. 서버는 해당하는 이름을 가진 .m3u8파일을 서빙해주면, videojs는 곧바로 .m3u8파일을 읽어서 바로 ts파일을 요청합니다

videoJS가 hls방식으로 요청하는 로그를 찍어봅시다.

  • 처음 요청엔, 비디오 이름을 가지고 .m3u8파일을 요청합니다.
  • 이후(.m3u8파일을 받았다면) .ts파일을 요청하는 로그가 보입니다.

그렇다면 서버에서 .m3u8과 .ts파일을 스트리밍해주는 라우터를 만들어봅시다.

  • 같은 라우터로 .m3u8과 .ts파일을 요청하는거 보니, 한 라우터 내에서 두가지 일을 해야겠네요
  • 만약 .m3u8을 받은 이후부터는 path parameter에 ts파일을 요청하는걸 볼수 있습니다.
    • 그렇다면 와일드카드 패턴으로 .ts파일 요청을 받아봅시다.

 

  • 해당하는 파일 이름의 .m3u8이 존재하는지 검사합니다. 만약 없다면 404코드를 반환합니다.
  • 만약 req.params[0]이 없다면 .m3u8파일을 요청하는걸로 간주하고 m3u8을 내려줍니다.
  • 이후부터는 req.params[0]엔 .ts파일이 들어올겁니다. 이제부터는 서버에서는 ts파일을 긁어서 헤더를 설정하고, 스트림해줍니다

결과

  • 성공적으로 ts파일을 스트리밍해주는걸 볼수 있을겁니다.

여담

해당 요구사항을 마주하면서, 당연하게 비디오 그대로를 내려준다는 생각을 했던 저의 생각이 안일했던것 같습니다.

사실 동영상을 스트리밍하는 방식은 매우 많습니다. 제가 한 방식은 정석적인 방식이 아닙니다. 굳이 HLS방식을 고집할 필요도 없구요.
원본파일을 트랜스코딩하는 작업은 CPU연산이 많이 필요한 작업이므로 어플리케이션 서버에서 진행하면 굉장히 많은 리소스가 소모됩니다. 특히 NodeJS로 웹 서버를 구성하면 CPU-intensive한 연산은 가급적으로 피해야 합니다.

   • [2024-08-28 수정] ffmpeg 연산은 백그라운드에서 Host OS의 프로세스에 의해 실행되기 때문에, 실제로 Node.js에서의 CPU-intensive 작업은 아닙니다. 따라서 이 연산이 어플리케이션 서버에서 독립적으로 비동기적으로 처리될 수 있습니다.


따라서 많은 서비스들은 AWS video converter라는, 완전관리 클라우드 서비스를 서비스를 이용해 s3에 영상파일을 업로드 하면 자동으로 트리거되게 설정합니다.

하지만 저의 경우, 영상을 업로드하는 주체는 프로그램의 총 관리자이고, 빈번히 업로드가 일어나지 않고, 만약 대량으로 올리더라도 관리자가 새벽시간에만 올린다면 문제 없을거라고 판단해 해당 방식으로 구현했습니다.
또 위와같은 클라우드서비스는 비용과 개발자 리소스가 추가적으로 필요하기 때문에, 적합하지 않다고 판단하여 어플리케이션 서버에서 구현하였습니다.

모든 기술에는 정답이 없고, 트레이드 오프가 존재하기 때문에 근거만 충분하다면 그것이 정답입니다