yaPlayer가 처음 출시되었을 때는 iOS가 지원하는 비디오 형식(MP4, M4V, MOV)만 지원했다. 이유는 간단하다. 연구실에 있는 iMac의 코어가 16개라 PC를 이용한 동영상 변환에 대해 부담감이 전혀 없었기 때문이다. 지난 게시물에서 말한 대로, 나를 위해 만들었기 때문에 사실 이 상태에서도 굉장히 만족했었다. 


그렇게 한동안은 잘 사용하고 있었다. 하지만 판매 실적은 굉장히 저조했고 (먹고 살 생각하면 판매 실적은 무척 중요하다) 구매한 일부 사용자들로부터 항의 메일을 받기 시작했다. '어째서 그 흔한 AVI, MKV 형식을 지원하지 않는거죠?'


앱 설명에 지원하는 비디오 형식을 써 놨지만, 일부 사용자들이 앱 설명을 자세히 읽지 않고 구매하는 것 같았다. 이는 일반적인 기대감 때문이리라. 앱스토어에 진열된 수 많은 동영상 플레이어들 대부분이 모든 형식의 비디오를 지원하다 보니 당연히 yaPlayer도 그럴 거란 기대를 한 것 같았다.


나를 위한 앱이 곧 사용자를 위한 앱이리라- 하지만 막상 뚜껑을 열고 리뷰나 메일 등의 피드백을 받고 나면 '이런 오만한 녀석! 너는 어찌 그리 생각이 짧은가!' 하는 소리가 5.1 채널 서라운드로 정신 사납게 울려 퍼진다. 


커피 한 잔 값(사실 커피 한 잔 만도 못하다)의 손해는 모바일 앱 시장에서는 굉장한 실망과 충격으로 다가온다. 그리고 이러한 실망과 충격은 리뷰로 바로 이어진다. 별 한 개 짜리 리뷰... 항상 자식처럼 여기고 모든 노력과 정성을 다해 만든 앱인데 좋지 않은 평가를 받으면 굉장히 마음이 아프다. 그리고 다소 공격적인 리뷰를 읽다 보면 정신적인 스트레스가 엄청나게 쌓인다. (특히 한국 앱스토어 리뷰가 심하다. 외국어는 번역해서 읽다보니 나름 걸러서 읽히는데 한국어는 그냥 바로 읽히니까; 게다가 한국 앱스토어의 별 한 개 짜리 리뷰는 다른 나라 앱스토어 리뷰에 비해 굉장히 공격적이다)


급기야 yaPlayer에 대한 만족감이 크게 줄어들었다. 

무인코딩 재생이라는거... 나도 한 번 해볼까?





무인코딩 재생의 핵심 - FFmpeg 라이브러리


누가 먼저 시작한 용어인지는 모르겠으나, 무인코딩 재생이란 별도의 비디오 변환 없이 바로 재생한다는 걸 의미한다. iPhone/iPad 등의 iOS 기반 모바일 기기들은 MP4, M4V, MOV 형식의 동영상만 지원하고 나머지 형식은 지원하지 않는다. 그래서 다른 형식의 동영상을 재생하려면 iOS 기기가 지원하는 형식으로 변환(인코딩)을 해 주어야 한다.


이 변환을 앱 내에서 직접 수행하면 별도의 인코딩 과정이 필요 없어진다. 이를 위해 동영상 형식에 따라 구조를 해석하고, 압축된 영상과 음성을 해석하는 알고리즘이 필요하다. 현존하는 거의 모든 비디오/오디오 압축 형식에 대한 해석 알고리즘은 오픈 소스인 FFmpeg에 구현되어 있다. 

FFmpeg은 소스 코드로 제공되기 때문에 iOS 기기에 맞게 컴파일해야 앱에서 사용할 수 있다. 문제는 이 컴파일부터 난관이라는 거. 현재 OSX의 최신 버전은 마운틴 라이언이고, Xcode는 4.5.1, 마지막으로 FFmpeg은 1.0 인데 이 세 조합은 꽝! 이 상태로는 컴파일이 안된다. 


문제의 핵심은 Xcode, ARM 용 어셈블러에 몇 가지 변경이 있는 것 같더라. 이 문제를 해결하는 가장 쉬운 방법은 Xcode 4.3.3 을 내려받아 여기 들어있는 clang 컴파일러를 이용하는 것이다. 또 하나 간과할 수 없는 문제가 있는데 기본 설정으로 컴파일하면 MP3 음성을 ARM 아키텍처에서 제대로 해석하지 못하고 스피커 찢어지는 소리를 낸다. 이 문제는 ARM6 용 최적화 옵션을 비활성화하면 해결된다. (어차피 yaPlayer는 ARM7을 대상으로 하니까 ARM6 최적화에 대해 별 신경을 쓰지 않고 있다)


라이브러리 컴파일을 됐는데, 이제부터는 뭘 해야 하지?


컴파일을 마치고 나니 뭐부터 어떻게 시작해야 할지 너무나 막막했다. 당연한 결과다. 동영상을 재생만 해봤지, 직접 파해쳐 본 적이 없으니! 오랜 시간 (정말 길고 길었다) 붙잡고 이런 저런 자료들을 살펴보고 나니 천천히 그림이 그려지기 시작했다. 동영상이란 무엇인가, FFmpeg으로 할 수 있는 일은 무엇인가, 동영상을 재생하기 위해서는 무엇을 해야 하는가- 이 세 가지를 따로 공부하고 합치면 그림이 그려진다.


동영상 파일을 읽어 재생하기 위해서는 다음의 과정을 따라야 한다:

  1. 비디오 파일을 열고 그 안에 정의된 스트림 정보를 읽는다: 비디오 파일 안에는 여러 개의 스트림이 있다. 스트림은 일종의 수도관 같은 거다. 꼭지를 열면 정보가 쭉쭉- 밀려 나온다. 오디오, 비디오, 자막 등의 정보가 각각 분리된 스트림으로 존재한다.
  2. 스트림을 선택한다: 하나의 비디오 파일 안에는 여러 개의 비디오, 오디오가 섞여 있을 수 있다. 예를 들어 2 채널 오디오와 5.1 채널 오디오가 모두 포함된 동영상은 오디오 스트림이 두 개다. 따라서 재생할 스트림만 선택해서 꼭지를 열고, 나머지는 닫아 주어야 한다. (실은 굳이 닫지 않아도 상관은 없다. 하지만 최소한 어떤 스트림에 집중할 건지는 여기서 결정해야 한다)
  3. 정보를 읽는다: FFmpeg은 각각의 스트림 별로 따로 정보를 읽어내지 않고 마구잡이로 읽어낸다. 이는 동영상의 특성 때문이다. 스트림 별로 정보를 따로 담아 각각의 스트림을 차곡차곡 쌓은 형태라면 특정 시점의 영상 하나, 음성 하나를 읽기 위해 각각의 스트림 사이를 왔다갔다 해야 한다. 굉장히 비효율적이지 않은가! 그래서 동영상 파일에는 모든 정보가 스트림에 관계 없이 재생 시간 순으로 섞여 있다. 따라서 정보를 하나 읽고 이 정보가 어느 스트림에 속하는지 확인하여 그것이 영상인지, 음성인지 직접 구분해야 한다.
  4. 정보를 해석한다: 읽어낸 정보는 있는 그대로 사용할 수 없다. 왜냐하면 이 정보는 압축된 정보이기 때문이다. 따라서 압축을 풀어 원래의 정보로 복원해야만 하는데 이 과정을 디코딩이라고 한다. FFmpeg은 영상과 음성 디코딩을 위한 함수가 각각 따로 존재한다.
FFmpeg의 역할은 여기 까지다. 영상과 음성 정보를 디코딩 했으면 이 정보를 화면과 스피커로 뿌려줘야 하는데 이건 iOS SDK에 정의된 프레임워크를 이용해야 한다. 이 때 영상과 음성의 싱크에 주의해야 한다. 정보가 해석되는 대로 바로 보여주고 들려주면 아주 신명나고 정신없는 비디오를 감상하게 된다. 싱크를 맞추는 방법은 그야말로 '개발자 마음대로'. 싱크를 맞추는 것도 기술이다.

참고할 만한 링크:
  • FFmpeg-for-iOS: FFmpeg을 이용한 기본적인 비디오 재생 프로젝트
  • ffmpeg4iphone: 위와 같은 목적, 하지만 코드가 좀 더 복잡해서 기본 원리를 이해할 목적이라면 위의 링크를 추천한다.
  • ffplay.c: FFmpeg 프로젝트 안에 포함된 플레이어 소스 코드. iOS와 무관하여 화면 출력 및 소리 재생은 별도의 작업이 필요하지만, FFmpeg의 최신 버전에 맞추어 사용하는 API가 갱신되고 무엇보다 레퍼런스라는 강점이 크다!


색의 기본은 RGB가 아니던가!


FFmpeg을 이용해 영상 정보를 디코딩하면 한 장면(그림)이 나오는데 이를 바로 화면에 출력하면 유령 영화를 보는 것 마냥 흥겨워진다. 이는 대부분의 동영상이 영상을 RGB 형식이 아닌 YUV 형식으로 저장하기 때문이다.


동영상은 영상과 음성이라는 엄청난 데이터의 집합으로 그 양이 실로 후덜덜하다. 그래서 AVI, MKV 등의 형식으로 인코딩하여 용량을 줄이는데 (사실 AVI, MKV 등은 압축 방식이 아니라 구조를 결정한다. 실제로는 H.264, MP3 등의 코덱이 압축 방식을 결정한다) 이 때 대부분의 동영상이 영상의 각 장면을 RGB 대신 YUV 형식을 사용하여 저장하고 압축한다. RGB 형식보다 YUV 형식이 더 적은 용량을 차지하기 때문이다.


하지만 모니터나 모바일 기기 등의 스크린은 모두 RGB 형식으로 장면 정보를 주어야 제대로 표시된다. 따라서 디코딩한 장면을 RGB 형식으로 변환해주어야만 제대로 된 화면을 볼 수 있다.


문제는 YUV 형식의 장면을 RGB 형식으로 변환하는데 엄청난 계산이 필요하고, 이로 인해 많은 CPU 자원이 소모된다는 것.


예를 들어 720p (1280x720) 동영상의 한 장면을 YUV 형식에서 RGB 형식으로 바꾸는 경우를 생각해보자. 한 장면은 1280x720 = 921600 개의 점으로 이루어져 있고, 한 점은 Y, U, V 데이터로 표현된다. 한 점을 RGB 형식으로 바꾸려면 적어도 3 번의 계산의 필요하므로 한 장면을 변환하려면 적어도 921600x3 = 2764800 번의 연산이 필요하다.


FFmpeg을 이용한 디코딩 자체가 이미 엄청나게 CPU를 소모하고 있다. iOS 기기의 CPU는 일반 PC의 CPU에 비해 현저히 느리기 때문에 720p 영상을 단순히 디코딩하는 것만으로도 벅차다. 그런데 여기에 RGB 형식으로 변환하는 연산까지 더해지면 어떻게 될까? 간단하다. 다음 화면을 제시간에 준비하지 못해 버벅이는 현상으로 나타난다.


저 연산을 어떻게 좀 해 볼 수 없을까?




OpenGL ES 2.0 그리고 Shader


방법이 하나 있다. CPU를 안 쓰면 된다. 잠깐, 그게 가능한가? 우리는 흔히 CPU 성능에 대해 논하므로 CPU에 대해서는 다들 쉽게 이해한다. 듀얼 코어니 쿼드 코어니, 클럭 수가 얼마니 하면서. 하지만 모바일 기기에 CPU 외에 또 다른 연산 장치가 존재하는 사실을 많은 사람들이 잘 모르고 있다. 바로 GPU다.


GPU는 3D 그래픽 전용 연산 장치다. 3D라고 해서 2D를 제외하는 건 아니다. Z 축을 0으로 고정하면 3D 연산을 2D에 그대로 적용할 수 있기 때문에 모든 그래픽 연산을 GPU로 할 수 있다. 일반적으로 GPU의 그래픽 연산은 동일 연산을 CPU로 처리할 때보다 엄청나게 빠르다. CPU는 고작해야 듀얼, 쿼드 코어라 동시에 최대 4 개의 연산을 동시에 하지만 GPU는 동시에 처리할 수 있는 연산의 수가 CPU의 그것과는 비교가 되지 않는다. 그리고 이 연산은 OpenGL ES API를 통해 제어 가능하다.


OpenGL은 GPU와 개발자가 대화하는 방법(API)을 정의한다. 모바일 기기는 PC 보다 제약을 더해 ES라는 수식어가 따라 붙으며 1.0, 그리고 2.0 버전이 존재한다. 2.0 버전의 특징은 그래픽 연산 과정 사이에 개발자가 임의의 연산 공식을 끼워 넣을 수 있다는 것! 쉐이더(shader)라 불리는 기술을 이용하면 GPU가 OpenGL 명령을 수행하는 사이에 임의의 변환 공식을 끼워 넣을 수 있다. 


이제 해답은 나왔다. RGB로 변환하는 공식을 쉐이더로 작성해서 끼워 넣고 YUV 형식의 장면을 GPU에 전달하여 그리라고 명령한다. 그러면 GPU는 쉐이더를 이용해 장면을 변환한 후 그 결과를 화면에 뿌린다. 물리적으로 CPU와 GPU는 분리되어 있기 때문에 쉐이더 연산은 CPU 성능에 영향을 미치지 않는다. 게다가 GPU는 철저히 '행렬 계산'에 특화된 연상 장치로써 RGB 변환 방정식을 CPU보다 훨씬 빠르게 처리한다. (연립 방정식은 모두 행렬로 표현할 수 있다)


이에 대해 보다 자세한 정보는 아래 링크에서 찾아볼 수 있다:

안타깝게도 나는 쪼렙이라 쉐이더를 이용한 RGB 변환을 구현하는데 1 주일이나 고생했다;




다음 이야기


오늘은 yaPlayer에 무인코딩 재생 기능을 추가하기 위해 FFmpeg을 이용한 과정을 살펴봤다. 처음에는 FFmpeg을 어떻게 이용해야 하는지 너무나 막막했지만, 시간을 두고 천천히 바닥부터 공부해보니 할 만 하더라. (괜히 조급한 마음에 서두르면 이해하기 힘든 오류로 고생하게 된다. 이 이야기는 나중에) 무인코딩 재생 기능을 구현하고 나서 뒤돌아보니 FFmpeg을 이용해 동영상을 읽어와 영상과 음성을 해석하는 것보다 둘 사이의 싱크를 맞추는 기술을 구현하는데서 더 큰 어려움을 겪은 것 같다. 일명 '부릉부릉 어택' 이라 불리우는 yaPlayer의 싱크 기술은 나중에 다룰 예정이다.


다음에는 yaPlayer의 아이콘이자 핵심 UI(User Interface, 사용자 인터페이스)인 터치 휠(Touch Wheel)에 대한 이야기를 할 예정이다. 





신고

CATEGORIES