본 포스팅은 개인적으로 진행한 토이프로젝트를 진행하면서 문제를 해결한 과정등을 정리한 포스팅입니다.


웹 브라우저에서 동작하는 네이버 뮤직은 어떻게 플러그인 하나 없이 스트리밍으로 노래를 들을 수 있을까? 라는 단순한 의문으로 간단하게 조사를 해 본적이 있는데, 네이버 뮤직 PC버전은 Http Live Streaming이라는 프로토콜을 이용해서 음원 스트리밍을 한다는 것을 알게 되었다.


HLS는 기존의 HTTP 웹서버로 쉽게 구성할 수 있도록 되어있어서 웹 서버 개발 경험이 있으면 쉽게 구현할 수 있을 것으로 보였습니다. 또한 스푼이라고 하는 라디오 방송 서비스에서도 HLS를 이용해서 라이브 음성 스트리밍으로 방송을 하도록 구현되어 있어서, 한번 비슷하게 만들어보기로 결정했다.


만들려고 하는 앱의 기능적 요구사항들은 다음과 같습니다.


1. 방송을 하는 사람은 웹 브라우저로 해당 방송을 하는 페이지로 접속합니다. 그러면 브라우저에 있는 마이크 API를 통해서 마이크에 녹음된 음성 정보를 일 정주기마다 서버로 보냅니다.

2. 서버는 받은 음성 정보를 처리해서 HLS로 전송가능한 형태로 바꿉니다.

3. 방송을 청취하는 사람은 웹 브라우저로 해당 방송을 청취하는 페이지로 접속합니다. 그러면 HLS를 통해서 방송하는 사람의 음성을 실시간으로 듣습니다.


HLS(Http Live Streaming) 프로토콜에 대한 내용은 네이버 d2에 잘 정리된 글이 있어서 해당 글을 보며 공부했다.

http://d2.naver.com/helloworld/7122


개략적인 HLS의 동작 방식을 확인한 후 비슷한 프로젝트가 있는지 한번 찾아보았더니 유사한 프로젝트가 있었다.


https://github.com/mjrusso/livestreaming-js


다만 조금 걱정이 되는 것은, 사용하는 nodejs 버전이 v0.2대 버전이고 최근 커밋이 2011년도이므로 현재 2018년인 상황에서 코드가 제대로 동작할지가 의문이었습니다. git clone을 받아서 실제로 필요한 파일들을 설치해서 구동을 시도했는데, 아니나 다를까 많은 에러 투성이었습니다.


nodejs 버전업이 많이 되면서 해당 프로젝트에서 사용한 API가 이미 deprecated되서 사라진 경우도 있었고, 결정적으로 ffmpeg의 SDK를 이용해서 미디어 데이터를 처리하는 C소스코드가 포함되어 있는데, 이 C API가 맞지 않아서 컴파일이 안되었다. 2011년도 버전 ffmpeg SDK를 사용하려고 했는데, 홈페이지에서도 구할 수 없을 만큼 오래된 SDK이라서 해당 프로젝트의 아키텍쳐와 문서를 참고해서 직접 새로 만들기로 했다.

또한 실제로 빌드가 된다고 하더라도, 해당 프로젝트는 파일 업로드를 직접 해서 스트리밍을 시키는 방식이고, 내가 원하는 프로젝트는 브라우저 마이크 API로 실시간으로 녹음되는 음성을 스트리밍 하는 것으로 조금 차이가 있다.


해당 서버를 단계별로 개발해보기로 했다.


일단 네이버 뮤직에서 m3u8파일과 ts파일들을 다운로드받아서 추출한 뒤, nodejs로 만든 간단한 서버로 뿌려주는 식으로만 작성해보았다.

Nodejs 패키지중에 hls-server라는 패키지가 있어서 확인해보았다.

https://www.npmjs.com/package/hls-server


문서가 많이 친절하지는 않지만, 대략적인 사용방법 예제코드들이 있다.

HLS 서버는 http 서버의 미들웨어 형태로 삽입되는 형식으로, http 요청 처리하는 로직 이전에 먼저 프로세싱을 하게 된다.


 

var server = http.createServer(function(req, res) { //HTTP 요청을 처리함. }); var hls = new HLSServer(server, { provider: { exists: function (req, callback) { // hls 미들웨어가 삽입된 서버에서 요청이 올 때마다 호출되는 함수 callback(null, true) // 파일이 존재하고 스트리밍을 할 경우 호출하는 콜백 callback(new Error("Server Error!")) // 500 error시 호출하는 콜백 callback(null, false) // 404 error시 호출하는 콜백 }, getManifestStream: function (req, callback) { // 적절한 .m3u8 파일을 리턴한다. // "req" is the http request // "callback" must be called with error-first arguments callback(null, myNodeStream) // or callback(new Error("Server error!"), null) }, getSegmentStream: function (req, callback) { // 적절한 .ts 파일을 리턴한다. callback(null, myNodeStream) } } }) server.listen(PORT);


npm 홈페이지에 있는 Using In-Memory Stream이란 탭에 있는 예제코드이다. 추가적으로 http 서버를 만드는 코드도 추가되었다.


HLSServer 함수의 첫번째 인자로 준 server에 해당 미들웨어가 삽입된다.

provider 객체 내의 exists함수는 해당 서버로 오는 모든 요청에 대해서 호출이 되며, Error을 던지게 되면 그 다음으로 진행하지 않고 에러를 응답하게 된다.

exists함수에서 callback(null, true)가 호출되게 되면, 요청 url의 path중 확장자를 추출해서 판단하게 되는데, 확장자가 .m3u8이면 getManifestStream 함수를 호출해서 요청을 처리하게 되고, 확장자가 .ts 이면 getSegmentStream 함수를 호출해서 요청을 처리하게 된다.

그리고 해당 확장자 (.m3u8이나 .ts)과 일치하지 않는 요청이면, httpServer 자체의 요청 처리 로직으로 넘어가서 처리되게 된다.


hls 패키지 미들웨어가 동작하는 방식이 잘 이해가 가지 않아서 소스코드를 확인해보았었다.


github에 공개된 hls-server node 패키지 소스코드 url이다.

https://github.com/RationalCoding/hls-server/blob/master/src/index.js


 

HLSServer.prototype._middleware = function (req, res, next) {
  var self = this

  var uri = url.parse(req.url).pathname
  var relativePath = path.relative(self.path, uri)
  var filePath = path.join(self.dir, relativePath)
  var extension = path.extname(filePath)

  req.filePath = filePath

  // Gzip support
  var ae = req.headers['accept-encoding'] || ''
  req.acceptsCompression = ae.match(/\bgzip\b/)

  if (uri === '/player.html' && self.debugPlayer) {
    self._writeDebugPlayer(res, next)
    return
  }

  self.provider.exists(req, function (err, exists) {
    if (err) {
      res.statusCode = 500
      res.end()
    } else if (!exists) {
      res.statusCode = 404
      res.end()
    } else {
      switch (extension) {
        case '.m3u8':
          self._writeManifest(req, res, next)
          break
        case '.ts':
          self._writeSegment(req, res, next)
          break
        default:
          next()
          break
      }
    }
  })
}


self.provider.exists 부분에 보면, 파일 확장자(extension)을 기준으로 알맞는 함수를 호출하고, 해당 확장자에 해당하지 않으면 해당 서버의 원래 처리 로직으로 가도록 되어있다. 그리고 두 확장자에 둘다 해당되지 않으면, next()를 호출해서 미들웨어에서 제어권을 넘겨준다. 이러한 함수들을 이용해서 이미 세그먼팅 된 .ts파일들과 .m3u8 매니페스트 파일로 브라우저단에서 잘 재생이 되는지 확인해보았다.


주로 쓰는 크롬 브라우저와 윈도우10에 디폴트로 설치되어있는 엣지 브라우저를 이용해서 테스트를 해 보았는데, 엣지 브라우저에서는 음악이 잘 재생되었는데, 크롬 브라우저에서는 재생이 되지 않았다. 어떻게 된 것인지 확인해보기 위해 HLS와 관련된 문서들을 찾아보았다.

+ Recent posts