2019년 1월 24일 목요일

DRM 동영상 스트리밍 서비스를 만들기 위한 모험

페이월 뒤에 일련의 동영상을 제공하는 서비스를 내가 만들 수 있는지 확인할 필요가 있었다. DRM도 포함해서.

그래서 몇 단계의 과정을 거쳐서 관련 정보를 찾아보았고, 알아가는 재미가 있었기에 브라우저에 열려있는 수많은 탭을 정리하며 여기에 그 과정을 남겨본다.


1단계 - 검색어 수집과 탐색

맨 처음이 뭐였는지는 기억나지 않는다. ffmpeg mp4 streaming 정도의 검색어로 시작했던 것 같다. ffmpeg가 이것저것 만능이라는 건 알고 있었는데, 실제로도 이런저런 검색결과가 나왔다.

2단계 - HLS라는 게 있다

검색결과에서 눈에 걸린 건 hls, rtmp 같은 용어였다.
https://www.keycdn.com/support/how-to-convert-mp4-to-hls 페이지에서는 hls 형태로 전환하는 명령이 나와 있다.
ffmpeg -i input.mp4 -profile:v baseline -level 3.0 -s 640x360 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls index.m3u8
 
적당한 동영상 파일을 index.mp4 파일명으로 맞추고 명령을 실행해보았다. index.m3u8 파일 하나와 indexNNN.ts 파일 272개가 생겼다.
m3u 확장자라면 재생목록인 걸 이미 알고 있고 m3u8 파일의 내용을 보아도 약간의 주석과 ts 파일들만 쭉 있었다. mpv index.m3u8 명령으로 실행해봐도 끊기는 마디 없이 하나의 긴 영상으로 뜬다.
이 정도면 파일들을 웹에서 접근할 수 있게 놔두면 그냥 재생이 될 것처럼 보인다.
 

3단계 - DRM 적용

다음으로는 ffmpeg create drm enabled content 정도의 검색어를 찾아보았다.
 
http://hlsbook.net/how-to-encrypt-hls-video-with-ffmpeg/ 페이지에는 hls를 만드는 ffmpeg 실행에 키 파일을 같이 넘기는 설명이 있다.
그럼 m3u8 파일에 키에 대한 언급이 들어가고, 해보진 않았지만 아마 결과물이 되는 파일도 키로 감싸져 있겠지.
 
https://stackoverflow.com/a/42721974/6629746 에서는 DRM이 보안 걸린 파일을 생성하는 것과, 서버와 사용자 간에 그 보안키를 공유하는 것으로 나뉜다고 전제하고 흔히 말하는 DRM이란 후자인 재생 쪽을 뜻한다고 설명한다.
https://shaka-player-demo.appspot.com/docs/api/tutorial-drm-config.html 페이지를 보라고 하는데 여기서는 재생기에 drm 옵션을 어떻게 주는지 설명하고 있다.
 
DRM 자체는 아직 정확히는 모르겠고 재생기를 붙이는 단계까지 가봐야 체감이 될 것 같다.
 

2.1 단계 - DASH라는 게 있다

DRM을 찾다 보니 걸려나오는 문서 중에 다른 것도 있었다.
 
https://www.axinom.com/creating-multi-drm-protected-videos-with-free-tools/ 페이지에서는 ffmpeg와 MP4Box라는 명령으로 dash라는 형식의 파일을 만들어낸다. 
MP4Box는 우분투에서 gpac 패키지가 제공하고 있어서 바로 설치하고 실행해봤다. 
생성되는 파일 중에 mpd 파일이 뭔지는 dash mpd 검색어로 검색해서 나온 https://stackoverflow.com/a/50060214/6629746 여기에 설명이 있다.
https://developer.mozilla.org/en-US/docs/Web/Apps/Fundamentals/Audio_and_video_delivery/Setting_up_adaptive_streaming_media_sources 페이지도 설명이 있는데 한눈에 들어오지 않는다.
  
dash hls라고 찾아보았다.
https://blog.wisen.co.kr/?p=2813 페이지는 CDN 업체의 홍보 페이지인데 DRM 적용된 인코딩 기능을 서비스로 제공하면서 그 일환으로 전반적인 구성을 설명한다. 특출하게 기술적인 내용을 담은 건 아니지만 길잡이로 삼을만했다. 
 
https://meetup.toast.com/posts/131 페이지에 한국어로 친절하게 정리가 되어 있었다. 
요약하면 DASH는 표준 규격, HLS는 애플 규격이었다.
 
모르는 말들이 나와서 caniuse.com에 물어봤다. 
  • https://caniuse.com/#search=hls - Edge와 웹킷 기반의 브라우저가 대체로 지원하는 걸로 보인다.
  • https://caniuse.com/#search=dash - Edge 말고 지원하는 브라우저가 없는 걸로 나온다. 
  • https://caniuse.com/#search=mse - DASH가 지원 브라우저가 거의 없는 건 실제로는 MSE를 통해 구현되기 때문이다.
  • https://caniuse.com/#search=eme - DRM을 처리하는 구현체의 이름. 당당하게 사파리가 빠져 있는데, https://github.com/Fyrd/caniuse/issues/4147 를 보면 실제로는 iOS 11.2부터 지원을 하는 걸로 되어 있다. 벌써 2018년 초반의 일인데 왜 caniuse 사이트에는 아직 미지원이라고 표시가 되어 있는지 모르겠다.

4단계 - 재생기

이쯤에서 내가 아는 DRM 동영상 서비스와 비교해봤다. 왓챠플레이.
왓챠플레이를 열고 브라우저 개발자 도구에서 웹페이지와 Network 탭을 뒤적거려보니 dash 기반이라는 것을 알 수 있었다.
마크업에 붙은 class 값을 보면 재생기는 DASH Everywhere인 걸로 보이는데 이게 그 사이에 이름이 바뀌었는지 검색해서 찾아간 공식 사이트처럼 보이는 곳에는 PRESTOplay라는 이름으로 나온다.
왓챠플레이를 쓰다보면 종종 알 수 없는 오류라면서 죽고 재생이 멈추는 경우가 있어서 왓챠에서 쓰는 건 택하고 싶지 않다.
 
https://blog.streamroot.io/how-to-choose-your-media-engine/ 페이지는 hls와 dash 형식마다 어떤 재생기를 쓸 수 있는지 표로 나열하고 있다.
iOS에서 DASH를 돌리는 것 말고는 다 방법이 있긴 한가보다. 
https://www.npmtrends.com/dash.js-vs-hls.js-vs-rx-player-vs-shaka-player-vs-videojs-contrib-hls 페이지를 보면 hls를 js로 구현하는 게 인기가 많다. 그렇다는 건 hls 포맷을 많이 쓴다는 걸까?
https://blog.videojs.com/dash-everywhere-ish-hack-project/ 페이지를 보면 iOS 때문에라도 HLS와 DASH를 모두 유지하도록 권하고 있다.
 
https://medium.com/@stepashka69/making-web-browser-play-streaming-video-mpeg-dash-smooth-streaming-hls-with-one-player-c5f4dd445b91 페이지에서는 아예 adapter를 하나 입혀서 구분없이 쓸 수 있게 하자는데 이게 실무에서 의미가 있을지 어떨지 모르겠다.
 

5단계 - 서버

이미 인코딩된 파일을 제공하는 것이니 파일을 제공하는 웹서버만 있으면 될 것 같다.

rtmp는 직접 인코딩을 실시간으로 할 수도 있는 것 같고 ffmpeg 인코딩을 받아서 내보내는 역할만 할 수도 있는 것 같다.
하지만 스트리밍을 대량으로 하려면 실시간 인코딩은 자원 소모가 심하고 지연도 발생할 것이니 선택할만한 방법이 아니다.

nginx를 그냥 쓰고 rtmp 플러그인을 추가하는 것도 보인다. https://hermantorjussen.no/how-to-livestream.html
apt search 명령으로 찾아본 우분투 패키지 중에는 crtmpserver라는 것도 있는데 자세히 찾아보진 않았다.

http://elelinux.blogspot.com/2015/11/dash-streaming-using-nginx.html 페이지에서는 nginx 설정에 rtmp 블럭을 두고 그 안에 dash 설정을 붙이는 방식이 나온다.
  

5.1단계 - 인증된 사용자에게만 서빙

paywall을 거쳐야 하기 때문에 접근을 제한할 방법이 필요하다. 

signed cookie, signed url이라는 용어를 초반 검색 어딘가에서 봤다. https://blog.embian.com/123 페이지였던 것 같다.
이건 탐색이 더 필요하다.
https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html

encryption key rotation이라는 게 재생기 옵션에 대체로 있는 것 같으니 지원을 다들 하는 것 같다.

http://blog.leedoing.com/87 여기는 샘플로 할만한 코드가 있다.

6단계 - AWS 사용

AWS에도 스트리밍 기능은 있으니 AWS의 문서를 읽어보면 위에서 검토한 내용들을 맞게 이해했는지, AWS를 써먹을 수 있는지를 알 수 있다.

aws hls dash라고 검색하면 Elastic Transcoder나 Elemental MediaConverter가 나온다. AWS Elemental 시리즈가 신형으로 Elastic Transcoder를 대체하는 것 같다.
aws hls dash drm라고 하나 더 붙여도 특별히 달라지는 건 없지만 DRM 관련 정보를 더 직접적으로 찾을 수 있다.

https://medium.com/@whatauseless/aws-media-services-%EC%A0%95%EB%A6%AC-232484cbdbd6 페이지는 Elemental 시리즈의 대강을 설명하는데 비슷비슷하게 보여서 잘 모르겠다.

https://www.slideshare.net/awskorea/pooq-ott-case-for-live-vod-build-on-aws 페이지는 마침 AWS로 POOQ 서비스를 구축한 사례인데 대체로 알아들을 수 있다.
"서비스 운영" 장에서 "룰 베이스 미디어 파일명 사용으로 Manifest와 Media 의존성 제거" 부분은 감이 안 온다.
"서명된 쿠키 사용"은 JSON이 적혀 있는데 당장은 어떻게 사용되는지 다 이해가 되지는 않는다.

5.2단계 - 인증된 사용자의 유출을 추적

forensic watermark라는 용어가 이건 것 같다. cloudfront forensic watermark라고 찾아보면 이런저런 단어들이 나온다. ffmpeg forensic watermark는 자동완성으로도 뜨는데 검색결과는 신통치 않다.

AWS 관련으로 검색하던 중에 https://docs.pallycon.com/ko/multidrm/packaging/aws-elemental/ 페이지를 보았는데 메뉴에 포렌식 워터마킹이라는 항목이 있었다. 전자책 업체 등에서 사용자가 다운로드 받은 파일에 누구인지 구분하는 식별자를 붙여서 복제가 발생했을 때 누가 유출한 건지 밝힌다는 얘기가 이건가 싶었다.

https://docs.pallycon.com/ko/watermarking/mixing/ 페이지의 설명을 읽고서야 동작 방식이 이해가 되었다. 실시간으로 영상 인코딩을 하는 건 아니고, HLS나 DASH를 만들 때 각 세그먼트(파일 조각)을 아예 0,1 두 벌로 만들고, 둘 중에 어떤 걸 송출할지를 세션 정보에 따라 구분해서 결과적으로 완성된 스트림을 녹화한다면 어떤 세션 기준으로 조합된 것인지 알 수 있게 한다.

https://docs.pallycon.com/ko/watermarking/mixing/cloudfront-mixer/ 페이지를 더 읽어보면 조합 절차는 lambda edge로 처리하는데 이 부분은 부하가 걸리지 않으려나? https://www.tvbeurope.com/technology/nagras-watermarking-solution-integrated-into-amazon-cloudfront-cdn 페이지를 보면 NexGuard 제품도 같은 방식인 것 같다. 결국 실시간으로 처리하는 구간이 어딘가는 존재해야 하고 그게 CDN과 붙어있는 람다 엣지라는 거겠지.

https://docs.pallycon.com/ko/watermarking/detecting/ 페이지에서는 유출 의심이 생겼을 때 의뢰도 자기들한테 하라고 되어 있다. 직접 검출하려면 어떤 방법이 있을지 모르겠다.

7단계 - 웹 푸시

네이티브 앱이 당분간 없을 예정인데, 가급적 사용자의 주목을 끌고 싶은 마음은 있어서, web push를 같이 붙이고 싶다.
https://www.binpress.com/building-useful-notifications-html5-apis/ 페이지에는 푸시 말고도 유용한 방법을 더 안내하고 있다.

https://developer.mozilla.org/ko/docs/Apps/Progressive/Re-engageable_Notifications_Push 페이지에서는 service worker가 필요하다고 한다. 그렇다는 건 PWA 구현체가 되어야 한다는 거지.
https://github.com/GoogleChromeLabs/sample-media-pwa 이런 구현 샘플을 뜯어보면 이해가 되려나.

하지만 https://caniuse.com/#feat=push-api 표를 보면 지원 현황이 좋지 않다.
https://medium.com/@firt/progressive-web-apps-on-ios-are-here-d00430dee3a7 페이지를 보면 iOS와 안드로이드에서 지원이 꽤 다르기도 하다.

7.1단계 - TypeScript 도입?

가급적 ES6 내지는 TS 정도를 도입하고 싶은데, PWA나 service worker도 모르는 판국이라 엄두가 나지 않는다.

2019년 1월 7일 월요일

동영상을 배경화면으로 쓰기 위한 모험

리눅스 데스크탑 환경을 잘 쓰고 있는데 아쉬운 것 중에 하나는 배경화면에 iOS 환경처럼 동영상을 깔아둘 수 없다는 거다. 굳이 찾아보면 그런 게 된다고 하는 프로그램이 없진 않은데 내 환경이랑은 안 맞기도 하고, 어떻게 동작하는지 단박에 이해가 되지 않아서 내 환경에 들이고 싶은 마음도 안 든다.

X11 Root Window

가장 쉽게 떠올릴 수 있는 건 mplayer -rootwin 혹은 mpv -wid 0 옵션을 적용해 X11 root window에서 재생하는 건데, 이게 예전에는 자연스러운 동작이었지만 (윈도우 10라면 dwm.exe에 해당하는) compositor가 일상적으로 돌아가는 요즘은 (나는 compton을 쓴다) 재생이 시작되자마자 디스플레이 영역 전체를 덮으며 영상이 나오는 바람에 써먹을 수가 없었다.

게다가 여러 모니터를 쓰는 환경이라서 root window를 쓰기엔 어울리지 않았다. 꽤 넓은 이 해상도에 맞게 동영상을 그리면서 계단 현상이 안 생기려면 동영상 자체의 해상도가 어지간히 높아야 하기도 하고, 한 화면에만 동영상을 깔아두고 싶기도 했다.

일반 창 + 추가 속성

mpv 옵션을 한참 만지작거리다가, 굳이 root window일 필요가 없다는 걸 깨달았다. mpv --screen=0 옵션 정도로 0번 모니터에 창이 뜨면 일단 되는 거였다. (창 관리자에 따라서 안 될 수도 있다고 옵션 설명에 적혀있긴 하다)

거기에, 배경화면처럼 동작해야 하니 (흔히 쓰는 Always on top의 반대인) Always on bottom 속성이 먹혀야 하는데 mpv에 --ontop 옵션은 있어도 --onbottom에 해당하는 옵션은 없다. wmctrl -r gl.mpv -x -F -b toggle,below 명령을 쓰면 되는 것까지는 확인했다. 하지만 이게 다가 아니었다. mpv --no-border 옵션을 주거나, wmctrl로 fullscreen 속성을 주면, below 속성이 무시되고 포커스가 갔을 때 다른 창보다 위로 올라간다.

Openbox

그래서 다른 방식으로 해보았다. openbox는 rc.xml에 여러 설정을 먹여서 창을 세밀하게 관리할 수 있으니까, wmctrl으로 차례로 조정하던 속성을 아예 창 관리자 자체에서 한 번에 먹이면 뭔가 다르지 않을까 하는 생각이었다.

대충 아래처럼 rc.xml의 applications 요소 안에 넣어주면 되는데 (mpv --title 옵션을 썼다) 그래도 차도가 없었다. 터미널에 아예 above 속성을 줘봤지만 그래도 fullscreen 상태가 된 mpv 창이 더 위로 올라왔다. 아무래도 openbox가 지정하는 순서가 그런 것 같다.
    <application title="THIS_IS_IT">
      <layer>below</layer>
      <fullscreen>yes</fullscreen>
      <position force="yes">
        <monitor>1</monitor>
      </position>
      <focus>no</focus>
    </application>

혹은 fullscreen 대신 decor 속성을 끄는 방식으로 해봐도 똑같다. man의 설명에 따르면 mpv --no-border 옵션이 decor 속성에 대응한다는데 이건 wmctrl로는 해결이 안 된다.

몇 가지 조합을 해본 바로는, fullscreen 속성이든 decor 속성이든 상관없이 mpv가 해당 스크린 영역을 모두 차지해서 openbox가 뭘 더 그리거나 mpv 외의 남는 영역이 있지 않으면 below 속성은 무시된다. 혹시나 해서 gnome-mpv로도 실행해봤는데 똑같은 방식으로 동작한다. 이건 아마도 openbox가 전체 창을 다루는 방식 때문이지 않을까 싶은데 openbox를 대신할 창 관리자가 없어서 의심만 할 뿐이다.

결과

그렇다면 해결책은 이렇다.

openbox로는 mpv나 wmctrl로 적용이 안 되는 below 속성을 기본으로 주고, 창 크기를 1픽셀 줄여서 전체화면이 아니게 한다. (남은 1픽셀은 적당히 눈에 안 띄게 그 아래에 배경색을 깔아주면 된다)
창 크기가 1픽셀 줄어들었으므로 영상이 찌그러지지 않도록(=불필요한 이미지 재처리가 일어나지 않도록) 가로세로 비율이 창의 영향을 받지 않도록 하고, (이건 openbox에서 설정해도 무방한데) 전체화면이 아니며 창 테두리도 없으며 0번 화면에 표시한다. 소리는 끄고, mpv가 종료하면 다음 재생시에 끝난 부분부터 다시 재생하게 한다. 배경화면이므로 키보드나 마우스로 제어되지 않게 한다. 화면보호기는 평소처럼 작동하고 재생중이던 파일들은 무한 반복한다.
여기에 -vo vaapi 같이 적당한 가속 기능을 줘서 CPU나 GPU 어느 쪽을 쓸지 정해주면 된다. htop과 radeontop으로 확인했을 때 어느 쪽이든 부하가 걸리기는 한다.

rc.xml
    <application title="THIS_IS_IT">
      <layer>below</layer>
      <focus>no</focus>      <skip_pager>yes</skip_pager>
      <skip_taskbar>yes</skip_taskbar>
    </application>

mpv \
 --geometry=1919x1080 \
 --keepaspect-window=no --video-unscaled=yes \
 --fullscreen=no --no-border --screen=0 \
 -ao null --save-position-on-quit \
 --no-input-default-bindings --osc=no --window-dragging=no \
 --stop-screensaver=no --keep-open --loop-playlist