사용자 도구

사이트 도구


project:personalstreaming

동영상 파일의 HTTP 라이브 스트리밍

미리 일러두기

본 위키의 문서들은 저 스스로가 조사한 바를 잘 정리해두고, 차후에 다시 필요할 때 빠르게 재학습이 가능하도록 하기 위해 제작되고 있습니다. 완성된 문서들은 현재는 PDF 파일의 형태로 공개되고 있습니다만, 항상 저 이외의 다른 사람들이 알아 보기 쉽게 제작하려 애쓰지는 않습니다.

저는 제 나름의 조사 기록을 위해 본 문서를 작성하고 있습니다. 전문가의 입장에서 올바른 전문 지식 전달을 위해 작성된 문서가 아니니 곳곳에 불충분한 내용, 오탈자, 그리고 내용 및 논리상의 오류가 있을 수 있습니다. 만일 발견더라도 너그러이 이해해 주시기를 바랍니다. 피드백 및 질문은 Cs.ChwNam@gmail.com으로 부탁드립니다.

시작하며

/*스트리밍 서버 비슷한 것을 만들 생각입니다. 미리 말씀드리자만, 최종 형태는 정말 볼품없는 프로토타입입니다. 다 아시다시피 멋진 물건을 만들기란 쉬운 일이 아니지요. 그냥 요즘 스마트폰에서 자주 사용되는 스트리밍 서버의 원리, 혹은 스트리밍 서비스를 위해 어떤 것들이 필요한지를 간단히 알아 보는데 그 의의를 두고 싶습니다. 즉 결과보다는 과정이 중요한 문서가 아닐까 생각합니다.*/

요즘 개인 서버 장비를 많이 사용하고 있습니다. 저렴한 가격의 NAS 장비도 많이 보급되었고 포고 플러그라즈베리 파이 같은 저전력 임베디드 기기를 이용하기도 하더군요. 이러한 장비들은 대개 저전력이므로 장시간 사용시 PC 대비 경제적이고, PC보다 성능이 떨어진다 해도 왠만한 멀티미디어 재생은 가능한 수준입니다. 그 뿐 아니라 스마트폰, 랩탑 피씨 등 다른 장비와 네트워크로 연결될 때 그 활용도는 더욱 증가합니다.

개인 서버 장비에서 많이 사용되는 서비스중 하나로 '멀티미디어 스트리밍 서비스'가 있습니다. 'Air Video Server'같은 녀석들입니다. 이 서버는 원하는 영상 파일을 트랜스코딩 시켜 휴대 장비로 전송합니다. 집 안에서도, 집 밖에서도 서버가 있다면 언제든 원하는 동영상을 감상할 수 있습니다. 물론 최근의 스마트폰의 사양은 정말 대단해서 고화질의 동영상을 트랜스코딩하지 않고도 재생이 되는 수준입니다. 하지만 고화질의, 따라서 고용량의 파일을 일일이 복사해기는 좀 번거롭지요. 물론 스트리밍 서비스는 항상 네트워크가 원활해야 한다는 단점이 있습니다만…

본 문서는 이러한 서비스가 어떻게 이루어지는지, 이런 서비스를 위해 필요한 기술들은 무엇이 있는지에 대해 다룹니다. 설명은 간략하고 어렵지 않은 수준에서 개략적인 부분만을 다룹니다. (그러나 저로서도 '어렵지 않은 수준'이 무엇인지 딱 잘라 말하기 쉽지 않습니다. 멀티미디어와 네트워크 분야가 겹치는 부분인 만큼 전제되는 상식이 꽤 많을 겁니다. 불친절한 부분이나 모르는 용어가 많이 나오더라도… 어쩔 수 없습니다. 그런 것들은 알아서 찾으셔야 합니다. 저도 초보자이며, 제가 모든 것을 해 드릴 수 는 없습니다.)

알아 두면 나쁘지 않을 것들

이 장의 이름은 '알아 두면 나쁘지 않을 것들' 입니다. 제가 조사한 것들을 정리하는 장입니다. 반드시 알아야 할 필수불가결한 것들만 나열한 것도 아니고, 필요한데 빠진 것도 있을 겁니다. 여하튼 제목대로 읽어 두어서 나쁠 것은 없습니다.
사실 이번 문서의 구현은 어쨌든 상관없다고 생각합니다. 이번 장에 정리된 사항이야말로 본 문서에서 가장 중요한 내용입니다. 단, 클라이언트와 서버 사이에 일어나는 요청 및 응답 등의 작동 과정은 이미 지난 문서1)에서 설명하였으므로 생략합니다.

코덱(codec)

'통합 코덱' 같은 것을 통해 많이 익숙하신 용어일 것 같아 가장 처음에 배치했습니다. 보통 동영상 감상을 하는데 소리가 나오지 않는다든가 영상이 나오지 않으면 '코덱이 없어서 발생한 현상이니 XXX 코덱을 설치하세요'란 해결책을 가장 많이 접해 보았으리라 생각합니다. 그래서인지 코덱이라고 하면 동영상 재생에만 관련된 용어라는 생각을 할 수도 있습니다만, 사실 그렇지는 않습니다. 코덱이란 말은 coder와 decoder를 합친 말입니다. 이를 한글로는 '부호화', '복호화'라고도 합니다.

조금 조잡한 부호화와 복호화의 예시지만 쉬우니까 이렇게 설명해 보겠습니다. 가령 '책상이 다섯 개 있다'와, '의자가 세 개 있다'라는 정보를 표시하기 위해서 'T5 C3'라고 적을 수도 있겠죠. 혹은 한국어로 '책5/의3' 이렇게 적을 수도 있겠죠. 이렇게 '책상 다섯 개와 의자 세 개'를 'T5 C3'나 '책5/의3'로 적는 것을 부호화, 그 반대를 복호화라고 합니다. 만일 '책'은 책상, '의'는 의자를 의미한다는 것과 각 글자 뒤의 숫자는 각 물건의 개수를 의미함을 알지 못한다면, 'T5 C3'나 '책5/의3'만 보고서는 대체 이것이 무엇을 의미하는지 이해하기 도통 어렵습니다. 부호화와 복호화는 하나의 규칙을 가진 동전의 양면과 같은 녀석들입니다.

즉 코덱은 어떤 데이터를 부호화(인코딩)를 하고, 신호를 다시 복호화(디코딩)하여 원래의 정보로 변환하는 역할을 맡은 것들을 일컬어 말합니다. 즉 코덱은 동영상 재생에만 쓰이는 것이 아닙니다. 그리고 코덱이란 말은 소프트웨어나 하드웨어에 둘 다 사용됩니다.

아래 그림을 예로 들어 보겠습니다.

디지털 카메라로 사진을 찍는한다고 가정합니다. 화살표의 흐름대로 하나씩 설명하도록 하겠습니다.

  1. 좌측 상단의 사막 그림은 현실 세계를 의미합니다. 카메라 렌즈를 통해 '빛'이라는 신호가 카메라로 들어옵니다. 이 때 입력되는 빛의 신호는 '아날로그 데이터'입니다.

  2. 디지털 카메라에는 필름 대신 CCDCMOS 같은 감광 센서가 있는데, 이것이 빛을 감지해 빛을 전기 신호로 변경합니다.
    • 아날로그 데이터들은 연속적입니다. 다시 말해 망원경으로 사막을 확대하고 확대해서 보아도 흔히 말하는 '깍두기 현상' 같은 것은 일어나지 않습니다.
    • 그러나 그 신호 모두를 다 기록하기란 불가능합니다. 그래서 센서는 신호의 일부분만을 띄엄띄엄 받아들입니다. 이러한 작업을 '샘플링(sampling)'이라고 합니다.
    • 물론 요즘 디지털 카메라들은 어마어마한 화수 수를 자랑하지만, 어쨌든 연속된 신호 형태로 기록하는 건 아니니 띄엄띄엄입니다.
    • 아무리 좋은 디지털 카메라라도 촬영된 영상을 확대하고 확대하면 결국 우측 위 사진처럼 깍두기 현상 - 계단 현상이 일어나기 마련입니다.
    • 샘플링은 비가역적인 작업입니다. 현실 세계를 샘플링해 사진으로 만들 수 있지만, 그 반대로 사진으로 현실 세계를 만들 수는 없습니다.
    • 샘플링된 빛은 보통 사각 모눈과 같이 빛의 3원색인 적, 녹, 청 3색의 강도(intensity)로 표현됩니다. 즉 빛의 세기가 숫자로 표현되는 것입니다.

  3. 보통 이렇게 샘플링된 데이터를 일컬어 '원본 데이터'라고 합니다. 보통 멀티미디어의 원본 데이터는 분량이 상당합니다.
    • 간단하게 계산으로 이 원본 데이터의 용량이 얼마나 되는지 알아봅니다.
      • 적/녹/청 각 채널당 8비트(1바이트)를 사용한다고 가정합니다. 그러면 각 채널은 0~255의 숫자 범위를 갖습니다.
      • 1픽셀당 24비트, 즉 3바이트를 차지합니다. HD 영상의 사이즈는 가로 1920픽셀, 세로 1080픽셀입니다. 그러면 HD 영상 사이즈의 채널 당 8비트인 총 24비트 영상의 순수하게 압축되지 않은 크기는,

        3 byte x 1920 x 1080 = 6220800 byte.
        (6220800 bytes –> 6075 kilobytes –> 5.93 megabytes)

  4. 단 1장의 데이터가 이만한 용량을 차지합니다. 그나마 정지 영상이 이렇습니다. 동영상은 이러한 그림을 1초에 24~30장 이상을 뿌려야 합니다. 어마어마한 저장 공간을 필요합니다. 그러므로 압축(compression)이 필수불가결하게 되었죠. 압축도 인코딩의 일종입니다.
    • 데이터와 같은 자료들은 한치의 오차 없이 정확히 기록되어야 하므로 압축을 하더라도 원래의 정보가 소실되어서는 안 됩니다. 이러한 압축을 무손실 압축(lossless compression)이라고 합니다. ZIP이나 RAR 등이 이러한 무손실 데이터 압축 프로그램입니다. 하지만 멀티미디어와 같이 사람의 감각 기관으로 전달되는 데이터의 경우는 어느 정도 소실을 허용합니다. 이를 손실 압축(lossy compression)이라고 합니다. 얼마만큼 원본과 같이 자연스럽게, 그러면서도 압축을 잘 하느냐가 관건입니다. JPEG은 정지 영상을 위한 포맷이며 디지털 카메라에서도 많이 사용됩니다. 이렇게 압축을 하면 우리가 흔히 보는 납득할 만한 용량을 가진 JPEG 파일이 됩니다.

  5. JPEG으로 압축된 파일은 JPEG 디코더를 통해 적, 녹, 청 빛의 삼원색 데이터를 복구할 수 있습니다. 이 신호를 프로젝터, 모니터, 프린터, 인화기 등 다양한 출력 장비로 보내 최종적으로 눈이 인지할 수 있는 빛 신호로 변화시키면 우리가 말하는 사진이란 것이 됩니다. 단, JPEG은 원본 데이터에 손실 주어 압축을 한 것이므로 디코딩된 데이터는 원본 데이터와는 같지 않습니다. 물론 같지 않다고 해서 사막 사진이 바다로 변한다거나, 화면을 알아볼 수 없을 정도로 변하는 것은 아닙니다. 단지 원본 데이터와 비교해 픽셀의 색상이 열화되는 것 뿐이죠. 압축을 덜 한 사진은 고품질의 영상이지만 용량이 많고, 압축을 많이 한 사진은 용량이 적은 이점이 있지만 품질이 좋지 못합니다. 또한 영상에서 시각적으로 뚜렷이 확인되는 열화된 부분을 가리켜 '아티펙트(artifact)'라고도 부릅니다.

위 그림은 원본 JPEG 파일을 여러번 압축하여 손실을 누적해 화질의 열화를 의도적으로 만든 예입니다. 오른쪽은 왼쪽과 비교해 동일한 이미지임을 시각적으로 판단할 수 있지만, 자세히 보면 여러 열화된 흔적을 발견할 수 있습니다.

샘플링 레이트(sampling rate)와 비트레이트(bitrate)

샘플링 레이트와 비트레이트에 대해 잠깐 언급을 하고 가겠습니다. 그냥 설명하기는 껄끄러우니 MP3 파일로 예를 들어 설명을 하도록 하지요.
MP3 파일을 재생할 때 샘플링 레이트와 비트레이트가 적힌 숫자가 표시되는 것을 보셨나요? 요즘은 MP3 파일 및 플레이어가 너무나 대중적으로 보급되어 이러한 복잡한 숫자나 단위 같이 조금 어려워 보이는 것들은 숨겨져 버리는 것 같습니다.

위 그림은 MP3 초창기 때부터 나와 꾸준히 인기를 얻었던 WinAmp의 고전 스킨입니다. 요즘은 다른 재생기에 많이 밀렸지만, 한때는 최강자로 군림했었죠… 아무튼 그림에서 확실히 '320', 'kbps', '44', 'kHz'라는 숫자와 문자가 있음을 확인할 수 있지요? 예시한 윈앰프는 샘플링 레이트가 44.1kHz, 320kbps의 비트레이트를 가진 MP3 파일을 재생 중입니다. MP3 파일을 조금 다뤄 보신 분들이라면 대개 비트레이트가 높을 수록 용량은 크지만 고음질이란 것을 상식적으로 알고 계실 겁니다. 그럼 샘플링 레이트는 무엇일까요? 이것도 낮으면 저음질이고, 높으면 고음질이 될까요?

앞서 코덱을 설명하면서 디지털 카메라로 촬영을 하는 예를 들면서 '샘플링'을 한다는 예를 들었습니다. 그 샘플링과 지금 설명하는 샘플링은 동일한 의미를 지닌 단어입니다. 다만 디지털 카메라는 빛을 샘플링한다면 음악의 파일은 '음파'를 샘플링한다는 점이 다릅니다. 디지털 카메라에서는 빛을 샘플링하여, 적/녹/청 빛의 3원색의 요소를 분석해, 그 강도를 '픽셀'에 저장하고, 픽셀의 집합은 하나의 사진이 되지요. 반면 소리라는 연속적인 아날로그 신호는 일정 주기를 기준으로 그 시점의 신호를 디지털 형태의 신호로 기록합니다. 이 주기가 음성 신호의 샘플링 레이트입니다.

사진의 샘플링 레이트는 사진의 해상도, 음악의 샘플링 레이트는 주기인 Hz가 됩니다. 아래 그래프2)를 보시지요. 푸른 곡선은 아날로그 신호, 붉은 선과 검은 점은 신호를 측정한 지점입니다. 1초라는 시간 동안(가로축이 시간) 찍히는 점의 갯수가 샘플링 레이트라고 보면 됩니다. 조밀할수록 원음와 가까워지겠죠? 그렇다고 너무 조밀해져도 데이터량이 지나치게 많아져 좋지 않습니다. 보통 CD는 44.1kHz, 48kHz를 쓰는데 이는 음성 신호를 초당 각각 4만 4천 1백회, 4만 8천회 기록한다는 뜻이지요.

그렇다면 비트레이트는 무엇일까요? 이것은 MP3 인코더가 얼마만큼 신호를 압축해버렸는지를 기록한 수치라고 보면 됩니다. 더 정확히 말하면 초당 얼마만큼 정보를 전송하는지를 기록한 것입니다. 이것이 크면 한 번에 전송할 수 있는 데이터의 양이 많아지죠. 320kbps (kilobit per second) 비트레이트의 MP3 파일은 초당 320kbit, 초당 40KiB를 전달합니다. 192kbps는 초당 24KiB를 전달하지요. 두 파일이 같은 오디오 CD에서 리핑되었다면 당연히 한 번에 전송하는 데이터양이 많은 320kbps의 MP3 파일이 더 음질이 좋습니다.

비트레이트는 그림 파일의 경우 그림 파일의 압축률에 해당합니다. 압축을 많이 하면 용량은 줄지만 보기 싫은 아티팩트가 생깁니다. 음악 파일도 압축을 많이 하면 음질이 심하게 떨어지게 되죠. 뭔가 자연스럽지 않은, 인공적인 잡음이 징징대는 것을 들어 보셨나요? 보통 음성의 상태보다는 전달하는 내용이 중요한 경우라면 비트레이트를 많이 삭감시켜 데이터 소모를 줄이곤 합니다.
이러한 잡음은 녹음 과정에서 발생하는 것이 아니라 이산적인 신호를 기록할 때 필연적으로 발생하는 손실 때문에 발생합니다. 재생을 할 때는 손실된 부분도 어쨌든 음성 신호로 복구해야만 합니다. 그러나 아무래도 알 수 없는 데이터를 원래와 같이 만들 수는 없기에 최선의 방법으로 알 수 없는 부분의 데이터를 이미 알려진 것으로 말미암아 어림짐작해 끼워 맞춥니다. 그러나 이는 어디까지나 어림값이므로 본디 소리와는 뭔가 다른 어색한 소리가 되어 들리는 것입니다.
CD나 고음질의 MP3 파일처럼 샘플링 레이트가 높고, 비트레이트가 높으면 이러한 손실은 피할 수 없다 하더라도 데이터의 양이 워낙 풍부하므로 원음과 달라짐을 귀로는 구분하기 어습니다. 반면 샘플링 레이트와 비트레이트가 낮으면 이 차이가 현격해집니다. 그래도 사진과 마찬가지로 '사과'라는 소리가 '수박'으로 변하진 않을 겁니다. :-)

트랜스코딩(transcoding)

인코딩은 원래의 자료, 혹은 비손실 압축 자료에서 부호화를 시키는 작업을 말하는 것이고 트랜스코딩은 어떤 포맷으로 인코딩된 자료를 다른 포맷으로 변환하는 작업을 말합니다3). 요즘은 쓰기 편리한 동영상 변환 소프트웨어가 많습니다. 기능이 꽤 충실함에도, 무료로 제공되는 경우도 있습니다. 다음의 팟 인코더라든지 곰 인코더 를 예로 들 수 있겠습니다.

보통 휴대기기에 알맞게 원본 동영상을 변환하는 작업을 가리켜 흔히 '인코딩한다'고 합니다. 하지만 앞서 말씀드렸듯 코덱의 인코딩과 디코딩은 동전의 양면과 같이 반드시 공존해야 합니다. 인코딩 없이 디코딩도 없고, 디코딩 없이는 인코딩도 의미가 없습니다. 그러므로 이런 작업들은 원본 동영상을 먼저 디코딩하여 원래 정보로 변환한 후, 원래 정보를 인코딩하여 다른 형식으로 정보를 저장하는 것입니다. 그냥 막무가내로 인코딩을 한다는 것은 있을 수 없습니다.

이런 의미로 본다면, '동영상 변환'이 '인코딩'은 아니지 않을까요? '트랜스코딩'이란 더 적절한 표현이 있는데 말이죠. 각 제작자분들에게 이름의 시비를 따지고 싶어서 이런 건 전혀 아닐 뿐더러, 요즘 대부분 동영상 변환은 '인코딩'으로 뭉뚱그려 표현합니다. 하지만 굳이 엄밀하게 한 번 따져 보자면 각각 '팟 트랜스코더', '곰 트랜스코더'라고 해야 맞지 않을까 생각합니다. 물론 저도 인코더 쪽의 어감이 더 좋다고 생각합니다. :-)

보통은 트랜스코딩은 어느 정도 손실이 있는 동영상을 좀 더 손실을 주어 더 적은 용량으로 만들 때 씁니다. 한 번 손실을 주면, 손실된 부분은 다시 원래대로는 복구하기 어렵기 때문에 그 반대로는 잘 하지 않지요. 일부러 하면 '용량 뻥튀기'가 될 뿐 쓸 데 없는 짓이 됩니다.

보통 압축 알고리즘은 많은 옵션들을 가지고 있습니다. 그 세부적인 옵션 값의 변화에 따라 압축률이 천차만별로 달라지는 경우도 있습니다. 특히 동영상 압축의 경우 영상과 음성 두 가지에 대해 작업을 해야 하므로 영상 압축 알고리즘, 음성 압축 알고리즘에 대해 잘 알고 있어야 합니다. 당연히 알고리즘을 이해하기 위해서는 각각에 대한 기반 지식도 충분히 갖추어야만 합니다.

일례로 인터넷에서 돌아다니는 동영상 중 어떤 것들은 적은 용량임에도 상당히 화질이 우수합니다. 요즘 편리한 동영상 변환 툴이 많으니, 같은 영상에 대해 비슷한 과정을 거쳐 누구나 간편하게 비슷한 결과물을 뚝딱뚝딱 찍어낼 수 있을 겁니다. 하지만 세부적인 옵션을 잘 알고 세세하게 과정을 조정하는 것과 그냥 틀에 맞춰 찍어내는 것의 결과물에는 엄청난 차이가 있음은 불 보듯 뻔합니다.

HLS (HTTP Live Streaming)

인터넷에서 동영상 재생(스트리밍)을 위해 'mms://' 라든지 'rtsp://' 등으로 시작하는 주소를 본 적이 있으신지 모르겠습니다. 멀티미디어 스트리밍을 위해 존재하는 프로토콜입니다. 이것들을 서비스하기 위해서는 별도의 서버가 작동해야 합니다. 그러나 HLS는 웹 서버에서 동영상을 웹 페이지를 다운로드 받는 것과 동일한 방식을 취합니다. 웹 서버에서 바로 스트리밍이 가능하므로 스트리밍 서버를 따로 구축할 필요가 없으며, 웹서버 내부에서도 스트리밍을 위해 기존의 설정을 굳이 변경하지 않고서도 바로 서비스가 가능합니다. 게다가 웹 서버를 위한 80 포트는 거의 열려 있는 경우가 대부분이므로 방화벽 보안 정책에서도 상당히 유리합니다. HLS는 원래 애플이 퀵타임과 iOS를 위해 만든 것이지만 최근에는 안드로이드도 지원합니다4). 그러므로 대개의 스마트폰에서 별 무리 없이 HLS로 전달되는 동영상을 감상할 수 있을 것입니다.

'서버에 저장된 멀티미디어 파일을 웹페이지나 첨부 파일 다운로드하듯 전송 받아 기기에서 바로 재생한다.' 이것이 HLS의 기본 개념이라고 생각하시면 됩니다. 한 블로거 분이 HLS에 대해 매우 잘 정리해 놓았습니다. 포스트는 여기서 확인하실 수 있습니다. 더욱 자세한 사항은 애플의 HTTP Live Streaming Overview 문서를 참고하면 되겠습니다.

HLS의 핵심 과정만 간결하게 나열하면 다음과 같습니다.

  1. 서버는 멀티미디어 파일을 트랜스코딩해 .ts (MPEG-2 Transport Streams)파일로 변환한다.
  2. 서버는 변환된 파일을 여러 개의 작은 파일로 쪼갠다.
  3. 서버는 잘게 쪼개진 파일의 목록을 .m3u8이라는 재생 목록 형식으로 기록한다.
  4. 클라이언트는 웹페이지 등을 통해 .m3u8 파일에 접근한다.
  5. 클라이언트는 재생 목록을 이용해 서버의 동영상 파일에 접근한다.
  6. 클라이언트는 파일을 다운로드 받아 재생한다.

M3U8 포맷

M3U8 포맷에 대한 자세한 문서를 읽어 보면 여러 기능이 많이 있습니다. 그렇지만 여기서는 기초적인 구동에 필요한 몇 가지 명령만 알아보고 넘어가도록 하겠습니다.

#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0

#EXTINF:10,
http://media.example.com/segment0.ts

#EXTINF:10,
http://media.example.com/segment1.ts

#EXTINF:10,
http://media.example.com/segment2.ts

#EXT-X-ENDLIST
태그 의미
#EXTM3U 이 파일이 M3U8 파일임을 명시하는 시작 태그입니다.
#EXT-X-TARGETDURATION각 미디어 부분 파일 중 가장 긴 재생 시간을 기록하기 위한 태그입니다. 초로 기록합니다.
#EXT-X-MEDIA-SEQUENCE재생되는 가장 처음 조각을 의미합니다. 위 예제에서 시퀀스 처음은 segments0.ts 입니다. 확장자를 제외한 접미사의 숫자는 0이므로 이 값은 0입니다. 만일 첫번째 파일의 시퀀스 번호가 2부터 시작한다면 이것은 2가 되어야 합니다. 그러나 제약사항은 아닌 듯합니다. 위 파일에서 0을 2로 바꾸어도 처음 재생되는 파일은 segment0.ts로 변하지 않습니다.
#EXTINF각 세그먼트의 재생 시간 및 재생 정보를 기록합니다.
#EXT-X-ENDLIST태그의 끝을 의미한다. 이 이후는 무시됩니다.

FFmpeg

FFmpeg은 아주아주 유명합니다. 인코딩을 한 번쯤 해 보셨다면 지나쳤을 단어일 겁니다. 멀티미디어 데이터 조작을 위한 프로그램이며 수많은 동영상 코덱을 처리할 수 있으며 기능도 매우 다양합니다. 단지 (공대 감성이 충만한) 개발자들을 위한 물건이라 상당히 불친절합니다. 잘 쓰려면 문서를 많이 참고해야 하고 꽤 많은 시간과 노력을 기울여야 합니다. 그러므로 초심자나 일반인이 쓰기에는… 어쩌면 엄청난 무리가 있을 겁니다. 하지만 검색을 잘 해보면 나름 충분히 많은 자료가 공개되어 있으며 이를 스크립트로 활용하면 정말 대단해집니다. 잘만 하면 왠만한 인코딩(트랜스코딩) 프로그램 쯤은 쌈싸먹을 수 있죠.

마우스 몇 번으로 간편한 작업을 할 수 있지만 기능상으로는 뭔가 하나씩은 아쉬운 GUI 기반의 무료 프로그램들이 많죠? 조금 나쁘게 말해 보자면 때론 뭔가 유용한 기능은 '프로페셔널 판 기능'이니 돈을 내라고 낚시를 하기도 합니다. 또는 결과물에 보기 싫은 워터마크를 떡하니 박아 놓고서는 이것을 없애려면 유료 버전을 사용해야 한다면서 인질극(?)을 펼칩니다. 그런 걸 무조건 나쁘다고 볼 수는 없지만 쓰는 사람은 참 감질납니다. 하지만 FFmpeg은 검색과 문서의 늪을 미친 듯 헤매야 적어도 이런 일은 없습니다.

FFmpeg은 크게 프로젝트 전체를 일컫고, 소프트웨어 기능에 따라 몇 개로 분리하여 배포됩니다5).

  • ffmpeg: 프로젝트 이름과 같은, 멀티미디어 데이터 조작을 담당한다.
  • ffplay: 비디오 파일의 재생을 담당한다.
  • ffprobe: 미디어 파일의 정보를 파악할 때 사용한다.
  • ffserver: 동영상을 네트워크로 스트리밍(streaming)할 때 사용한다. 윈도우에서는 바로 사용할 수 없어 binary로 배포되는 FFmpeg 빌드에는 제외되어 있다.

레시피

FFmpeg은 조사한 자료를 기록하는 차원에서 소개만 하고 넘어가겠습니다. 직접적으로 본 문서에서 다룰 프로그램은 다음에 소개할 VLC입니다. 그래도 그냥 넘어가는 건 아쉬우니 FFmpeg으로 할 수 있는 여러 작업 중 몇가지 것들만 골라 레시피로 소개합니다.

영상 파일의 한 장면을 그림 파일로 추출
ffmpeg -ss <시간> -i <파일 이름> -f image2 -s <그림 가로 크기>x<그림 세로 크기> -vframes <추출할 프레임 수> -v codec <변환 코덱> <출력 파일>
  • 시간: HH:MM:SS(.SSS) 로 입력한다.
  • 파일 이름: 입력할 영상 파일.
  • 그림 가로, 세로 크기: 추출할 스크린샷의 가로 사이즈.
  • 추출할 프레임 수: 추출할 장면 수를 정한다.
  • 변환 코덱: 어떤 형식의 그림파일로 만들지 지정한다. '.jpg'면 mjpeg, '.png' 면 png라고 입력한다.
  • 출력파일: 출력 파일의 이름을 지정한다. 스크린샷이 여러 개일 경우 '%d'와 같이 숫자를 지정할 수 있다.

예제

ffmpeg -ss 00:05:00 -i test.mp4 -f image2 -s 320x180 -vframes 1 -v codec png sreenshot.png
ffmpeg -ss 00:05:00 -i test.mp4 -f image2 -s 320x180 -vframes 10 -v codec png sreenshot_%02d.png

첫번째 예는 1 프레임만 추출. 두번째 예는 10프레임을 screenshot_01.png, screenshot_02.png, … 로 추출한다.

영상 파일의 일부분만을 잘라냄
ffmpeg -ss <시작시간> -t <재생시간> -i <입력파일> -acodec copy -vcodec copy <출력파일>
  • 시작시간: 00:00:00 과 같이 입력
  • 재생시간: 시작 시간부터 잘라낼 시간. 시작 시간과 마찬가지로 입력
기타

가로, 세로의 크기를 직접적으로 명시하는 것이 아닌 비율로 지정하고 싶을 때는 -s 옵션을 삭제하고 -vf scale 옵션을 사용합니다.

  -vf scale=iw/2:-1  --> 동영상의 가로 사이즈는 1/2로 줄이고 세로는 가로에 따라 자동으로 조절됩니다.
  -vf scale=-1:ih/2  --> 원래 동영상 세로 사이즈의 1/2로로 줄이며 가로는 세로에 따라 조절한다.

기존의 파일을 무조건 덮어쓰고 싶을 때는 -y 옵션을 더하면 됩니다. 기존의 파일을 덮어쓸지 물어보는 과정이 사라집니다. 또한 ffmpeg의 잡다한 메시지를 출력하지 않게 하려면 -v quiet를 더하면 됩니다.

자막 파일 또한 자기 나름대로의 데이터를 기록하는 규칙을 가지고 있으므로, 자막 파일을 기록하는 것을 인코딩, 읽어내는 일을 디코딩이라고 표현할 수 있습니다. ffmpeg은 SAMI (.smi), SubRip (.srt), ASS (.ass) 자막 디코딩도 지원합니다. SubRip과 ASS는 인코딩도 지원하므로, ffmpeg을 이용해 .smi 파일을 .srt/.ass 파일로도 변경 가능합니다. 단 SAMI에 대한 인코딩은 지원하지 않으므로 역으로는 불가능합니다.

ffmpeg -i input.smi output.srt
ffprobe를 사용한 동영상/음악 파일의 정보 분석
ffprobe -v quiet -show_format -show_streams -of <출력 양식> -i <입력 파일>
  • 출력 양식: default, ini, csv, xml 을 사용할 수 있다.
  • 입력 파일: 말 그대로 입력할 파일.

음악, 영상 등 멀티미디어 파일의 경우 어떤 출력을 보이며, 아닌 경우는 에러를 냅니다. 그러나 단순 텍스트 파일이며, .txt 확장자를 가진 파일에 대해서는 다음과 같은 출력을 보입니다.

Input #0, tty, from '[파일명]':
  Duration: 00:00:00.04, bitrate: 5 kb/s
    Stream #0:0: Video: ansi, pal8, 640x400, 25 tbr, 25 tbn, 25 tbc

조사한 바를 기록하기 위해 ffprobe의 레시피도 기록했습니다만, 단순한 동영상 정보 확인을 위해서라면 저는 ffprobe보다는 MediaInfo를 권장합니다. 다양한 OS에서 사용 가능한 GUI 툴입니다.

VLC Media Player

FFmpeg만큼이나 유명한 동영상 재생기로 VLC media player가 있습니다. 국내에는 사사미2K 이후로 Swan's MP아드레날린 등이 꾸준히 개발되었고, 현재는 곰플레이어, KMP, 다음 팟플레이어 등등 강력하면서 개성있는 프로그램들이 많아 VLC는 일반인들에게 거의 '듣보잡' 취급을 받습니다만, 기본적인 능력만으로는 최정상급인 오픈 소스 프로그램입니다. 종종 mplayer와 비교되곤 하지요. 이 쪽도 사용법은 쉽지 않지만 멀티미디어 데이터에 있어서는 '전능하다'고까지 부르는 프로그램입니다.

FFMpeg이 동영상 변환을 위해 ffmpeg을, 동영상 재생을 위해 ffplay를 각각 제공하는 반면 VLC는 이 둘을 하나의 프로그램이 처리합니다. GUI가 있지만 mplayer처럼 VLC도 CLI를 지원합니다. 말하자면 VLC는 동영상 변환기로도, 동영상 재생기로도 쓸 수 있고, 마우스를 이용해 쉽고 간편하게 이용할 수도 있으며 스크립트를 이용한 단순 반복 작업 또한 가능한 다재다능한 녀석입니다.

VLC를 이용한 동영상 파일의 HTTP Live Streaming

먼저 외부에서 바라본 VLC라는 프로그램의 내부 모습에 대해 먼저 언급하겠습니다. VLC는 수많은 세부 모듈(module)들을 뭉뚱그려놓은 집합체입니다. 수많은 기능들이 '모듈', 즉 하나의 부속품처럼 구성되어 있으며 VLC는 이 모듈을 알맞게 조합하여 요청한 작업을 수행하게 됩니다. 예를 들어 VLC의 껍데기를 담당하는 UI는 UI 모듈로써 독립적으로 구성되어 있습니다. VLC의 코어와는 독립되어 있기 때문에 다양한 UI를 구성할 수 있습니다. 실제로 VLC는 데스크탑을 위한 UI, 웹을 위한 UI, 명령 프롬프트을 위한 UI, 심지어 텔넷을 통한 UI도 가지고 있습니다.

또한 동영상 처리 과정에서 있을 수 있는 여러 처리 과정 또한 모듈화 되어 있습니다. 입력 프로세스와 출력 프로세스 또한 모듈화되어 있고, 어떤 모듈을 쓰느냐에 따라 여러 다른 결과를 가져옵니다. VLC는 모듈을 기반으로 작성되어 있으므로 융통성이 아주 높습니다. 모듈을 어떻게 조합하느냐에 따라 VLC는 동영상 재생기로도, 트랜스코더 및 스트리밍 서버로도 응용할 수 있습니다.

사용자는 어떤 모듈을 사용할지, 그리고 모듈을 어떻게 동작시킬지를 인터페이스를 통해 제어합니다. GUI를 통해 직관적이고 편리한 방법으로 명령을 내릴 수 있고, CLI를 통해 조금은 복잡하지만 훨씬 간결하고 자동화된 방법으로 명령을 내릴 수 있습니다. 문서에서는 GUI를 사용하지 않고 전부 CLI를 사용합니다. 그러므로 사용자의 명령은 모두 VLC에게 전달하는 인자 목록 형태로 전달됩니다.

옵션 지정하기

인자 목록으로는 옵션과 파일의 경로를 입력할 수 있습니다. 파일의 경로는 디스크의 파일 경로, URL 등이 될 수 있습니다. 옵션은 두 종류가 있는데 하나는 옵션의 이름, 다른 하나는 옵션의 값입니다. 옵션이 이름은 두 개의 하이픈(--)을 이용한 긴 이름과 하나의 하이픈(-)을 이용한 짧은 이름 두 종류가 있습니다. 어떤 명령은 두 종류를 다 가지는 경우도 있고, 어떤 것은 긴 이름의 옵션만 지원합니다. 이 문서에서는 긴 이름의 옵션만을 사용하겠습니다. 옵션의 값은 해당 옵션이 특정한 값을 필요로 할 때 주어집니다. 당연히 모든 옵션은 VLC가 지정한 문법에 맞게 주어져야 합니다.

가장 간단한 예로, CLI에서 VLC로 파일을 재생하는 명령을 적어 보겠습니다.

vlc --intf=dummy example_video.avi

옵션으로 --intf=dummy, 파일의 경로는 example_video.avi가 주어졌습니다. --intf=dummy 옵션의 이름은 --intf, 값은 dummy 입니다. 파일의 경로는 상대 경로로, 현재 디렉토리의 example_video.avi를 재생합니다. 차후에서도 설명하겠지만, --intf 옵션은 VLC의 UI 모듈 변경을 지시하는 명령입니다.

가장 간단한 명령으로 VLC에게 어떻게 명령을 내리는지에 대해 간단히 살펴보았습니다. VLC의 기능이 방대한 만큼 수많은 옵션들이 있습니다. 그러나 본 문서는 원하는 목표에 맞는 모듈들의 해당 옵션에만 집중할 것입니다. 옵션을 잘 알면 알수록 세세한 제어가 가능하지만, 옵션이 점점 쌓이면 인자의 형태는 마치 프로그래밍 구문처럼 복잡해집니다.

기본 사용법

GUI를 통해 트랜스코딩 및 스트리밍도 가능하지만, 본 문서에서 GUI 활용법을 다루는 것은 생략하고 철저하게 '더미(dummy) 인터페이스'를 이용해 CLI 만을 이용하도록 하겠습니다. VLC는 어디서든 사용할 수 있도록 패스를 걸어두세요.
/*여담이지만 환경변수를 편집하는 프로그램으로 Rapid Environment Editor를 추천합니다. 제가 한국어 번역을 했습니다… 부끄러워라… ;-) */

VLC를 실행하면 기본값으로 GUI 창이 생성됩니다. 윈도우 기반의 인터페이스가 아닌 인터페이스도 작동시킬 수 있는데, 기본적으로 rc, ncurses, telnet이 있습니다. rc는 VLC가 별도로 생성한 콘솔 창에서 명령을 수행하며, ncursescurses 기반의 인터페이스를 보여 줍니다. telnet은 telnet을 통해 VLC에 원격으로 접속할 수 있도록 해 줍니다6).

앞서 언급한 더미 인터페이스는 rc와 유사한 창이 생성되지만 미리 명령 구문을 통해 입력된 내용만을 실행하는 점이 다릅니다. --intf=dummy 옵션을 붙이면 됩니다.

vlc --intf=dummy

이렇게 하면 CLI 창이 생성되고 사라지지 않습니다. 더미 인터페이스가 지정한 작업을 수행한 후 자동으로 닫히게 하려면 뒤에 항상 VLC://quit을 입력해 주면 됩니다. 즉 vlc는 여러 파일의 목록을 입력 받을 수 있는데 VLC://quit은 (여기까지) 재생이 완료되면 VLC를 종료하라는 일종의 매직 워드인 셈입니다.

vlc --intf=dummy VLC://quit

이제는 더미 인터페이스가 생성된 후 바로 사라지는 것을 확인할 수 있습니다. 이 외에도 VLC는 많은 인터페이스를 지원합니다. 이 곳에서 목록을 확인해 보세요

트랜스코딩을 위한 옵션 설명

트랜스코딩에 대한 전형적인 옵션은 다음과 같습니다7)

vlc --sout "#transcode{[TRANSCODE_OPTIONS]}:std{[OUTPUT_OPTIONS]}" INPUT

구문은 크게 세 덩어리로 나눌 수 있습니다.

  1. vlc
    vlc 실행 파일입니다.
  2. INPUT
    입력 파일. 보통 입력된 파일은 영상 및 음성 데이터를 가지고 있습니다. 영상 및 음성 데이터는 시간 순으로 일렬로 죽 나열될 수 있습니다. 이러한 형태의 데이터들을 스트림(stream)이라고 합니다. 입력 파일은 VLC 내부의 입력 처리 모듈들에 의해 입력 스트림이 됩니다.
  3. --sout “…“
    입력 스트림은 보통은 감상을 위해 모니터와 사운드 카드를 통해 사용자에게 전달됩니다. 그러나 트랜스코딩 같은 경우 영상과 소리가 출력될 필요는 없습니다. 그러므로 출력되는 대상은 모니터와 사운드 카드가 아닌 또다른 파일이 되어야 합니다. 예컨데 물의 흐름을 변경하는 것처럼, 입력 데이터의 흐름을 사용자가 원하는 방향으로 흐르도록 조절해야 합니다. 이 옵션은 그러한 '출력 스트림'의 방향을 지정해주는 옵션입니다.

따옴표 내부의 구문에 대해서 조금 더 설명드리겟습니다.

  • '#transcode'
    transcode 모듈을 불러오는 역할을 합니다. 모듈의 세부 옵션을 설정하기 위하여 transcode 바로 다음에 괄호 '{' 와 '}'를 삽입하고, 괄호 사이에 옵션을 열거합니다.
  • standerd (std) 모듈
    stdstandard 모듈의 별명(alias)입니다. 입력 스트림을 다른 파일, 네트워크에 먹싱(muxing) 하여 보내는 역할을 맡습니다. 먹싱(muxing)이란 '멀티플렉싱(multiplexing)'의 준말이며 여러 신호를 하나에 담는다는 의미를 가지고 있습니다. 쉽게 말해 동영상 파일은 영상 정보와 음성 정보 두 가지를 가지고 있습니다. 하지만 이를 보통 영상 파일과 음성 파일로 각각 보관하지는 않고 하나의 파일로 보관합니다. 영상과 음성 데이터를 하나의 파일로 만드는 것을 먹싱이라고 합니다. 앞에 콜론(:)이 붙은 것은 트랜스코딩을 거쳐 나온 출력 스트림을 standard 모듈이 이어 받아 처리를 계속한다는 의미입니다.
  • 이외에도 duplicate, display 모듈 등이 있습니다8).

간단하게 MPEG-1 동영상 파일을 하나 열어 동영상의 해상도를 반으로 줄이는 예를 들어 보겠습니다. 옵션을 읽어 보면 크게 어려운 바는 없습니다.

vlc.exe --intf=dummy --sout=#transcode{vcodec=mp1v,scale=0.5}:standard{access=file,dst=output.mpg} input.mpg VLC://quit

다음은 MediaInfo로 조사한 입력 파일과 출력 파일의 정보입니다.

입력 파일

General
Complete name                            : C:\Users\changwoo\input.mpg
Format                                   : MPEG-PS
File size                                : 2.51 MiB
Duration                                 : 14s 928ms
Overall bit rate                         : 1 411 Kbps
Writing library                          : encoded by TMPGEnc b12j

Video
ID                                       : 224 (0xE0)
Format                                   : MPEG Video
Format version                           : Version 1
Format settings, BVOP                    : Yes
Format settings, Matrix                  : Custom
Format settings, GOP                     : M=3, N=18
Duration                                 : 14s 915ms
Bit rate                                 : 1 191 Kbps
Maximum bit rate                         : 1 150 Kbps
Width                                    : 352 pixels
Height                                   : 240 pixels
Display aspect ratio                     : 4:3
Frame rate                               : 29.970 fps
Standard                                 : NTSC
Color space                              : YUV
Bit depth                                : 8 bits
Scan type                                : Progressive
Compression mode                         : Lossy
Bits/(Pixel*Frame)                       : 0.470
Stream size                              : 2.12 MiB (84%)
Alignment                                : encoded by TMPGEnc b12j
Title                                    : TMPGEnc
Writing application                      : b12j
Writing library                          : TMPGEnc b12j

Audio
ID                                       : 192 (0xC0)
Format                                   : MPEG Audio
Format version                           : Version 1
Format profile                           : Layer 2
Duration                                 : 14s 928ms
Bit rate mode                            : Constant
Bit rate                                 : 192 Kbps
Channel(s)                               : 2 channels
Sampling rate                            : 48.0 KHz
Compression mode                         : Lossy
Stream size                              : 350 KiB (14%)

출력 파일

General
Complete name                            : C:\Users\changwoo\output.mpg
Format                                   : MPEG-PS
File size                                : 683 KiB
Duration                                 : 14s 848ms
Overall bit rate mode                    : Variable
Overall bit rate                         : 377 Kbps

Video
ID                                       : 224 (0xE0)
Format                                   : MPEG Video
Format version                           : Version 2
Format profile                           : Main@Main
Format settings, BVOP                    : No
Format settings, Matrix                  : Default
Format settings, GOP                     : Variable
Duration                                 : 14s 848ms
Bit rate mode                            : Variable
Bit rate                                 : 178 Kbps
Width                                    : 176 pixels
Height                                   : 120 pixels
Display aspect ratio                     : 4:3
Frame rate                               : 29.970 fps
Color space                              : YUV
Chroma subsampling                       : 4:2:0
Bit depth                                : 8 bits
Scan type                                : Progressive
Compression mode                         : Lossy
Bits/(Pixel*Frame)                       : 0.281
Stream size                              : 322 KiB (47%)

Audio
ID                                       : 192 (0xC0)
Format                                   : MPEG Audio
Format version                           : Version 1
Format profile                           : Layer 2
Duration                                 : 14s 832ms
Bit rate mode                            : Constant
Bit rate                                 : 192 Kbps
Channel(s)                               : 2 channels
Sampling rate                            : 48.0 KHz
Compression mode                         : Lossy
Stream size                              : 348 KiB (51%)

트랜스코드 모듈의 scale=0.5라는 옵션이 동영상의 가로, 세로 길이가 절반으로 줄여주는 것임을 직관적으로 알 수 있습니다. 위 명령은 동영상의 해상도만을 조정하기 위한 거의 최소한의 명령입니다. vcodec=mp1v로 비디오 코덱을 mp1v로 지정해 준 것 외에는 나머지는 거의 건드리지 않았습니다. 여기서 vcodec=mp1v을 제거하면 동영상의 크기가 변경되지 않습니다(왜 그럴까요?). 지정되지 않은 항목은 필수적인 요소가 아니라면 VLC가 알아서 기본값으로 처리합니다.

결과를 보면 음성 정보는 거의 변하지 않은 것을 알 수 있습니다. 반면 영상 정보를 보면 꽤 많은 데이터가 변해 있음을 볼 수 있습니다. 왜 이런 일이 일어났을까요? 영상의 해상도만 조절했을 뿐인데? 나머지는 똑같이 하고 싶은데 말이죠… 하지만 그렇게 하기는 쉽지 않을 것 같습니다. 제가 생각한 바는 다음과 같습니다.

  1. VLC의 MPEG-1 인코더와 예전의 TMPEGEnc 인코더는 완전히 다른 인코더입니다. 게다가 입력 동영상은 2001년도에 나온 TMPEGEnc Beta12j 버전을 이용해 인코딩 되었다고 코멘트되어 있습니다. 10년도 더 된 구닥다리 인코더의 결과물입니다.

  2. 영상에 대해서는 우리가 '해상도 변경'을 지시하였으므로 VLC는 디코딩된 스트림을 이용해 인코딩을 시도할 것입니다.
    • 핵심 포인트는 바로 '디코딩된 스트림을 다시 인코딩한다'는 것입니다. 앞서 서술한 바와 같이 인코더와 디코더는 동전의 양면과 같은 존재입니다. 디코딩 없이 인코딩은 없습니다. 디코딩을 하지 않은 데이터에 인코딩을 해 봐야 의미가 없습니다. 즉, 최초의 원본 동영상을 인코딩할 때(이 결과로 input.mpg가 나왔겠죠)의 인코더 및 인코딩 옵션을 알 수 없는 이상, 해상도 항목만 따로 쏙 빼낸 후 해상도만 깔끔하게 변경하기는 사실상 불가능하다는 이야기입니다.
    • 입력 파일의 디코딩이 일어난 후에 인코딩이 일어날 것이므로, 입력 스트림이 인코더로 향하는 시점에서 이미 입력 스트림은 입력 파일의 인코딩 형식과는 결별된 상태입니다.
    • 동영상을 기록할 때 세세하게 옵션을 주지 않았으므로 출력은 VLC가 사용하는 MPEG 코덱의 기본값을 따릅니다.
    • 세월이 흘러 코덱도 발전되었을 것입니다. 아무래도 VLC가 더 개선된 버전의 코덱을 쓸 것이라 생각합니다. 확실히 입력 파일은 'MPEG Video Version 1', 출력 파일은 'MPEG VIdeo Version 2'로 차이가 있습니다.

  3. 반면 아마 음성에 대해서는 별다른 옵션을 지정해주지 않았기 때문에 효율적인 처리를 위해 데이터를 그냥 복사했을 가능성이 있습니다. 그러므로 음성 데이터의 인코딩 정보는 거의 변함이 없습니다.

HLS을 위한 옵션

다행히 VLC는 HLS를 바로 사용할 수 있도록 모듈을 제공하고 있습니다9). 이것을 이용하면 별로 어렵지 않게 HLS를 구현할 수 있습니다. 입력 동영상은 'input.mpg'으로 가정합니다. 그리고 서버는 '192.168.0.200'이란 IP 주소에서 작동 중이며, 서버의 동영상 및 m3u8의 실제 경로는 'D:/html/stream'에 있다고 가정합니다.

vlc input.mpg --intf=dummy --mms-caching 0 --sout=#transcode{vcodec=h264,venc=x264{aud,profile=baseline,level=30,keyint=30,no-cabac},acodec=mp4a,ab=48,channels=2,samplerate=44100,scodec,soverlay}:std{access=livehttp{seglen=10,delsegs=true,index=D:/html/stream/out.m3u8,index-url=http://192.168.0.200/stream/out_######.ts},mux=ts{use-key-frames},dst=D:/html/stream/out_######.ts} VLC://quit

이번 문서는 결국 이 한 줄의 명령어를 위해 만들어진 것과 진배없습니다. 명령줄에 사용된 키워드를 하나씩 설명하도록 하겠습니다.

키워드 설명
mms-caching MMS의 캐시를 설정합니다. 1/1000초 단위로 정수를 입력하면 되는데, MMS를 사용하지 않으므로 캐시를 0으로 만들었습니다.
vcodec=h264 변환될 동영상의 코덱을 설정합니다. H.264를 선택하면 무난합니다.
venc=x264 인코딩을 위해 어떤 인코더를 사용할지 결정합니다. X264면 무난합니다.
aud access unit delimiter를 지정합니다.
profile=baseline 코덱 자체의 규약입니다. H.264의 Profiles, Levels 항목을 참고하기 바랍니다.
level=30
keyint=30 키프레임 사이 가능한 최대의 프레임 수.
bframes 생략되었지만 이 옵션을 사용하면 B-Frame을 삽입할 수 있습니다. 기본값은 3입니다10).
no-cabac CABAC(Context Adaptive Binary Arithmetic Coder) 알고리즘을 사용하지 않습니다. CABAC을 사용하면 퀄리티를 향상시키나 느립니다.
acodec=mp4a 오디오 코덱의 종류를 선택합니다. mp4a면 무난합니다.
ab 오디오의 비트레이트를 설정합니다. 적절히 선택합니다.
channels 2채널 스테레오를 선택하면 트랜스코딩으로 무난합니다.
samplerate 샘플링 레이트를 지정합니다.
scodec 자막 코덱을 지정합니다. 값을 지정하지 않아도 자동 인식합니다. 동영상 파일 이름과 동일한 자막 파일이 있다면 VLC가 알아서 처리합니다.
soverlay 동영상에 자막을 입힙니다.
access=livehttp HLS를 위한 출력을 만듭니다.
seglen .ts 파일 세그먼트의 최대 길이를 지정합니다. 초 단위로 지정합니다.
numsegs .m3u8 파일 내부에 유지되는 세그먼트 개수를 지정합니다. 0이면 영상의 처음부터 끝까지 세그먼트를 모두 유지합니다. 기본은 0입니다.
delsegs세그먼트가 필요하지 않으면 삭제합니다. numsegs가 0이면 무시됩니다.
index 재생목록인 .m3u8 파일이 기록될 경로를 지정합니다. 이것은 서버가 내부에서 참고하는 값입니다. 물론 클라이언트가 접근 가능한 경로에 위치해야 합니다.
index-url.m3u8 파일 내부에 기록된 세그먼트의 URL 주소입니다. 클라이언트가 접근 가능한 주소로 기록되어야 합니다. '#' 기호는 VLC이 알아서 세그먼트들의 번호를 붙여주도록 하기 위해 붙입니다. 기호의 갯수만큼 자릿수가 만들어집니다.
ts 먹싱을 .ts 파일로 지정합니다.
use-key-frames'mux='의 ts 아이템 옵션을 참고하기 바랍니다.
dst.ts 파일 세그먼트들이 저장되는 경로입니다. VLC를 작동시키는 서버가 접근하는 내부 경로입니다.

스트리밍된 파일 확인하기

웹서버를 구동하여 클라이언트가 .m3u8 파일 및 .ts 파일에 접근할 수 있기만 하면 됩니다. 미리 웹서버를 운영중이시라면 원하는 경로에 파일을 두어 확인하면 됩니다. 서버는 없지만 파이썬은 사용중이신가요? 파이썬이 자체적으로 구비한 웹서버를 사용하면 됩니다.

python -m SimpleHTTPServer

얼렁뚱땅이지만 이 한 줄로도 웹서버를 구동시킬 수 있습니다. 모바일 장치로 서버에 접근(8000번 포트)해 웹서버가 작동중인지 확인해 보세요. 이것은 아주 단순한 서버이므로 테스트로만 사용하시고, 재생 시간은 가급적 짧은 영상을 이용하셔야 할 겁니다. 그리고 웹서버의 경로와 .m3u8 파일 내부의 .ts 파일의 경로가 올바른지 꼭 확인하세요. 만일 .ts 파일의 경로가 클라이언트에서 접근 불가능한 경우 동영상이 재생되지 않습니다.

/* 문서 주석화 시작

각 단계별 상세한 소개

클라이언트 요청, 서버 응답 단계

Apache나 최근 블로그를 위해 설치한 nginx등의 웹서버를 기반으로 동작하면, 뽀대도 나고 좋지만, 문서를 위해서는 그다지 좋은 설정이 아니라고 판단한다. 물론 나만을 위한 문서라면 별 문제 없겠지만 웹서버들의 크기가 큰지라 트러블 슈팅이 쉽지 않다. 그냥 아주 간단한 파이썬 웹서버 스크립트를 통해 간단한 기능만 제안하도록 한다. (이것은 혹여 프로그램의 크기가 멋대로 커져버리는 것을 방지하는 역할을 하기도 할 것이다.

서버는 간단하게 클라이언트로부터 세 가지의 요청만 받을 것이다.

  1. 서버가 제공하는 동영상(혹은 음악)의 목록을 클라이언트로 전달하는 것.
  2. 동영상(혹은 음악)의 트랜스코딩 시작 요청을 받음.
  3. 트랜스코딩 된 파일의 제거.

프로그램의 단순함을 위해 클라이언트에서 동작하는 프로그램에서 동적인 요소는 완전히 배제하고, 다음과 같이 세 가지의 주소 요청만으로 서버의 동작을 제어한다고 하자. 각 번호의 항목은 서버의 응답과 대구를 이룬다.

  1. 목록 요청을 위한 URL.
  2. 동영상 트랜스코딩 요청 및 재생 시작.
  3. 트랜스코딩된 파일의 제거 요청.

서버의 트랜스코딩

서버의 트랜스코딩은 파이썬 이나 다른 언어로 만들어진 라이브러리를 이용해 멋들어진 (혹은 장황한) 코딩을 하는 것이 아니라 잘 만들어진 CLI 프로그램을 제어하는 것으로 그 초라함을 과시한다. … 라기 보다는 그렇게 할 수도 있겠지만 그렇게 한다면 문서를 쓰는 쪽도, 보는 쪽도 넉다운이 될 것이다. 필요한 개념만을 소개하고 그런 어려운 것은 나중에 관심이 있다면 따로 알아보는 것이 좋겠다.

성능 보안 등등의 골치 아픈 문제도 산뜻하게 집어치우도록 하겠다. 임베디드 서버에서는 성능 문제가 중요한 문제가 될 수 있지만 그쪽에서 쓸만한 성능을 가질 수 있도록 내가 일일이 프로그램한다는 것도 어렵다. 만약 내가 사용할 VLC나 FFMPEG등의 프로그램이 해당 플랫폼에서 실행될 수 있고, 꽤 좋은 성능을 발휘할 수 있도록 되어 있다면 아마 사용할 수는 있을 것이다만 나는 이 문서를 보고 사용하는 대상을 일반 PC, 윈도우 OS 기반의 파이썬 프로그램 학습자로 가정하고 문서를 작성할 것이다.

트랜스코딩의 전문가들이 보면 어이없는 것인지는 모르겠지만 ffmpeg이나 vlc를 이용해 트랜스코딩, 스트리밍을 할 것이다. 정확히 말해 트랜스코딩와 스트리밍에 이용하는 것은 vlc이고 ffmpeg은 양념으로 사용할 것이다. 이것은 취향의 문제가 아닌 본 엉터리 방법에서 어쩔 수 없는 이유이다. ffmpeg은 스트리밍을 위해 ffserver라는 프로그램을 이용하는데, 이 ffserver는 순수 윈도우 기반으로는 바이너리 파일을 만들 수 없고, 시그윈 기반에서 직접 사용자가 일일이 컴파일을 해 주어야 한다. ffserver 하나 쓰겠다고 시그윈까지 설치해서 컴파일하는 문서를 만들기는 너무 어렵다. 그냥 vlc를 쓰도록 하겠다. vlc도 무료고, 쓰기 나쁘지 않다.

http://localhost:8080/streamserver/list/

스트리밍

사실 이 부분은 vlc의 강력한 기능으로 말미암아 조잡한 일개 파이썬 코더인 내가 할 부분이 거의 없다. 나는 파이썬으로 vlc를 내가 원하는대로 동작시키는 스크립트만 작성하면 된다.

이렇게 보면 결국 파이썬이 하는 궁극적으로 수행하는 일은 단지 세가지 밖에 없다. 파이썬 프로그램이 자체가 수행하는 것은 디렉토리의 파일의 목록을 받아오고, 특정 파일과 디렉토리를 삭제하고, VLC를 제어하기 위한 명령 구문을 만들어내는 보조자 역할에만 불과하다. 최소한 파이썬을 이용한 CGI라도, 장고 맛이라도 봐야 하는 것이 아닐까 생각 많이 해 봤지만, 그냥 넘어가자. 본 문서는 파이썬 언어나 어떠한 기술에 대한 소개를 위함이지 어떤 라이브러리를 다루는 법을 소개하기 위해 쓴 것이 아님. 그런 것은 차후에 천천히 다루도록 하자. 본 문서는 아이디어를 빠르게 프로토타이핑해보기 위함이지, 절대 앱이나 시스템을 잘 만들기 위함이 아님을.

http://localhost:8080/streamserver/play/

트랜스코딩 파일 제거

Adaptive HLS

매우 흔하게 어떤 기술 명칭 앞에 'adaptive'나 'advanced' 란 형용사가 붙곤 합니다. '더욱 새로워진 ○○○!' 같은 상투적인 CM 문구 같은 느낌인데, 100% 확실하지는 않지만 제 나름대로는 둘을 이렇게 해석합니다. 'advanced'가 '진일보한' 이란 뜻이니 기술 앞에 'advanced'란 표현이 붙으면 기존 기술에 새로운 방법을 보태어 기술의 개선을 이끌어내었다는 의미가 큽니다. 반면 'adaptive'또한 기술의 개선을 뜻하는 말지만, 보통은 기술이 사용되는 환경에 보다 '최적화'되도록 기존의 기술을 튜닝시켰다는 의미가 큽니다. 'adaptive'는 '적응하는' 이란 뜻이니까요.

그렇다면 HLS의 어떤 점을 더 최적화시켰길래 'adaptive' 라는 표현을 사용할까요? 본 장에서는 'Adaptive HLS(이후 편의상 'AHLS'라고 쓰겠습니다)에 대해 간단히 짚어 보도록 하겠습니다.

Adaptive Bitrate Streaming

AHLS의 기본 개념은 'Adaptive Bitrate Streaming' (이후 'ABS'라고 쓰겠습니다.)이라는 미디어 스트리밍 분야 기술의 일종입니다. 여기도 'adaptive'라는 단어가 발견되는군요. Adaptive bitrate streaming의 핵심 개념을 요약해 보자면 다음과 같습니다.

  • 기존의 방법은 항상 영상의 품질은 고정되어 있다.
    • 다운로드 속도에 여유가 있으면 좀 더 나은 화질을 볼 수 있을 것이다.
    • 보통 아예 안 되는 것보다, 조금 나쁘더라도 되는 것이 낫다. 영상의 품질을 희생하더라도 보다 쾌적하게 재생이 되는 것이 더 중요하다.
  • 그렇다면 '사용자의 네트워크 속도'라는 환경 요소를 고려해 한 영상을 여러 조건에 맞추어 다양하게 인코딩한다.
    • 시청자가 좋은 인터넷 환경(빠른 다운로드 속도)에 있을 때는 보다 높은 품질의 동영상을 제공한다.
    • 다운로드 속도가 양호하지 못할 때는 낮은 품질로 재생한다.
    • 시청자의 가변적인 인터넷 속도 환경에 맞추어 서버도 환경에 시청자의 조건에 적합한 품질의 영상을 가변적으로 전송한다.

저는 머리에 딱 YouTube 플레이어의 톱니바퀴 부분이 떠오르던군요. 아래는 위키피디아11)12)에서 가져온 ABS의 얼개를 나타낸 그림입니다.

AHLS가 기존의 HLS의 공정에 비해 두드러지게 달라지는 부분은 인코더의 출력 파일이 하나가 아닌 여러 개의 출력이 동시에 나온다는 것입니다. AHLS의 경우 그림의 menifest file은 .m3u8 파일이 되겠습니다. 스마트폰으로 동영상을 스트리밍 받아 감상 중에 스마트 폰은 자신의 다운로드 속도가 얼마나 나오고 있는지 감지할 수 있습니다. 그 속도에 맞추어 .ts 파일을 가져올 때 속도에 적합한 인코딩 결과물을 골라 다운로드 받아서 재생을 이어갑니다. 네트워크 환경이 쾌적한 경우 높은 비트레이트의 영상을, 그렇지 못할 경우 적절한 비트레이트의 영상을 스트리밍합니다.

구현 방법

AHLS를 구현하려면 서버와 클라이언트 둘 다 좀 더 바쁘게 움직여야 합니다. 서버는 하나의 출력이 아닌 다수의 출력을 동시에 만들어야 하고, 스트리밍 영상을 재생하는 클라이언트도 속 편하게 영상만 재생하고 있으면 안 되고 자신의 네트워크 상태를 체크해서 현재 상황에 적절한 영상을 선택하는 작업을 해야 합니다.

트랜스코더의 출력 변경

HLS는 단 하나의 비트레이트로 하나의 .ts 세그먼트 파일 세트를 만들면 되지만 AHLS는 같은 내용의 다른 비트레이트를 가진 .ts 세그먼트를 여러 세트로 준비해야 합니다. 그러므로 실시간 스트리밍을 제공하려면 서버는 한 번에 다수의 출력을 만들 수 있는 성능 좋은 인코더를 준비해야 합니다.

HLS를 소개하며 사용한 VLC는 한 번에 다수의 인코딩 결과를 내놓을 수 있도록 잘 설계 되어 있지만, 2013년 2월 현재 AHSL는 지원하지 않는 듯합니다. 다만 비슷한 ABS 기술인 MPEG-DASH는 VLC 플러그-인이 나왔습니다. MPEG-DASH에 대해서는 따로 검색을 하시기 바랍니다.

M3U8 파일의 태그 내용 추가

애플의 기술 문서에서는 AHSL에 필요한 M3U8 플레이 리스트 파일을 가리켜 'variant playlist'라고 합니다. 다음은 variant playlist의 예제입니다.

#EXTM3U

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/low/index.m3u8

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/lo_mid/index.m3u8

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/hi_mid/index.m3u8

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/high/index.m3u8

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=64000,CODECS="mp4a.40.5"
http://example.com/audio/index.m3u8

EXT-X-STREAM-INF 태그는 다른 플레이 리스트를 참조할 때 사용합니다. 다음과 같은 속성들을 지정해 줍니다.

  • PROGRAM-ID
    정수로 표현하며 여러 다른 설정의 인코딩이 하나의 영상에서 나왔음을 표시하기 위해 사용합니다. 위 예제의 하위 플레이 리스트는 모두 5개이지만 5개 모두 같은 동영상을 인코딩한 것입니다.

  • BANDWIDTH
    BPS 단위로 값을 설정합니다. EXT-X-STREAM-INF 태그는 이 속성을 반드시 지정해야 합니다. 각 하위 플레이 리스트 동영상의 비트레이트 상한선(오버헤드 포함)을 기록하면 됩니다.

  • RESOLUTON
    동영상의 해상도를 기록하면 됩니다.

  • CODECS
    인코딩 및 프로파일 정보를 기록합니다. 이것은 RFC 6387에 정의되어 있습니다. 아래는 그 예입니다13). 항상 기록해 주어야 합니다.
    • AAC-LC: “mp4a.40.2”
    • HE-AAC: “mp4a.40.5”
    • MP3: “mp4a.40.34”
    • H.264 Baseline Profile level 3.0: “avc1.42001e” or “avc1.66.30”
    • H.264 Baseline Profile level 3.1: “avc1.42001f”
    • H.264 Main Profile level 3.0 : “avc1.4d001e” or “avc1.77.30”
    • H.264 Main Profile level 3.1 : “avc1.4d001f”
    • avc1.66.30avc1.77.30은 iOS 버전 3.0부터 3.12와의 호환을 위해 사용합니다.

실습해보기

VLC가 아직 AHLS를 지원하지 않아 조금 불편하긴 하지만 간단한 수준에서 실습을 해 보겠습니다. 간단한 수준이니 .ts와 .m3u8을 직접 손으로 만들어 보는 것도 그리 나쁘지 않은 것 같습니다.

우선 석 장의 아주 간단한 그림 파일을 만들었습니다.

hi.jpg mi.jpg lo.jpg

'H'가 써진 그림 파일은 640×480, 'M'와 'L'이 그려진 그림 파일은 480×360으로 만들었습니다. 그리고 적당히 일정한 톤의 소리를 만들었습니다. 각기 다른 3개의 10초의 WAV 파일을 생성했는데, 각각 높은 주파수, 중간 주파수, 낮은 주파수입니다. 'H'는 높은 주파수와, 'M'은 중간 주파수와, 'L'은 낮은 주파수와 같이 사용할 것입니다.
한편 WAV 파일을 lame을 이용해 mp3 파일로 변환시켰습니다. 기본이 128kbps 이므로 대략 1/10로 용량이 줄어듭니다.

lame hi.wav hi.mp3
lame mi.wav mi.mp3
lame lo.wav lo.mp3

그다음 ffmpeg을 이용해 영상과 소리를 합쳐 각 10초의 재생 시간을 가진 동영상으로 만들어냅니다. 실제 서비스에 사용될 동영상은 비트레이트 및 여러 환경을 주의 깊게 고려해서 인코딩 해야 하나, 여기서는 단순 실습용이기 때문에 그렇게 큰 신경을 쓰지 않고 만들었습니다. 생성 후 모바일 장치에서 올바르게 재생됨을 확인하였습니다.

ffmpeg -loop 1 -i hi.jpg -i hi.wav -acodec aac -ac 2 -strict experimental -ab 40k -vcodec libx264 -preset slow -profile:v baseline -level 31 -b:v 600k -f mpegts -threads 0 -y -t 10 hi.ts

ffmpeg -loop 1 -i mi.jpg -i mi.wav -acodec aac -ac 2 -strict experimental -ab 40k -vcodec libx264 -preset slow -profile:v baseline -level 31 -b:v 400k -f mpegts -threads 0 -y -t 10 mi.ts

ffmpeg -loop 1 -i lo.jpg -i lo.wav -acodec aac -ac 2 -strict experimental -ab 40k -vcodec libx264 -preset slow -profile:v baseline -level 31 -b:v 110k -f mpegts -threads 0 -y -t 10 lo.ts

각 파일을 모두 5개씩 사본을 만들어 H, M, L이 각각 1분간 재생되도록 만들었습니다. 그리고 파일의 이름은 알맞게 {hi, mi, lo}_{0-5}.ts로 변경했습니다. 원래는 긴 파일을 잘게 쪼개 세그먼트를 만들어야 하지만, 이렇게 반대로 해도 큰 문제는 없습니다.
이제 플레이 리스트를 만들어야 합니다. hi, mi, lo 각 대역폭에 대한 플레이 리스트는 다음과 같이 생성했습니다.

#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0

#EXTINF:10,
http://<address>/hi_0.ts

#EXTINF:10,
http://<address>/hi_1.ts

#EXTINF:10,
http://<address>/hi_2.ts

]#EXTINF:10,
http://<address>/hi_3.ts

#EXTINF:10,
http://<address>/hi_4.ts

#EXTINF:10,
http://<address>/hi_5.ts

#EXT-X-ENDLIST

'hi' 부분은 적절히 mi, lo로 변경하여 hi.m3u8, mi.m3u8, lo.m3u8로 각각 저장합니다.
이제 마지막으로 variable playlist인 ahls.m3u8 파일을 작성해 보겠습니다. 앞서 제시한 예제 파일과 크게 달라지는 것은 없습니다.

#EXTM3U

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=150000,RESOLUTION=480x360,CODECS="avc1.42001f,mp4a.40.2"
http://<address>/lo.m3u8

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=480x360,CODECS="avc1.42001f,mp4a.40.2"
http://<address>/mi.m3u8

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=640000,RESOLUTION=640x480,CODECS="avc1.42001f,mp4a.40.2"
http://<address>/hi.m3u8

파일들을 서버에 위치시키고, 테스트해 보았습니다. 처음에는 'L' 글씨가 나오다가 10초 후에 'H'로 변경되었습니다. 그리고 동영상이 끝날 때까지 'H' 글자가 변하지 않습니다. 살짝 문제가 있습니다. 요즘 워낙 인터넷 속도가 빠른 게 문제입니다. 모바일 장치의 대역폭을 임의로 조절해야 하는데, 장치의 대역폭을 물리적으로 변경하기는 조절하기는 쉽지 않습니다.
이럴 때는 ahls.m3u8의 BANDWIDTH 값을 장치의 대역폭에 맞추여 상대적으로 조정하면 됩니다. 값을 적절히 조정하면, 10초 이후 영상에서 'M' 글자가 나오는 것을 확인할 수 있습니다. 아예 mi와 hi의 BANDWIDTH를 회선 속도가 미칠 수 없는 수치까지 올려버리면 영상은 계속 'L'만을 출력할 것입니다. 또한 10초 이후 'M'이나 'H'가 출력될 때, 영상의 슬라이더 바를 옮겨 0초부터 다시 재생을 해 보세요. 0초부터에서도 'M'이나 'H'가 나오는 것을 확인할 수 있습니다.

제가 직접 아이패드로 테스트한 결과를 비디오로 촬영하여 YouTube에 업로드 하였습니다. 여기여기서 확인하실 수 있습니다.

아래는 작업한 파일들을 zip 파일로 압축한 것입니다.
ahls_src.zip

마치며

/*문서를 작성하는데 무진장 많은 시간과 분량을 소비했습니다. 결국 VLC 한 줄의 스크립트일 뿐인데, 분량이 눈덩이처럼 불어났군요. 스크립트를 단순히 사용할 뿐이라면 무의미한 일일지도 모릅니다. 하지만 스크립트를 아무 생각 없이 가져다 쓰는 건 제 성미에 맞지 않습니다. 왜 이렇게 동작할까 이유를 파고 든 결과 여기까지 오게 되었습니다. 개인적으로 상당히 전문적인 분야 직전까지 도달한 것 같습니다. 동영상 인코딩에 조예가 깊으신 분들이나 영상 처리에 일가견이 있으신 분들은 코웃음을 치시겠지만요 ;-) */

지금까지 HTTP Live Streaming에 대해 소개하고 VLC media player를 이용해 동영상 파일을 트랜스코딩하여 네트워크를 통해 전송하는 예를 들어 보았습니다. 이 기술은 사실 웹과 음성, 그리고 영상 데이터까지 처리해야 하므로 그 기반은 매우 다채롭고 복잡합니다. 문서는 개론적인 사항들에 대해서만 간결히 소개하는 정도로 그칩니다. 이 이후의 더욱 자세한 사항은 영상 처리 및 압축 기술에 대한 이해를 필요로 할 것입니다. 서버 운영에 대한 지식도 필요하겠죠. 쉽지 않습니다. ;-)

한편 예제를 통해 HLS를 직접 실험해 보았습니다. 기본적인 HLS는 VLC의 기반으로 간편하게 구현할 수 있었습니다. 이를 통해 HLS는 기존의 웹서버를 크게 변경시키지 않고도 멀티미디어의 스트리밍이 가능함을 확인했습니다. 한편 AHLS에서는 사용자의 네트워크 환경에 맞추어 스트리밍되는 파일의 품질이 동적으로 변경되는 것을 확인하였습니다.

문서를 통해 스트리밍의 개념과 단순한 동작은 확인하였지만, 사용자가 보다 쓰기 좋도록 다듬는 일은 전혀 별개의 작업이며, 매우 힘들고 고된 작업입니다. 혼자만으로는 거의 불가능하며, 매우 많은 시간과 노력을 투자해야 합니다. 만약 파이썬 기반으로 웹 앱을 만들 거라면 '장고(Djago)'와 같은 프레임워크가 붙여야 할 것이고, 원활한 스트리밍을 위해서라면 튼튼하고 성능이 뛰어난 인코더 장비가 따로 있어야 할 겁니다. 차후 장고에 대한 튜토리얼 형식으로 문서의 내용을 심화시켜볼 생각은 갖고 있습니다. 그러나 완성도 높은 여타 프로젝트가 있으니 확실한 목표 없이 '바퀴를 다시 만들' 필요는 없지 않을까 생각합니다.

부록: 참조 링크

다음은 조사를 하면서 수집한 인터넷 문서들입니다. 나름 참고할 만한 자료들이라 기록해둡니다.

project/personalstreaming.txt · 마지막으로 수정됨: 2014/10/09 21:24 저자 127.0.0.1

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki