2012년 12월 19일 수요일

Jelly Bean Media Codec API 사용 기록

Jelly Bean 부터 Media Codec API 를 사용할 수 있게 되었습니다.

1. 소개
미디어 코덱을 이용해서 RIMM 파일을 재생하는 플레이어를 만들어 보았습니다.
그 과정에서 이해하기 힘들었던 점을 기록하겠습니다.
*) 참고로 플레이어 제작 경험 없이 단편적으로 경험한 문제들만 다룹니다.
이전에 플레이어를 만들어 본 경험 없이 적용한 것이라 블로그 내용을 채우는데 시간이 오래 걸릴 수 있습니다.

안드로이드 소스 코드를 확인해 보면 Media Codec API 가 추가되었다기 보다는 기존에 존재하는 API 를 일반 사용자들이 사용할 수 있게 공개했다는 성격이 더 강한 것으로 생각합니다.

아래 페이지에 예제가 있지만 정보가 너무 부족합니다.
http://developer.android.com/reference/android/media/MediaCodec.html

필연적으로 안드로이드 소스 코드를 확인해볼 필요가 있습니다.
Awesome Player 라는 키워드로 소스 코드를 검색해 보시기 바랍니다.

2. 주요 클래스 소개

 - MediaCodec
추가된 API 는 encoding 된 media 데이터를 decoding 해서 출력 가능한 형태로 변환해 주는 MediaCodec 이 있습니다.

 - MediaExtractor
Media 를 저장하는 container 행태 (mp4 등) 로 부터 media 에 대한 메타 정보를 추출하고 chunk 단위로 데이터를 추출해 주는 MediaExtrator 가 있습니다.

MediaExtractor 를 이용하게 되면 mp4 파일 등으로 부터 MediaCodec 을 생성하는 데 필요한 MediaFormat 객체를 손쉽게 얻을 수 있습니다.

만약 안드로이드에서 지원하지 않는 종류의 container 를 재생하고자 한다면 제일 핵심은 MediaCodec 을 생성하는데 필요한 MediaFormat 을 만드는 일일 겁니다.

3. 개발 경험

경험1) 오디오 출력
AAC 오디오를 재생해야 하는 상황이 있었습니다.

http://developer.android.com/reference/android/media/MediaCodec.html
위 링크의 예제대로 하면 에러가 발생했습니다.

Media codec 에 대한 무지로 인해 해법을 찾는 데 더 오랜 시간이 걸렸습니다.
구글로 찾은 AAC 에 대한 자료와 안드로이드 소스를 확인한 끝에 오디오 데이터에 대한 정보를 담는 2 바이트 크기의 descriptor 역할을 하는 데이터에 대해 알게 되었습니다.

Audio Specific Config
 - http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config

MediaFormat 객체에 csd-0 라는 key 이름으로 위 데이터를 직접 입력해 주셔야 합니다.
예)
MediaFormat audioFormat = MediaFormat.createAudioFormat("audio/mp4a-latm", 32000, 1);

ByteBuffer bb = ByteBuffer.allocateDirect(2);
bb.put(new byte[] {(byte)0x12, (byte)0x88});
bb.flip();
audioFormat.setByteBuffer("csd-0", bb);


경험2) 비디오 출력
AVC 비디오를 재생해야 하는 상황이 있었습니다.
AAC 오디오의 경우 처럼 MediaFormat 을 만들 때 따로 descriptor 를 집어 넣어 줄 필요는 없었습니다.
코덱에 대한 무지로 인해 정확히는 모르겠지만 RIMM 포맷의 경우에는 스트림 초반에 AVC 비디오에 대한 Config frame 이 존재하였습니다.
해당 config frame 을 Media Codec 의 input buffer 에 넣어 주니 정상적으로 재생을 하였습니다.

queueInputBuffer() 함수를 이용해 config frame 을 넣어주면서 flag 를 MediaCodec.BUFFER_FLAG_CODEC_CONFIG 로 입력해 줍니다.

경험 3) AV sync 문제
이번 MediaCodec API 에는 재생 부분 까지는 지원을 해주지 않는 것으로 보입니다. (단지 제가 찾지 못하는 경우 일 수도 있습니다.)
그렇기 때문에 재생에 관한 부분 AV sync 등의 문제는 스스로 해결을 해야 했습니다.

재생에 있어서 가장 중요한 값은 PTS 입니다.
PTS 란 해당 비디오 또는 오디오 프레임을 출력하는 시간입니다.
PTS 값은 절대값 또는 상대값일 수 있으며 시간 단위도 미디어 포맷별로 다양할 것입니다.
하지만 어떤 포맷이든 AV 싱크를 위한 값이 없을 수는 없다고 봅니다.

!) 사람은 비디오보다 오디오 재생에 민감합니다.
비디오 재생이 조금 원활하지 않은 경우 인식하지 못하거나 무시하고 넘어가는 경우가 많습니다.
하지만 오디오 재생이 조금만 이상해도 금새 인식할 수 있습니다.
그래서 오디오 재생을 우선하고 비디오 재생 속도를 맞추도록 개발했습니다.

경험 4) 삼성 갤럭시 류의 폰들
삼성 갤럭시 S3, 갤럭시 노트2 JB 폰에서 mono channel audio 를 decode 하고 재생하는데 소리가 늘어지는 현상이 있었습니다.

소스로 들어온 오디오의 channel count 가 하나 (mono) 인데 codec 으로 decode 되는 결과물의 channel count 가 두개 (stereo) 였습니다.

기존에는 소스로 들어온 오디오 데이터의 정보를 토대로 AudioTrack 을 생성했는데 위 현상 확인 후 MediaCodec.INFO_OUTPUT_FORMAT_CHANGED 이벤트가 발생했을 때 넘어오는 mediaformat 정보를 토대로 AudioTrack 을 생성하게 수정하였습니다.

댓글 28개:

  1. Streaming 및 Local File에 대해서 MediaCodec을 성공헀나요?

    답글삭제
  2. 네 RIMM 포맷의 Stream 을 재생하는데 성공 했습니다.

    답글삭제
  3. MediaCodec에 대해 궁굼한게 있습니다.
    지금 제가 여러개의 연속 캡쳐된 사진들을 모아서 동영상파일로 만드려는데
    MediaCodec을 사용하면 해결이 될지 잘 모르겠습니다.
    지금 MediaCodec이 예제코드도 너무 없는 실정이라 이렇게 질문올립니다.

    가능할까요?

    답글삭제
  4. 죄송합니다.

    저는 MediaCodec 의 decoder 기능만 사용했기 때문에 encoder 기능에 대해서는 답을 드리기 어렵습니다.

    답글삭제
    답글
    1. 요것때문에 정말 난감하네요 알겠습니다 ㅎ

      삭제
  5. mediaExtractor를 사용하면 rtsp서버에서 받아오는 video의 메타데이터도 추출할수있나요?

    답글삭제
    답글
    1. MediaExtractor 는 RTSP 형태의 스트림을 지원하지 않고 파일 형태의 container 만 지원하는 것으로 보입니다. setDataSource 함수들 중에 URL 형태를 입력받는 함수도 보이지만 테스트해 보니 RTSP 는 지원하지 않는 거 같습니다.

      RTSP 를 직접 구현하셔야 되겠고 MediaExtractor 대신 MediaCodec 을 사용해서 추출한 미디어를 decoding 하셔야 할 거 같습니다.

      삭제
    2. 답변 정말 너무 감사합니다 : ) 좋은하루되세요!

      삭제
  6. 안녕하세요 현재 MediaCodec클래스를 사용해보려 공부중인데요 비디오쪽만 일단 재생하는데에는 성공 했습니다. 헌데 궁금한 것이 발생 하네요, 실제 뮤직비디오같은 영상은 잘 동작 하지만 디바이스로 촬영한 영상중 세로로 촬영한 영상을 회전하고싶은데 전혀 방법이 보이질 않네요 혹시 이부분 겪어 보셨는지 궁금합니다

    답글삭제
    답글
    1. 그런 경험은 없었지만 제 생각으로는 video rendering 을 직접 구현 해주셔야 할 거 같습니다.

      Video demuxing 을 담당하는 MediaCodec 이 output 데이터를 리턴하면 해당 미디어 데이터를 회전하여 SurfaceView 등에 출력하는 작업을 구현하셔야 할 것입니다.
      - MediaCodec 의 releaseOutputBuffer() 함수에 두번째 인자(boolean render) 를 false 로 주면 MediaCodec 이 rendering 을 하지 않습니다.
      - 이 부분에 rendering 구현을 대체 하시면 될 것입니다.

      다만 해당 영상이 세로로 촬영이 되었는지 감지할 수 있는 방법이 있는지 저도 궁금하네요.
      - 만약 있다면 파일의 description 부분 등에 존재하지 않을까 싶습니다.

      삭제
  7. 동영상 파일에서 오디오 추출 하려고 하는데요.. MediaCodec로 가능할까요 ??
    FFMpeg로 계속 시도중인데... 잘 안돼서 방법을 바꿔보려고 합니다..
    혹시 오디오추출 하는 방법에 대해 아시는부분좀 알려주실수 있으세요 ?

    답글삭제
    답글
    1. MediaCodec 은 encoding 된 비디오/오디오 데이터를 decode 하거나 또는 그 반대의 기능 외에는 없습니다.
      mp4 나 ts 등의 container 스펙을 이해 하고 encoding 된 오디오 패킷을 추출해서 MediaCodec 으로 decode 하는 방법 밖에 없을 거 같습니다.

      만약 mp4 등의 MediaExtractor 가 지원하는 container 라면 MediaExtractor 를 해서 쉽게 얻으실 수 있을 겁니다.

      실제 경험은 없습니다.

      삭제
    2. 오디오 추출해서 그냥 mp3 파일로 저장만 하면 돼는데... decode가 필요 한가요 ??
      그냥 file stream 에다가 write 하면 안돼나요 ??
      재생하거나 다른 작업은 안합니다. 동영상파일에서 -> aaa.mp3 파일로 만드는 작업입니다..
      MediaCodec만으로는 힘든가요 ??

      삭제
    3. mp3 container 에 대한 지식이 없어서 정확한 답은 못드리겠네요.
      아무래도 mp3 파일을 작성하시려면 mp3 container 에 대한 이해가 먼저 있어야 겠습니다.

      미디어 파일에서 오디오 부분만 추출해서 mp3 container 형태의 파일로 write 하는 작업이 필요하겠습니다.
      http://mpgedit.org/mpgedit/mpeg_format/MP3Format.html

      만약 decode 가 필요없다면 MediaCodec 은 사용할 일이 없겠습니다.

      삭제
    4. 답변 감사드립니다.
      음.... 이해는 안돼네요...

      삭제
  8. 제가 지금 폰에 저장된 동영상파일을 디코딩 하여 프레임별로 영상처리를 한 프레임들을 다시 동영상 파일로 만들고싶은데 이것이 mediacodec 으로 가능할까요? 읽어보니까 가능할것도같고 .ㅠㅠ 조언좀 부탁드릴게요!

    답글삭제
    답글
    1. 부족하나마 답변 드리겠습니다.
      참고로 저도 이론적 이해만 있을 뿐 자세한 방법을 설명 못 드리는 점 양해 부탁드립니다.

      기본 개념:

      1. 미디어 데이터
      영상 및 음성을 디지털 (0, 1) 로 만든 데이터를 미디어 데이터라고 합니다.

      2. 미디어 코덱
      미디어 데이터는 용량이 너무 크기 때문에 압축 기술을 이용합니다.
      압축하고 압축을 풀고 하는 기능을 decode/encode 라고 합니다.

      3. 미디어 container
      플레이어 등이 재생을 하기 위해서 미디어가 압축된 코덱이나 여러 정보들이 필요하기 때문에 보통 container 형태로 미디어 데이터가 저장됩니다.
      예를 들어 ts, mp4, mpeg 등의 확장자 파일로 만들어지는 과정입니다.

      관련 답변:

      동영상 파일을 만들기 위해서는 미디어 데이터를 encode 하고 미디어 container 에 넣어주는 작업이 필요합니다.

      Android MediaCodec API 는 decode 와 encode API 를 모두 제공하고 있습니다.

      MediaCodec API 를 이용해서 encode 하실 수 있을 것입니다.

      Encode 된 데이터를 미디어 container 로 구성하는 일은 어렵지만 spec 을 검토해 보셔야 할 것 같습니다.

      첨언:

      제가 글을 쓸 시점의 경험을 간단히 설명드리자면 RIMM 미디어 container 에서 encode 된 미디어 데이터를 추출하여 MediaCodec API 의 decode 기능을 사용하여 재생한 일입니다.

      말씀은 간단하게 드리지만 RIMM 미디어 container 구조를 먼저 공부했고 재생을 위해서는 AV sync 를 맞추는 작업을 해주었습니다.

      Media Codec 이나 재생에 무지했던 상황이라 생소한 용어나 spec 문서를 찾아보고 이해하는데 꽤나 노력이 들었습니다.

      다시 한번 추상적인 개념만 설명 드리지 못하는 점 양해 부탁드리겠습니다.

      삭제
    2. 작성자가 댓글을 삭제했습니다.

      삭제
    3. 작성자가 댓글을 삭제했습니다.

      삭제
  9. 하나만 더 여쭈어봐도될까여 ㅠ 인코딩 성공하셧다고햇는데 동영상에 비디오와 오디오 둘다 녹화되서 나오는거 성공하셧나요 ?ㅠ

    답글삭제
    답글
    1. Encode 는 해보지 않았고 decode 만 사용해 보았습니다.

      삭제
  10. 안녕하세요.
    혹시 Mediacodec만을 사용하여 mp4에 있는 오디오와 비디오를
    동시에 출력하는데 성공하셨다는 말씀이신가요?

    답글삭제
    답글
    1. MediaCodec 의 encode 기능을 이용해 RIMM 에 있는 오디오와 비디오를 동시에 출력 (재생) 하는데 성공하였습니다.

      MP4 container 는 아니었고 RIMM container 였습니다.

      삭제
    2. 죄송합니다. decode 기능을 encode 라고 잘못 적었습니다.

      삭제
  11. 안녕하세요
    혹시
    inputBuffers

    outputBuffers



    dequeueInputBuffers

    dequeueOutputBuffers

    이 뭘 가리키는 지 좀 가르쳐주세요 ㅠㅠ
    인코딩 디코딩 할떄 둘다쓰여서 헷갈리네요 ㅠㅠ

    답글삭제
    답글
    1. 아마 아래 링크를 보셨을 거 같습니다.
      http://developer.android.com/reference/android/media/MediaCodec.html

      1. Buffering
      인코드된 미디어 스트림은 스트림이라는 말을 연상해 보시면 마치 강물처럼 코덱 속으로 들어가게 됩니다.
      코덱이 유입되는 스트림을 들어오는 족족 빠르게 처리하면 문제가 없겠지만 스트림이 유입되는 양은 일정치 않아 때로 코덱이 처리하기 버거울 정도로 많이 유입이되면 넘치게 되어 문제가 생깁니다.
      그래서 스트림 유입을 일정하게 처리할 수 있도록 여유 버퍼를 미리 잡아 둡니다.

      2. dequeueInputBuffer()
      MediaCodec 은 내부적으로 배열과 같은 형태로 버퍼를 조각내어 관리합니다.
      이 함수는 여유있는 버퍼 조각의 index 값을 리턴합니다.

      3. getInputBuffer()
      index 값이 0 이상이면 getInputBuffer() 함수를 이용해 여유 버퍼 조각을 얻을 수 있습니다.
      - ByteBuffer 에 대한 내용은 http://tjjava.blogspot.kr/2011/06/bytebuffer.html 를 참고 바랍니다.

      4. queueInputBuffer()
      여유 버퍼 조각에 encode 데이터를 쓰고 나서 이 함수를 이용해 버퍼 조각에 쓰기가 완료되었으니 여유있으면 decode 해달라고 코덱에게 알리는 역할을 합니다.

      5. dequeueOutputBuffer()
      코덱은 버퍼에서 encode 된 프레임을 하나씩 decode 하여 output buffer 에 쌓습니다.
      사용자는 이 output buffer 가 쌓여 있는지 확인하여 redering 할 수 있습니다.
      이 함수는 output buffer 에 사용자가 가져갈 수 있는 버퍼의 index 값을 return 합니다.

      6. getOutputBuffer()
      이 함수를 이용해 위 index 값에 해당 하는 버퍼 조각을 얻을 수 있습니다.

      7. releaseOutputBuffer()
      이 함수를 이용해 내가 해당 버퍼 조각을 가져갔으니 해당 버퍼 부분은 다른 것으로 채워도 된다고 코덱에게 알려주는 역할을 합니다.

      8. 정리
      다시 정리해 보면 MediaCodec 은 encode 프레임을 입력 받는 버퍼가 존재하고 decode 처리 후 사용자가 가져가서 rendering 할 수 있게 decode 프레임을 버퍼에 쌓아두고 원하면 사용자가 가져갈 수 있도록 합니다.

      9. 참고
      버퍼 조각은 일반적으로 프레임이라는 화면에 출력할 수 있는 이미지 한장 또는 (아주 짧은) 일정 시간의 오디오 등의 조각 단위로 저장이 됩니다.

      삭제
  12. 친절한 설명 너무감사드립니다!
    제가 지금 인코딩까지햇는데 FRAME_RATE을 적어줘도
    그대로 안나오더라구요 혹시 fps에 관하여 조절하시는법 알고계신지 ..ㅠ 구글링해봐도
    찾기가힘드네요 ㅠ

    mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
    mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
    mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, mColorFormat);
    mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,
    IFRAME_INTERVAL);

    여기서 FRAME_RATE에다가 집어넣어도 그대로 셋팅이 안되는거같아서여 ..ㅠ30을 적으면 17프레임이나오네요
    지금 33ms 마다 타이머로 버퍼를 집어넣어줘요

    답글삭제