티스토리 뷰

크리에이티브 커먼즈 라이선스
Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시 2.0 대한민국 라이선스에 따라 이용하실 수 있습니다.
본문

 Android MediaCodec을 이용하여 비디오 디코하는 예제를 작성해보았습니다. 예제는 이미 오래전에 올려두고 블로그에 정리하는것이지만... 이번 글에서는 디코딩만 진행합니다.



지금까지 블로그에 포스팅한 내용


그사이 발표도 2번 진행하였고, 그에 대한 정리를 올리지 못하였습니다. 그래서 순차적으로 올려보려고 합니다.



Video Decoder를 하기 위한 조건

  • Android 4.1 이상
  • MediaCodec을 이용하여 동작하지 않는 단말기도 있을 수 있어서 듀얼 코어 이상을 추천드립니다.
    • 일부 기기에서 사운드와 비디오를 동시에 디코딩할 경우 동작하지 않는 경우가 있었습니다.
  • MP4 동영상 또는 H.264, OS 버전과 제조사 지원에 따라서 추가 코덱이 지원될 수 있습니다.
    • H.264 데이터를 다룰 때에는 비디오에 대한 화면 사이즈를 정확하게 알아야 합니다.
    • MP4 영상의 경우 파일로 저장되어있는 영상을 불러오게 되므로 MediaExtractor API를 사용합니다.
  • VP8/VP9 역시 지원됩니다.



Video Decoder를 사용하기 위한 API



 각 API에 대한 간단한 예제를 살펴보겠습니다. 이 예제들은 Android API 문서에 설명되어 있는 내용이며, 이를 기초로 한다면 디코딩하는 코드는 아래의 Github에 올려둔 예제와 같이 동작할 수 있습니다.

 Github 비디오 디코딩 예제


 이번 글에서는 MediaCodec을 통한 디코딩에 대한 코드 설명과 MediaExtractor API를 사용하여 mp4 파일을 읽어들이는 방법을 간단하게 설명하겠습니다. 자세한 예제는 github 주소를 참고하세요.



MediaCodec의 초기화

 MediaCodec을 초기화 하기 위해서는 MediaFormat을 알아야 합니다. 이 MediaFormat은 직접 작성하여도 되고, MediaExtractor를 이용하여 값을 가져올 수도 있습니다. 디코딩에서 필요한 정보는 화면의 사이즈 정보만 알면 됩니다.


 가장 간단한 방법은 아래와 같습니다. 이중 mime Type은 H.264를 나타내는 "video/avc"를 적고, 넓이와 높이값을 적어주시면 됩니다.

MediaFormat format = MediaFormat.createVideoFormat(String mime, int width, int height);

 

 MediaCodec을 초기화 할 때에도 mime type을 넘겨주어야 합니다. MediaFormat을 수동으로 셋팅하였을 때와 동일한 값이 들어가게 됩니다.

MediaCodec codec = MediaCodec.createDecoderByType(String mime);


 MediaFormat 정보를 셋팅해주어야 하는 단계입니다. 셋팅하기 위한 값은 아래와 같습니다.

 - MediaFormat : 위에서 설정한 MediaFormat의 설정값(MediaExtractor를 이용할 경우 해당 MediaFormat 정보를 return 받아 올 수 있습니다.)

 - Surface : 디코딩에서만 사용하며 화면을 그리기 위한 View를 넘겨주면 됩니다.

 - MediaCrypto : 암호화된 Media data를 다룰 때 사용하는 옵션입니다. 일반적으로 null로 초기화 해서 사용합니다.(저도 아직 어떤 경우에 사용해야 하는지를 모르겠네요)

 - flags : 인코딩시에만 다음의 값(MediaCodec.CONFIGURE_FLAG_ENCODE)을 초기화 하고, 그외에는 0으로 초기화 합니다.

codec.configure (MediaFormat format, Surface surface, MediaCrypto crypto, int flags);


준비가 완료되었다면 MediaCodec start를 호출합니다.

codec.start();




MediaCodec을 이용한 디코딩 진행

 MediaCodec을 사용하는 방법이 5.0을 기준으로 조금 변경되었습니다. 아래 구글에서 제공하는 주석에서도 보면 알 수 있겠지만 Encode/Decode를 할 때 굳이 알필요 없는 정보이기도 합니다.


 그 알 필요없는 정보는 시스템 내부의 프레임웍에서 MediaCodec에서 사용할 Buffer를 몇개를 사용할지를 정하는 부분에 해당되는데 실제 기존 API 상으로는 Buffer를 몇개 사용하고 있는지에 대한 정보를 사용자가 알더라도 줄이거나, 늘리거나 하는 동작이 불가능하였습니다. 이에 대한 API를 5.0에서는 @deprecated 하였습니다. 5.0을 개발할 경우가 아니라면 기존 방식 그대로 사용해야합니다.


 MediaCodec에서 사용할 inputBuffer와 outputBuffer를 초기화 합니다. 이 작업은 5.0에서 @deprecated 되었습니다.

// if API level <= 20, get input and output buffer arrays here

ByteBuffer[] inputBuffers = codec.getInputBuffers();

ByteBuffer[] outputBuffers = codec.getOutputBuffers();


 위와 같이 사용할 Buffer를 미리 알아두어야 합니다. ByteBuffer에 대한 모든 정보는 MediaCodec의 프레임웍단에서 알고 있습니다. 그렇기에 실제 몇개가 생성되는지는 알 수 없습니다. 제가 테스트할 때는 많으면 20개까지도 생성되는것을 확인하였습니다.

 참고 : Surface를 통해서 Output을 하거나, Input을 하는 경우에는 ByteBuffer는 1개가 생성되며, 반대쪽이 여러개가 생성됩니다.



아래 데이터 입력과 출력 부분은 루프(for, while 등)를 통해서 동작하는 부분입니다.


데이터 입력 부분

 데이터를 입력하는 부분을 먼저 설명하겠습니다. MediaCodec에 입력되어야 할 데이터는 2가지 형태가 있습니다.

  • YUV의 색상정보 데이터 : 다음 링크 글의 MediaCodec 사용하기 부분을 참고하세요 http://thdev.net/576
  • Surface : 인코딩시에만 해당되며 4.3이상에서만 사용할 수 있습니다.

 입력되어야 할 데이터는 2가지 형태가 필요하지만 지금은 Decoder를 할 경우이기 때문에 MediaExtractor에서 데이터를 가지고와서 처리해야합니다. 가지고와서 집어넣는 부분은 github의 예제를 참고해주세요.



1. 데이터를 넣을 Buffer index 가져오기

 buffer index는 dequeueInputBuffer 함수를 통해서 가지고오게됩니다. 해당 Buffer index가 -1 보다 큰 경우에만 실제 데이터를 writer 하여 Decoder 요청할 수 있습니다.

  int inputBufferIndex = codec.dequeueInputBuffer(timeoutUs);


2. 가지고 온 Buffer index에 데이터 채워넣기

 21 이상 : Lollipop에서만 사용할 것이라면 ByteBuffer 를 직접 받아올 수 있는 함수가 제공됩니다.

   - getInputBuffer(index);

 20 미만 : Lollipop이 아닌 이전 방식은 위에서 생성한 inputBuffers를 활용하여야 합니다.

   - ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; 를 return 받아 이 buffer를 사용하면 됩니다.

  if (inputBufferIndex >= 0) {

    // if API level >= 21, get input buffer here

    ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferIndex);

    // fill inputBuffers[inputBufferIndex] with valid data

    ...

    codec.queueInputBuffer(inputBufferIndex, ...);

  }


 Buffer를 채워넣었다면 마지막 함수를 호출합니다. (위에 나와있는데 좀 더 자세히 보기위해서 분리하였습니다.)

 - indexdequeueInputBuffer 에서 return 받은 index 번호를 넣습니다.

 - offset : 항상 0이겠지만 Buffer에 채워넣은 데이터의 시작 점을 지정할 수 있습니다.

 - size : Buffer에 채워넣은 데이터 사이즈 정보

 - presentationTimeUs : 디코딩의 경우 Play 할 데이터의 시간(마이크로 초)

 - flags : 읽은 버퍼의 정보가 설정값인지 BUFFER_FLAG_CODEC_CONFIG, 마지막 데이터인지BUFFER_FLAG_END_OF_STREAM에 대한 정보를 초기화 할 수 있습니다.

              대부분은 0을 채워넣고 마지막 데이터를 알리기 위해서는 BUFFER_FLAGS_END_OF_STREAM을 넣습니다.

queueInputBuffer (int index, int offset, int size, long presentationTimeUs, int flags)



3. 디코딩 된 데이터 가져오기

 2번의 과정과 동일합니다. 함수 명만 output 이라는 이름만 포함되어 있습니다. InputBuffer index를 가져오는 방법과 동일한대 아래와 같이 OutputBuffer를 가져올 수 있습니다. outputBuffer에서는 BufferInfo라는 함수를 추가로 가져올 수도 있습니다.


BufferInfo API : http://developer.android.com/reference/android/media/MediaCodec.BufferInfo.html

BufferInfo에는 flags, size, offset, presentationTimeUs 정보가 포함되어 있습니다.

 int outputBufferIndex = codec.dequeueOutputBuffer(timeoutUs);

 또는  

 int outputBufferIndex = codec.dequeueOutputBuffer(timeoutUs, bufferInfo);


Output의 경우는 다음 4가지 사항이 나타나게 됩니다.

MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED

 Buffer 정보가 1번 변경되게 됩니다. API 21인 Lollipop 부터는 이 @deprecated 되었기에 불필요하지만 이전 API에서는 꼭 필요한 정보입니다. 이게 호출되면 처음에 생성한 ByteBuffer[] 배열의 변화가 일어나게 됩니다.


MediaCodec.INFO_OUTPUT_FORMAT_CHANGED

 처음에 생성하였든 MediaFormat을 기억하시는지요. 그 MediaFormat이 변경된 정보를 알려주게됩니다. 이 경우는 Encoder에서만 주로 사용하고, 디코더에서는 사용할 일은 없습니다.


MediaCodec.INFO_TRY_AGAIN_LATER

 이 함수가 호출되는 경우라면 사실 무시하여도 됩니다. 아래 새로 올라온 API 예제에는 해당 함수가 표시되지 않았으나. 필요에 따라 사용할 수 있습니다.


outputBufferIndex >= 0

 이 경우에 실제 디코딩 된 데이터가 들어오는 경우에 해당됩니다. 디코딩된 데이터를 초기화때 사용한 Surface에 바로 그리려면 아래와 같이 할 수 있습니다.

 2번째 함수에 true를 하게되면 Surface에 데이터를 바로 그리게 됩니다.

codec.releaseOutputBuffer(outIndex, true /* Surface init */);


또는 직접 데이터를 다룬다면 아래와 같이 할 수 있습니다.

해당 되는 ByteBuffer를 return 받고, 이를 활용하면 됩니다.

if (outputBufferIndex >= 0) {

  // if API level >= 21, get output buffer here

  ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferIndex);

  // outputBuffer is ready to be processed or rendered.

  ...

  codec.releaseOutputBuffer(outputBufferIndex, ...);

}



코덱의 종료

 열기가 있었으면 닫기도 필요합니다. MediaCodec은 네이티브로 동작합니다. 그래서 stop을 호출해주지 않으면 문제가 발생하게 됩니다. 아래와 같이 3개의 함수를 호출하게 되는데.. 실제로 따라가보면 codec.release()만 호출하여도 문제는 없습니다.

codec.stop();

codec.release();

codec = null;



간단하게 MediaCodec 사용방법을 알아보았습니다. 이제는 데이터를 읽어오는 API를 간단하게 살펴보겠습니다.



MediaExtroactor API 사용하기

 데이터를 가장 쉽게 불러올 수 있는 방법은 MediaExtroactor를 이용하는 방법입니다. 제한적이기는 하나 Android API에서 간단하게 MP4 동영상을 가져와 플레이 해보기에는 가장 좋은 방법입니다.

 참고 : 만약 MP4 동영상이 아니라면 H.264 데이터를 직접 input 하여 MediaCodec과 연결하여 사용하셔도됩니다.



MediaExtractor 초기화

 초기화는 간단합니다. setDataSource 함수에 직접 파일 경로를 셋팅할 수도 있고, xml에 있는 파일을 읽어올 수도 있습니다.

 MediaExtractor extractor = new MediaExtractor();

 extractor.setDataSource(...);



트랙 찾기

 MP4 파일에는 트랙이 1개만 있을 수도 있고, 여러개가 있을 수 있습니다. 가장 일반적인 mp4 파일은 Audio/Video를 가지는 2 트랙이 가장 많이 있습니다. 이중에서 비디오 데이터만 찾는 방법은 String 찾기를 통해서 할 수 있습니다.


일단 몇개의 트랙이 있는지를 찾습니다. 읽어들인 파일의 트랙 갯수는 getTrackCount() 함수를 통하면 됩니다.

 int numTracks = extractor.getTrackCount();


 읽어 드린 트랙 정보는 MediaFormat과 mime Type을 가지고 올 수 있습니다. 이 2가지 정보를 이용하여 위에서 설명한 MediaCodec에 각각 넣어주면 됩니다. 문제는 해당 Track이 Audio 인지 Video인지 알고 있어야 쉽게 사용이 가능하다는 점입니다.


 간단하게 mime.startWith("video/"); 함수를 통해서 Video 정보인지 구분하시면 되며, 해당 트랙을 사용할 때에는(비디오 트랙이라고 가정하고 사용한다면) selectTrack(index); 를 설정하면 됩니다.


 for (int i = 0; i < numTracks; ++i) {

   MediaFormat format = extractor.getTrackFormat(i);

   String mime = format.getString(MediaFormat.KEY_MIME);

   if (weAreInterestedInThisTrack) {

     extractor.selectTrack(i);

   }

 }



종료

 역시 사용을 완료하였다면 종료하는게 필요합니다.

 extractor.release();

 extractor = null;



2개의 API를 사용하여 간단한 예제는 github에 올려두었으나 여기에 아래와 같이 사용할 수 있습니다.

아래 예제는 Video 트랙을 찾아서 디코딩 준비하는 예입니다.

try {

mExtractor = new MediaExtractor();

mExtractor.setDataSource(filePath);

for (int i = 0; i < mExtractor.getTrackCount(); i++) {

MediaFormat format = mExtractor.getTrackFormat(i);

String mime = format.getString(MediaFormat.KEY_MIME);

if (mime.startsWith(VIDEO)) {

mExtractor.selectTrack(i); // Video 트랙을 선택합니다.

mDecoder = MediaCodec.createDecoderByType(mime); // Decoder를 초기화 합니다.

try {

Log.d(TAG, "format : " + format);

mDecoder.configure(format, surface, null, 0 /* Decoder */); // 사용할 Format 정보를 셋팅합니다.

} catch (IllegalStateException e) {

Log.e(TAG, "codec '" + mime + "' failed configuration. " + e);

return false;

}

mDecoder.start(); // 문제가 없다면 Decoder를 시작합니다.

break;

}

}

} catch (IOException e) {

e.printStackTrace();

}



마무리

 이를 사용한 디코딩 예제는 github에 올려두었으니 파일 경로만 변경하면 바로 테스트가 가능합니다. 싱크를 어느정도 맞게 작업해두어서 문제는 없으리라고 생각됩니다. 어디까지나 예제이니... ^^;

개인 광고 영역


댓글
  • 이전 댓글 더보기
  • 프로필사진 Jin 답변 감사합니다.
    g2 4.4.2 버전에서는 스트리밍 영상이 재생이 되는데.

    5.0 버전 이상에서는

    09-16 14:19:33.511: E/ACodec(13568): [OMX.qcom.video.decoder.avc] ERROR(0x80001009)
    09-16 14:19:33.512: E/ACodec(13568): signalError(omxError 0x80001009, internalError -2147483648)
    09-16 14:19:33.512: E/MediaCodec(13568): Codec reported err 0x80001009, actionCode 0, while in state 6
    09-16 14:19:33.615: E/AndroidRuntime(13568): FATAL EXCEPTION: Thread-19450
    09-16 14:19:33.615: E/AndroidRuntime(13568): Process: kr.cnsi.h264decode, PID: 13568
    09-16 14:19:33.615: E/AndroidRuntime(13568): java.lang.IllegalStateException
    09-16 14:19:33.615: E/AndroidRuntime(13568): at android.media.MediaCodec.native_dequeueInputBuffer(Native Method)
    09-16 14:19:33.615: E/AndroidRuntime(13568): at android.media.MediaCodec.dequeueInputBuffer(MediaCodec.java:983)
    09-16 14:19:33.615: E/AndroidRuntime(13568): at kr.cnsi.h264decode.VideoDecoderThread.run(VideoDecoderThread.java:243)

    이와 같은 에러 메세지가 발생하네요..
    g2 4.4.2에서 영상이 정상적으로 재생이 된다면 데이터 자체는 정상적이지 않은가 라는 판단을 하고 있는데.

    롤리팝 에서 돌릴 때만 이와 같은 문제가 발생하네요..
    2015.09.16 14:21 신고
  • 프로필사진 leehj 좋은 자료 유용하게 참고 하고있습니다!
    https://github.com/.../android/grafika/VideoEncoderCore.java
    위 example과 본 포스팅을 참고해서 레코딩을 진행하고 있는데 fast&slow mode에 관한 레퍼런스를 찾기가 어렵네요.(video자체를 배속,감속 하여 저장하려 합니다.)
    영상 encoding 후에 다시 decode를 하는 식으로 방향을 잡고 있는데 참고 할 레퍼런스나 혹은 도움을 좀 받을 수 있을까요?
    2015.10.01 16:55 신고
  • 프로필사진 BlogIcon taehwan 배속 감소는 시간으로 할 수는 있지만 실제 플레이어가 여러 해석 방법을 가지고 있어 적용되지 않을 수 있습니다.

    - H.264 데이터를 직접 로드 하여 순차적으로 재생
    이 경우는 시간 감속을 하더라도 적용되지 않게 되는것이죠.
    - MP4 파일의 시간 정보를 직접 로드 하여 재생
    이 경우라면 하실려고 하시는게 가능하시겠지만 이 방법으로 플레이하는 동영상 플레이는 많지 않습니다.

    만약 직접 디코딩과 인코딩 모두를 하신다면 .. 컨트롤이 가능하겠지만 2개중 하나만 하시는것이라면 힘들다고 생각됩니다.
    2015.10.11 14:06 신고
  • 프로필사진 tedahn 안녕하세요. 좋은 자료 올려주신 덕분에 많은 도움 받았습니다.
    궁금한 점이 있습니다. 영상을 거꾸로 (rewind) 재생을 시도하려 합니다.
    영상을 디코딩하며 정방향으로 재생하다 사용자가 입력을하면 입력한만큼 다시 뒤로 거꾸로 재생되게 하려 합니다.
    제가 생각하기엔 디코딩된 Bytebuffer 들을 모아놨다가 이걸 통해서 재생을 하려 하는데 이게 가능할까요??
    2016.06.10 11:40 신고
  • 프로필사진 BlogIcon taehwan 안녕하세요.

    말씀하신 ByteBuffer를 모아두었다가 거꾸로 돌리는건 가능은 합니다. 하지만 영상의 사이즈를 생각해주셔야 합니다. 디코딩된 이미지 한장은 가로*세로에 색상값에 대한 값이 포함됩니다. 대략 1080의 한장이라면 1920*1080*3/2(I420, NV21)의 사이즈를 가집니다. 초당 25FPS라고 한다면
    (1920*1080*3/2)*25=초당 7MB 이상의 용량을 가지게 될겁니다....

    rewind처리는 어떤식으로 하면 좋을지 생각나는게 없네요 현재는.... 그냥 거꾸로 프레임을 뽑아서 보여주는 식이 더 빠를것 같다는 생각입니다.
    2016.06.15 00:22 신고
  • 프로필사진 tedahn 감사합니다!!
    말씀해주신 방법이랑 비슷하게 약간의 선처리를 통해 리와인드 기능을 만들었습니다.
    많은 도움되었습니다. 정말 감사합니다!!
    2016.06.20 14:11 신고
  • 프로필사진 BlogIcon taehwan 오홋 성공하셨군요. 제가 직접적으로 해본게 아니라서 도움이 될지 몰랐네요^^; 2016.06.22 20:08 신고
  • 프로필사진 Bohemian 안녕하세요. 궁금한게 있어서 질문드립니다.
    지금 동영상을 업로드하는 기능을 추가하고 있는데, 그 전에 동영상의 사이즈(용량)을 줄이고 싶습니다.
    본 코드를 이용하여 할 수 있나요? 아님 혹시 참고할만한 내용이 있을까요?
    초보라 검색해도 어떻게 구현해야할 지 감이 잘 안잡히네요..
    2016.06.30 14:53 신고
  • 프로필사진 BlogIcon taehwan 동영상 사이즈를 변경하시려면 재인코딩이라는 방법을 사용하셔야 합니다.

    그렇지만 단말기에서 이 방법을 하시게 되면 백그라운드 처리가 되어야 하며, 동영상 용량을 줄이는 과정에서 손실이 발생합니다.

    일반적인 방법으로는 서버로 보내고, 이를 재인코딩하는 방식으로 접근하게 됩니다. 그래야 배터리라도 아낄 수 있으니.. 이렇게 하긴 합니다.

    추천 드리는 방법은
    - Wi-Fi 상태이거나(사용자 설정), 특정 용량 이하일 경우 네트워크로 전송하는 방법을 사용하셔도 좋습니다. 이렇게 다운받은 다음 서버에서 재인코딩 하는 방법을 사용하시는걸 추천합니다.

    재인코딩을 하시게 되면 화질이 낮아지게되어 원본 보전이 불가능해집니다.
    로컬 단말기에 용량도 확인해주셔야 합니다.(재인코딩을 하기 위한 용량)

    재인코딩을 하더라도 조건이 있습니다.
    - 화면 사이즈가 줄어야 합니다.
    - 비트레이트가 낮아져야 합니다.

    최소 위의 2가지가 허용되어야 용량이 줄어듭니다.
    대략 가로*세로*.. 값이 되면 비트레이트가 낮아지겠습니다.

    MediaCodec을 이용하시면 됩니다.
    2016.06.30 20:22 신고
  • 프로필사진 Bohemian 답변 감사합니다!
    목적은 카카오톡처럼 20mb 이하의 동영상만 업로드하고 싶은 것이었습니다. 그래서 화면 사이즈를 줄여 용량을 낮추고 전송하는 방법을 찾아보다 찾아오게 되었네요.
    알려주신 방법에 대해 찾아보겠습니다. :)
    2016.07.01 12:31 신고
  • 프로필사진 BlogIcon taehwan 카카오톡은 처음 찍을때 용량을 제한하긴 하는데요! 음 ㅋㅋ 재인코딩까지 하는지는 모르겠네요. 구글 CTS 코드에 살펴보시면 재인코딩하는 예제도 있습니다. 어렵지 않게 하실 수 있을것 같아요. 2016.07.01 14:34 신고
  • 프로필사진 Bohemian 안녕하세요! 혹시 한가지만 더 질문해도 될까요?
    구글 CTS 코드를 살펴봤는데도 잘 사용하는 방법을 모르겠더라구요ㅠㅠ 예제도 많아서 어떤게 제 상황에 어울리는지도 감이 안오고...

    현재 http://stackoverflow.com/questions/29943121/mediamuxer-video-compression-change-resolution 링크에 나와있는 VideoResolutionChanger 코드로 용량 줄이는데는 성공했는데요. 궁금한게 압축하는 과정을 view에 표시할 필요가 없으면 디코딩에서 surface를 null로 줘도 상관없지 않나요?
    해당 코드에서 InputSurface, OutputSurface 부분을 지워버리고 돌려보면 에러가 나네요.. 개념을 잘못 이해하고 있는건지..
    아 그리고 MediaMuxer가 API 18 이후부터 지원하던데, 그럼 그 이하 level은 동영상 압축이 불가능한가요?

    두서없는 질문에도 답변해주셔서 감사합니다!ㅜㅜ
    2016.07.07 11:38 신고
  • 프로필사진 BlogIcon taehwan 안녕하세요 추가 질문에 대한 답변 드리도록 하겠습니다.

    Surface를 셋팅해주셔야 합니다. 가상의 화면을 EGL로 생성해주시면 눈에 안보이게 할 수 잇습니다.
    Null로 하시면 .. OutputStream을 통해서 값이 넘어오게 되는데요. 이걸 직접 컨트롤 할 수 있긴 합니다.

    없으시면.. 화면 사이즈 변경에 대한 처리를 직접 해주셔야 합니다.

    2번째는
    디코딩을 기준
    - InputStream은 한개의 영상 프레임을 말합니다.
    - OutputStream은 한개의 디코딩된 프레임(완전한 이미지 한장을 말합니다.)

    인코딩을 기준
    - InputStream 압축할 데이터의 원본(Surface를 셋팅하면 Surface에 그려진 그림)
    - OutputStream 압축된 동영상 데이터가 나옵니다.

    그래서 Input/Output Stream이 없으면 당연히 모두 오류가 나게 됩니다.
    2016.07.10 21:29 신고
  • 프로필사진 WangBawoo 안녕하세요 올려주신 설명 잘 보고 열심히 따라해서 앱을 만드는 중인데 질문이 있습니다.
    저는 mideaExtactor와 같이 파일로 출력하지 않고 실시간으로 단말 화면을 인코딩을 하고 나서 만든 ByteBuffer 를 서버를 이용해 전달해서 실시간으로 다른 단말에서 받아 디코딩하여 SurfaceView 에 뿌리는 것을 하고 있는데요
    디코딩할 때 queueInputBuffer 부분에서 size랑 presentationtime 을 어떻게 해야할지 고민이 됩니다
    인코딩할때 Buffer info 의 size랑 presentationtime 까지 한번에 전달하는게 만만치않아서요
    인코딩해서 만들어진 ByteBuffer 만으로 size랑 presentationtime을 알수는 없을까요?
    2016.07.20 16:03 신고
  • 프로필사진 BlogIcon taehwan 안녕하세요. 서버에서 받을때 데이터를 쪼개서 내려줄탠데요... 그 인코딩된 데이터가 1프레임이 될려면 쪼개진 데이터를 합쳐야 디코딩이 가능할겁니다.

    그 과정에서 인코딩된 데이터의 첫 4바이트가 아마 사이즈일겁니다. 그리고 인코딩시에 시간을 가지고 있을탠데요.. 그걸 그대로 받으시면 될것 같은데...;;

    대충 구조가

    서버에서 데이터를 받을려면
    - 시간, 인코딩된 Byte의 사이즈, 실제 Byte
    요런 형태로 전달 받으시면 될것 같은데... 어려운 부분이 있을지 모르겠네요. 만약 이게 실시간 데이터라면 시간을 서버에서 내려주는게 좋겠구요.
    이미 인코딩된 데이터라면... 더 쉽게 시간을 가져올 수 있습니다...

    단순 H.264를 디코딩하는 걸 보시려면 EXoPlayer 같은 오픈 소스를 참고해보세요.
    2016.07.24 21:26 신고
  • 프로필사진 WangBawoo 아 감사합니다 제가 잘못 생각하고있었네요
    인코딩한 Byte bufffer에서 byte array와 offset , size로 wrap 메서드를 이용해 전달하여 해결했습니다
    2016.07.25 11:00 신고
  • 프로필사진 Ayoung 뭐쫌 여쪄볼려구요~
    Mediacodec으로 mp3파일을 decode 한 다음에 버퍼에 저장되잖아요 그것을 노래재생될때 파형보여주듯이 보여주는
    opensorce (어플: https://github.com/google/ringdroid)가 있는데. 이때 x축은 초가 기준이라는 것을 알겠는데 y축은 디코딩 했던것을 그냥 가져와서 사용하더라구요. 그렇게 사용이 가능한지.. 그때는 y축이 뭐가 기준이 되나요. 그냥 값을 가져와서 픽셀 단위로 출력을 시키더라구요..
    2016.08.30 17:44 신고
  • 프로필사진 BlogIcon taehwan 해당 부분은 제가 답을 드릴수가 없네요. 저도 좀 봐야할것 같습니다. 2016.09.04 00:38 신고
  • 프로필사진 궁그미 안녕하세요
    .h264에 대한 데이터를 문자열로 받아왔는데요 ㅠㅠ
    그것에 대한 것을 어떻게 해결해줘야 할 지 감이 안와요..
    위에 소스를 확인해봤는데 파일 경로로 되어있는데 어떻게 수정해야 할 지 잘 모르겠습니다.
    2016.09.05 14:52 신고
  • 프로필사진 BlogIcon taehwan 안녕하세요. .h264의 데이터를 문자열로 받아왔다는게 String으로 받으셨다는건가요?

    일단 .h264는 최소 byte로 받으셔야 합니다. 문자열로 받으시면 원본 데이터가 망가진 상태가 됩니다.
    2016.09.07 13:46 신고
  • 프로필사진 궁그미 네네 현재 라즈베리파이에서 영상을 찍은 다음에 그 영상데이터 .h264를 String으로 전송했는데 이렇게 보내면 안되고 byte로 변경해서 전송해야 한다는 뜻인가여? 2016.09.16 21:01 신고
  • 프로필사진 BlogIcon taehwan String으로 전송하셨다는게... 어떤식으로 전송하셨는지 이해가 안되고 있네요..

    실시간 스트리밍의 경우 스트리밍 프로토콜이 따로 있습니다. 그런 자료들을 보시거나 단순히 전송하시려면 TCP를 이용하셔서 보내시면 됩니다.... 데이터 형태는 byte 그대로 보내셔야겠죠....
    2016.09.18 22:01 신고
  • 프로필사진 궁그미 음 현재 글의 내용의 예제는 파일경로에 있는 동영상을 열어서 재생하는 것인데

    제가 구현하고 싶은 내용은 라즈베리파이에서 .h264파일을 암호화해서 String으로 APP에서 전송을 받으면 복호화를 하게 됩니다. 그러면 String으로 남게 되는데 위의 예제에서 filepath를 통하여 파일여는데 그 부분을 생략하고 그 밑의 쓰레드 start부분부터 시작하면 되는 것 인가요??ㅠㅠ
    2016.09.20 20:44 신고
  • 프로필사진 BlogIcon taehwan 아 .. 넵 파일 여는 부분은 불필요합니다.

    h.264 프레임을 전달 받으셨다면 그걸 복호화 하신다음 그대로 data에 셋팅해주시면 됩니다. 그러면 그게 디코딩이 진행되고, 플레이가 되는것입니다.

    암호화를 나중에 처리하시는게 좋다고 생각됩니다. 검증 과정을 거치신 다음 하는게 가장 좋은 방법입니다!^^; 그리고 파일로 저장해서 플레이도 가능하니깐요. 암호화 처리는 나중으로 미루시고

    1. H.264를 전달해서 디코딩만 해본다.
    2. 만약 안되면 H.264를 보내기전에 저장해서 h.264 플레이어를 통해 재생해본다.
    3. TCP로 받은 데이터를 저장해서 디코딩 해본다.
    4. 이상이 없다면 암호화를 추가한다.

    저는 이러한 방법을 추천드립니다.
    2016.09.21 01:15 신고
  • 프로필사진 궁그미 계속 질문드려서 죄송합니다.
    밑에가 저의소스인데요..

    text가 이제 입력받은 .h264의 데이터입니다.

    해당 코드에서
    ByteBuffer[] inputBuffers = codec.getInputBuffers();
    ByteBuffer[] outputBuffers = codec.getOutputBuffers();

    이 부분은 안쓰이고 있는데 제가 다른 방향으로 접근하고 있는건가요?ㅠㅠ

    아니면 저 부분에 제가 입력 받은 String데이터를 연결시켜줘야 되는 것 같은데 그 방법이 감이 안잡힙니다 ㅠㅠ



    public boolean init(Surface surface, String text) {
    String mime = "video/avc";
    content = text;
    try {
    format = MediaFormat.createVideoFormat(mime, 300, 300);
    codec = MediaCodec.createDecoderByType(mime);
    codec.configure(format, surface, null, 0);
    codec.start();
    start();
    } catch(Exception e) {
    Log.e("TAG", "에러";);
    }

    return true;
    }

    @Override
    public void run() {
    // if API level <= 20, get input and output buffer arrays here
    ByteBuffer[] inputBuffers = codec.getInputBuffers();
    ByteBuffer[] outputBuffers = codec.getOutputBuffers();
    int inputBufferIndex = codec.dequeueInputBuffer(10000);
    boolean first = false;
    long startWhen = 0;
    BufferInfo info = new BufferInfo();

    if (inputBufferIndex >= 0) {
    // if API level >= 21, get input buffer here
    // ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferIndex);
    byte[] array = content.getBytes();
    ByteBuffer inputBuffer = ByteBuffer.wrap(array);
    // ByteBuffer inputBuffer = inputBuffers[inputIndex];
    // fill inputBuffers[inputBufferIndex] with valid data

    mExtractor = new MediaExtractor();
    int sampleSize = mExtractor.readSampleData(inputBuffer, 0);


    if (mExtractor.advance() && sampleSize > 0) {
    codec.queueInputBuffer(inputBufferIndex, 0, sampleSize, mExtractor.getSampleTime(), 0);

    } else {
    Log.d(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM";);
    codec.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
    // isInput = false;
    }

    int outIndex = codec.dequeueOutputBuffer(info, 10000);
    switch (outIndex) {
    case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
    Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED";);
    codec.getOutputBuffers();
    break;

    case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
    Log.d(TAG, "INFO_OUTPUT_FORMAT_CHANGED format : " + codec.getOutputFormat());
    break;

    case MediaCodec.INFO_TRY_AGAIN_LATER:
    // Log.d(TAG, "INFO_TRY_AGAIN_LATER";);
    break;

    default:
    if (!first) {
    startWhen = System.currentTimeMillis();
    first = true;
    }
    try {
    long sleepTime = (info.presentationTimeUs / 1000) - (System.currentTimeMillis() - startWhen);
    Log.d(TAG, "info.presentationTimeUs : " + (info.presentationTimeUs / 1000) + " playTime: " + (System.currentTimeMillis() - startWhen) + " sleepTime : " + sleepTime);

    if (sleepTime > 0)
    Thread.sleep(sleepTime);
    } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }

    codec.releaseOutputBuffer(outIndex, true /* Surface init */);
    break;
    }

    // All decoded frames have been rendered, we can stop playing now
    if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
    Log.d(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM";);
    // break;
    }
    }

    codec.stop();
    codec.release();
    mExtractor.release();



    }
    2016.09.26 15:42 신고
  • 프로필사진 BlogIcon taehwan 답변이 많이 늦었네요....

    byte를 전달 받으셔서 처리하는 경우에는

    output은 의미 없구요. - output은 서피스가 될것이니...

    inputdata에 담으시면 됩니다.

    음 .. 5.0 이후 부터는 inputButters를 사용하지 않아도 되는것이지.. inputBuffers를 사용하지 않는건 아닙니다. 다시 말해서... 데이터를 입력하시는 부분에 별도로
    ByteBuffer[] inputBuffers = mDecoder.getInputBuffers();

    을 선언하지 않으셔도 된다는 말이되겠습니다^^;;;;
    2016.10.09 01:47 신고
  • 프로필사진 myungji 파일을 여는 경우가 아닐 때
    ByteBuffer[] inputBuffers = codec.getInputBuffers();
    ByteBuffer[] outputBuffers = codec.getOutputBuffers();
    밑의 코드는 쓰이지 않는 것 같은데 그렇다면
    어떻게 data의 값을 불러와줘야할지 궁금합니다.
    2016.10.04 19:55 신고
  • 프로필사진 BlogIcon taehwan 파일이든 아니든 .. ByteBuffer[] inputBuffers = mDecoder.getInputBuffers();을 해야하는건 맞습니다. 다만 5.0 이전 버전이라면 해줘야 하고, 5.0 이후 버전이라면 해당 부분을 제거하여도 좋습니다. 2016.10.09 01:48 신고
  • 프로필사진 LIM 안녕하세요
    블로그를 잘 읽었습니다.

    제가 decoding 되면서 해당 frame을 5개씩 뛰어가면서 인코딩을 하고싶은데요.
    if (nowFrame <5 ) {
    bufferInfo.presentationTimeUs = Time;
    muxer.writeSampleData(indexMap.get(trackIndex), dstBuf, bufferInfo);
    Time+=33333;
    }
    로하면 깨지듯이 저장되서 해당 dstBuf를 bitmap으로 저장해보고자 합니다.
    byte[] imageBytes= new byte[dstBuf.remaining() ];
    dstBuf.get(imageBytes);
    Bitmap bitmap = BitmapFactory.decodeByteArray( imageBytes, 0, imageBytes.length );

    이렇게 저장해봤지만 아무파일도 안써지더라구요.
    혹시
    저렇게 프레임을 점프뛰면서 인코딩을 할수있는 방법이있을까요?
    몇일을 고민해도 답이안나오네요 ㅠ


    아니면 surface view에 원하는 프레임만 뿌리게할수있는 방법이있을까요?
    2016.10.15 15:06 신고
  • 프로필사진 BlogIcon taehwan 안녕하세요.

    5프레임을 뛰어서 저장을 하고 싶으시다면 다음과 같이 하셔야 합니다.

    디코딩을 5장을 모으신 다음 인코딩 1번을 호출하셔야 합니다.

    디코딩/디코딩/디코딩/디코딩/디코딩
    이렇게 5번째에서
    인코딩 호출 1회

    를 해주셔야 합니다.

    단순히 muxer에서 5프레임을 버리시면 동영상이 깨지게 됩니다. 그리고 첫 프레임은 무조건 적으로 있어야 합니다.

    그래서 다시 정리하면

    - 디코딩을 5번 하고, 인코딩을 호출한다.
    - Muxer에서는 데이터를 버리는 일이 없어야 한다.(한프레임이라도 버리시면 파일이 깨집니다.)

    파일이 안써지시는건... nowFrame 처리가 잘못되신거겠죠. if (nowFrame < 5) 라면.. 5보다 작을동안 파일에 쓰는건데.. 코드도 조금 확인해보셔야 할것 같습니다.

    감사합니다.
    2016.10.16 22:38 신고
  • 프로필사진 Aquabenedict 바쁘신 도중 실례하지만 질문드립니다.
    지금 안드로이드 미디어 코댁을 이용해서 이미지파일(jpg)를 영상(mp4)화 하는 어플을 제작중입니다.
    그래서 JPG->BMP->YUV420으로 변경하는것까지는 됬는데 그후 인코딩과 디코딩이 어떻게 움직이는게
    이해가 되지 않습니다. 인코딩과 디코딩에 대해서 여러가지 조사해봤지만 프로그램 관점에서 적혀있는글은
    찾지 못해서 개념도 잘 이해가 되지 않습니다.
    실례하지만 왜 인코딩을 하고 디코딩을 하는지 만약 인코딩 디코딩 작업을 하면 어떤방식으로
    작업해야되는데 어드바이스 부탁드립니다.
    아래가 지금 작업중인 소스코드입니다

    package com.example.suzukiyc.hellocamera;

    import android.media.MediaCodecList;
    import android.media.MediaMuxer;
    import android.media.MediaCodec;
    import android.media.MediaCodecInfo;
    import android.media.MediaFormat;
    import android.os.Environment;
    import android.util.Log;


    import java.io.File;
    import java.io.IOException;
    import java.nio.ByteBuffer;

    import static android.R.attr.duration;

    public class AvcEncoder {

    private static final String MIME_TYPE = "video/avc";
    private static final File OUTPUT_DIR = Environment.getExternalStorageDirectory();

    private int mWidth = -1;
    private int mHeight = -1;
    private int mBitRate = -1;
    private static final int FRAME_RATE = 15;
    private static final int IFRAME_INTERVAL = 10;

    private MediaCodec mediaCodec;
    private MediaMuxer mediaMuxer;
    private MediaCodec.BufferInfo bufferInfo;
    public int mTrackIndex;

    private byte[] sps;
    private byte[] pps;

    //16真数を守るために
    private void setParameters(int width, int height, int bitRate) {
    if ((width % 16) != 0 || (height % 16) != 0) {
    Log.w("setParameter", "WARNING: width or height not multiple of 16";);
    }
    mWidth = width;
    mHeight = height;
    mBitRate = bitRate;
    }

    public AvcEncoder(byte[] outData) {

    try {
    prepareEncoder();
    offerEncoder(outData);
    } finally {
    releaseEncoder();
    }
    }

    public void offerEncoder(byte[] input) {
    try {
    ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();


    int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
    if (inputBufferIndex >= 0) {
    ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
    inputBuffer.clear();
    inputBuffer.put(input);
    mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, 0, 0);
    }

    ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
    int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, -1);
    while (outputBufferIndex >= 0) {
    if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
    outputBuffers = mediaCodec.getOutputBuffers();
    } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
    MediaFormat videoFormat = mediaCodec.getOutputFormat();

    // now that we have the Magic Goodies, start the muxer
    mTrackIndex = mediaMuxer.addTrack(videoFormat);
    mediaMuxer.start();
    } else {
    ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
    mediaMuxer.writeSampleData(mTrackIndex, outputBuffer, bufferInfo);
    }
    mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
    }
    } catch (Throwable t) {
    t.printStackTrace();
    }
    }

    private void prepareEncoder() {
    bufferInfo = new MediaCodec.BufferInfo();

    int colorFormat;
    try {
    colorFormat = selectColorFormat(MIME_TYPE);
    } catch (Exception e) {
    colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar;
    }

    MediaFormat mediaFormat;
    setParameters(320, 240, 125000);
    mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
    mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);
    mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
    mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
    mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);

    try {
    mediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
    mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    mediaCodec.start();
    } catch ( IOException e ) {
    e.printStackTrace();
    }

    // Output filename. Ideally this would use Context.getFilesDir() rather than a
    // hard-coded output directory.
    String outputPath = new File(OUTPUT_DIR,
    "test." + mWidth + "x" + mHeight + ".mp4";).toString();

    // Create a MediaMuxer. We can't add the video track and start() the muxer here,
    // because our MediaFormat doesn't have the Magic Goodies. These can only be
    // obtained from the encoder after it has started processing data.
    //
    // We're not actually interested in multiplexing audio. We just want to convert
    // the raw H.264 elementary stream we get from MediaCodec into a .mp4 file.
    try {
    mediaMuxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
    } catch (IOException ioe) {
    throw new RuntimeException("MediaMuxer creation failed", ioe);
    }

    mTrackIndex = -1;
    }

    /**
    * Releases encoder resources. May be called after partial / failed initialization.
    */
    private void releaseEncoder() {
    if (mediaCodec != null) {
    mediaCodec.stop();
    mediaCodec.release();
    mediaCodec = null;
    }
    if (mediaMuxer != null) {
    mediaMuxer.stop();
    mediaMuxer.release();
    mediaMuxer = null;
    }
    }


    /**
    * Returns the first codec capable of encoding the specified MIME type, or
    * null if no match was found.
    */
    private MediaCodecInfo selectCodec(String mimeType) {
    int numCodecs = MediaCodecList.getCodecCount();
    for (int i = 0; i < numCodecs; i++) {
    MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);

    if (!codecInfo.isEncoder()) {
    continue;
    }

    for (String type : codecInfo.getSupportedTypes()) {
    if (type.equalsIgnoreCase(mimeType)) {
    Log.i("selectCodec", "SelectCodec : " + codecInfo.getName());
    return codecInfo;
    }
    }
    }
    return null;
    }

    /**
    * Retruns a color format that is supported by the codec and by this test
    * code. If no match is found, this throws a test failure -- the set of
    * formats known to the test should be expanded for new platforms.
    */
    protected int selectColorFormat(String mimeType) {
    MediaCodecInfo codecInfo = selectCodec(mimeType);
    if (codecInfo == null) {
    throw new RuntimeException("Unable to find an appropriate codec for " + mimeType);
    }

    MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType);
    for (int i = 0; i < capabilities.colorFormats.length; i++) {
    int colorFormat = capabilities.colorFormats[i];
    if (isRecognizedFormat(colorFormat)) {
    Log.d("ColorFomar", "Find a good color format for " + codecInfo.getName() + " / " + mimeType);
    return colorFormat;
    }
    }
    return -1;
    }

    /**
    * Returns true if this is a color format that this test code understands
    * (i.e. we know how to read and generate frames in this format).
    */
    private boolean isRecognizedFormat(int colorFormat) {
    switch (colorFormat) {
    // these are the formats we know how to handle for this test
    case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
    case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
    case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
    case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
    case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar:
    return true;
    default:
    return false;
    }
    }

    }



    ----------------------------------------------------------------------------------------
    package com.example.suzukiyc.hellocamera;

    import android.graphics.Bitmap;

    /**
    * Created by suzuki.yc on 2016/10/17.
    */

    public class bitmaptoYUV420 {
    byte [] getNV21(int inputWidth, int inputHeight, Bitmap scaled) {
    int [] argb = new int[inputWidth * inputHeight];

    // Returns in pixels[] a copy of the data in the bitmap.
    // https://developer.android.com/reference/android/graphics/Bitmap.html#getPixels(int[], int, int, int, int, int, int)
    scaled.getPixels(argb, 0, inputWidth, 0, 0, inputWidth, inputHeight);

    byte [] yuv = new byte[inputWidth * inputHeight * 3/2];
    encodeYUV420SP(yuv, argb, inputWidth, inputHeight);

    scaled.recycle();

    return yuv;

    }

    void encodeYUV420SP(byte[] yuv420sp, int[] argb, int width, int height) {
    final int frameSize = width * height;

    int yIndex = 0;
    int uvIndex = frameSize;

    int a, R, G, B, Y, U, V;
    int index = 0;
    for(int j = 0; j < height; j++) {
    for (int i = 0; i < width; i++) {

    a = (argb[index] & 0xff000000) >> 24;
    R = (argb[index] & 0xff0000) >> 16;
    G = (argb[index] & 0xff00) >> 8;
    B = (argb[index] & 0xff) >> 0;

    // well know RGB to YUV alogithm
    Y = (( 66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
    U = (( -38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
    V = (( 112 * R - 94 * G - 18 * B + 128) >> 8) + 128;

    // NV21 has a plane of Y and interleaved planes of VU each sampled by a factor of 2
    // meaning for every 4 Y pixels there are 1 V and 1 U. Note the sampling is every other
    // pixel AND every other scanline.
    yuv420sp[yIndex++] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y));
    if (j % 2 == 0 && index % 2 == 0) {
    yuv420sp[uvIndex++] = (byte)((V<0) ? 0 : ((V > 255) ? 255 : V));
    yuv420sp[uvIndex++] = (byte)((U<0) ? 0 : ((U > 255) ? 255 : U));
    }

    index ++;
    }
    }
    }
    }

    2016.10.18 06:48 신고
  • 프로필사진 BlogIcon taehwan 안녕하세요.

    코드를 봐드릴순 없구요...

    JPG를 BMP로 가져오시구요. 이를 SurfaceView에 그리거나, I420/NV21으로 변환하셔야 합니다.

    안드로이드에서는 I42/NV21을 사용하지 YUV420을 사용하지 않습니다. 생삭값 공간이 모두 서로 상의합니다.

    디코딩과 인코딩 개념은 아래와 같습니다.

    디코딩 : 압축된 연속된 데이터를 이미지 처럼 완전한 이미지를 만드는 것
    인코딩 : 이미지를 공통점을 찾아서 이전 프레임과 다음 프레임의 차이점만을 가질 수 있도록 압축하는 방법

    위와 같습니다. 2개의 개념을 이해하시고 다음의 자료를 추가로 보시면 좋을것 같습니다.

    http://thdev.net/577
    2016.10.23 21:10 신고
  • 프로필사진 Aquabenedict 설명 감사합니다 ^^
    열심히 만들어볼게요!
    2016.10.28 00:09 신고
  • 프로필사진 BlogIcon taehwan 감사합니다! 2016.10.30 14:07 신고
  • 프로필사진 JUNG 안녕하세요, 먼저 블로그 글 너무 잘 읽었습니다. 덕분에 mediacodec을 사용하여 디코딩 하는 개념을 잘 이해한 것 같습니다.
    질문 드릴 것은 네트워크 상에서 byte[] 이미지(h264 raw image)를 받아서 디코딩 후 화면에 띄우려고 합니다.
    MediaCodec으로 디코딩/인코딩처리 시 duqueuInputBuffer나 dequeueOutputBuffer를 사용하여 내부적으로 Queue를사용한다는 것은 알았는데 그렇다면, 네트워크 상에서 받은 byte[] 데이터를 어떻게 queue에 넣어야 할까요?
    정보를 찾아보면 sps나 pps 정보를 활용하던데, 이 부분을 찾아봐야 하는 걸까요?
    답변 부탁드립니다!
    2016.12.27 15:18 신고
  • 프로필사진 BlogIcon taehwan 안녕하세요. 모든 동영상의 최초 첫번째 프레임에는 SPS/PPS 정보가 담겨져 있습니다.

    매우 작은 수십바이트가 이에 해당하게 됩니다.

    압축하는 코덱에 따라서 이 정보의 길이는 달라질수 있는데요 16 이상입니다.

    해당 정보부터해서 하나의 프레임(I-프레임/P-프레임)에 대한 정보를 InputBuffer에 담아주시면 되겠습니다.

    그렇게되면 바로 디코딩이 시작되게 됩니다.
    - 이전에는 당연히 동영상 플레이가 가능한 형태가 만들어져야겠죠.(미디오 코덱 초기화)

    그리고 OutputBuffer의 경우 별도 처리하실게 없습니다. SurfaceView로 이를 대체할 수 있습니다.
    만약 직접 버퍼를 건드리셔야 한다면... 그건 아주 힘든일이 됩니다. 가능은 하지만 단순 동영상을 하실때는 우선 SurfaceView에 출력하는 형태를 먼저 해보시고, 이해하신 다음 다음을 넘어가는게 좋습니다.

    다시 정리하면
    - 서버 소켓을 통해서 넘어온 데이터는 Queue에 담아야 합니다.
    - 서버에서 잘라서 보낼 수도 있기 때문에 하나의 프레임 정보는 그 규격에 맞게 하시면 되겠죠.

    예를 들어서 32byte씩 잘라서 보낸다면 1개의 완전한 프레임이 만들어질 때까지 이를 반복적으로 처리하게 하시면 되겠습니다.(이 때는 NDK로 하시는게 좋습니다.)

    - 하나의 프레임이 다 받아지면, mediaCodec의 InputBuffer에 한프레임을 추가합니다.
    - 이후 처리는 output의 경우 SurfaceView에 바로 출력하게 되는 형태이실 태니.. 따로 할건 없습니다.

    결국 앞단에 받아오는 정보를 잘 활용하시면 재생하는데 문제가 없게됩니다.
    특히 SPS/PPS에 대한 정보는 MP4 코덱에서 가장 먼저 제공하는 정보입니다. H.264 데이터로 넘어오는 경우에는 재생에 문제가 없지만

    mp4인 경우에는 header가 맨 끝에 있을 수 있으므로, 서버에서 읽을때 잘 파악하셔야겠습니다.

    감사합니다.
    2016.12.29 00:56 신고
  • 프로필사진 JUNG 답 주셔서 정말 감사합니다!
    서버에서 받아 디코딩해서 띄우기 전에
    먼저 동영상 파일(.mp4)을 MediaExtrator를 사용하지 않고 디코딩하여 띄우는 테스트를 해보려 합니다.
    MediaFormat에서 재생하려는 동영상의 SPS/PPS를 set 해주었습니다.
    MediaExtractor를 사용하는 예제에서는 영상의 frame을 buffer에 넣는 작업을 extractor가 하는데
    (int sampleSize = extractor.readSampleData(buffer, 0); 이 부분이 아닌가요?)
    extractor를 사용하지 않고 동영상 프레임 정보를 buffer에 넣어 주는 방식은 어떤 게 있는지
    알 수 있을 까요?

    감사합니다!
    2017.01.03 18:41 신고
  • 프로필사진 BlogIcon taehwan 안녕하세요.

    네 해당 부분입니다.

    mDecoder.queueInputBuffer(inIndex, 0, sampleSize, mExtractor.getSampleTime(), 0);

    그런데 비디오 데이터는 inputByteBuffer에 담아주셔야 합니다.

    SPS/PPS 데이터를 inputByteBuffer에 담아서 그냥 추가해주시면 됩니다.

    샘플은 Audio에서 제가 만들어두었는데 참고하시면 될것 같습니다.

    https://github.com/taehwandev/MediaCodecExample/blob/master/src/net/thdev/mediacodecexample/decoder/AudioDecoderThread.java
    2017.01.09 00:17 신고
  • 프로필사진 JUNG 답변 감사합니다! 2017.01.10 13:42 신고
  • 프로필사진 JUNG 또 질문 드립니다.
    PC에서 h264로 인코딩된 프레임을 안드로이드에서 실시간으로 받아서 디코딩 후 뿌려주는 프로젝트를 진행하고 있습니다.
    pc가 서버가되고 안드로이드가 클라이언트가 되서 한프레임씩 계속 받아서 띄우고있습니다.
    하지만 영상이 계속 계져서 받아지고 있는 상태입니다.
    (디버깅하면서 한 프레임씩 테스트를 할 때는 깨짐없이 잘 보입니다.-->통신시 문제는 없어보입니다.)

    메인액티비티와 프레임을 받는 통신 쓰레드와 디코딩을 하는 쓰레드로 이루어져있습니다.

    통신을 통해 받은 프레임을 통신쓰레드에 전달할 때에는,
    통신쓰레드와 디코딩 쓰레드 사이에 LinkedBlockingQueue를 사용하여 전달 해주고 있는데요.
    처음엔 쓰레드간에 속도가 문제인가 해서 큐에 조금 쌓인후에 디코딩을 시작하라고 디코더 쓰레드에 슬립을 줘보기도 했구요.

    BlockingQueue를 사용한게 잘못인 걸까요?
    어느 부분이 의심이 가는지 얘기해 주시면 감사하겠습니다.
    2017.02.20 15:52 신고
  • 프로필사진 BlogIcon taehwan 안녕하세요.

    서버로부터 받은 데이터는 프레임을 통째로 보내신건지 아니면 한 프레임씩 무조건 던지신건지가 중요합니다.

    일단 비디오 정보는 1개의 Frame이 하나의 데이터입니다.

    예를 들면

    head(4byte)|데이터(1024byte)|데이터(640byte)

    이런식이라면 디코딩을 위해서는 head 4byte만 전달되어야 하며, 다음에는 데이터 1024byte만 전달되어야 합니다.

    그래서 아래와 같이 해주시는게 좋겠죠.

    byteSize|head(4byte)|byteSize|데이터(1024byte)|byteSize|데이터(640byte)

    이렇게 해주셔야겠죠. 그래야 누락되는 데이터가 없겠습니다.

    그리고 당연히 Queue에 쌓을때는 앞에있는 byteSize 정보(int)데이터는 날려주셔야 합니다.

    다시 정리하면

    1개의 프레임은 완전한 인코딩 데이터여야 합니다.
    앞에 붙여주는 4byte(int) 정보는 1개의 데이터를 만들기 위한 데이터이고, 그에 맞는 실제 enc byte가 queue에 쌓여야 합니다.
    2017.02.22 00:56 신고
  • 프로필사진 hoi 안녕하세요 , 잘 보고 공부하고 있습니다.
    동영상을 가져와서 재생하는것까지는 됐는데
    소리가 안나오네요. 소리도 같이 재생하고 싶으면 어떻게 해야 하나요?
    VideoDecoderThread.java 의 init에서
    MediaFormat format = MediaFormat.createVideoFormat("video/avc",1920, 1080);
    mDecoder = MediaCodec.createDecoderByType("video/avc";);
    mDecoder.configure(format, surface, null, 0);
    mDecoder.start();
    로 for문을 대체했는데 코덱이 열렸다 바로 닫히네요..
    조언 부탁드립니다. 감사합니다~!
    2017.07.26 21:28 신고
  • 프로필사진 BlogIcon taehwan 해당 코드는 오디오에 대한 디코딩 설정이 아니고, 비디오에대한 디코딩 초기화입니다. 오디오 디코딩 코덱의 종류에 맞게 다시 작성하셔야합니다 2017.08.09 09:38 신고
  • 프로필사진 LEE 안녕하세요.
    너무 유익한 정보 제공해주셔서 감사합니다.
    그런데 한 가지 궁금한 점이 있어서 질문드립니다.

    codec.configure (MediaFormat format, Surface surface, MediaCrypto crypto, int flags);
    codec.releaseOutputBuffer(outIndex, true /* Surface init */);
    H.264 video를 디코딩 할 때 위와 같은 코드를 사용하면 디코딩된 화면이 서피스에 그려지는데요.
    만약에 디코딩된 화면의 일부분만 서피스에 그리자고 하면 어떻게 해야할까요?

    codec.getOutputBuffer(outputBufferIndex);
    위 메소드를 통해서 bytebuffer를 얻어와서 원하는 처리를 한 다음에 서피스에 어떠한 방법으로 그려야할지 궁금합니다.

    조언부탁드립니다.^^;;


    2017.08.21 16:03 신고
  • 프로필사진 BlogIcon taehwan 일부가 어떠한 일부인가요? 2017.09.26 12:48 신고
  • 프로필사진 LEE 디코딩된 픽쳐의 사이즈가 w * h라고 하면, 서피스 크기에 맞게 픽쳐가 스케일링되서 보여지는데요. 일부 즉 (w - m) * (h - n) 크기의 영역만 서피스 크기에 맞게 보여지게 하고 싶습니다. 2017.10.31 17:39 신고
  • 프로필사진 BlogIcon taehwan 그 부분은 서피스의 크기를 w * h로 잡으신 다음 그려질 이미지의 좌표를 이동하시는 방법을 사용해야 할것 같습니다. OpenGL쪽을 참고하셔야 하겠네요 2017.10.31 23:52 신고
  • 프로필사진 LEE 답변 감사합니다. OpenGL은 접해본적이 없어서 생소하지만 공부해봐야겠네요^^;; 2017.11.01 09:51 신고
  • 프로필사진 BlogIcon taehwan 서피스로 그리고 있으시기 때문에 OpenGL로 해주셔야 할거에요.
    기본 View의 크기는 영상의 크기로 잡지 마시구요. 원하시는 w x h로 잡아주시고, 아마 그냥 그리시면 왼쪽 상단부터 0,0 으로 잡혀서 이미지가 짤려서 나올거에요
    그 부분을 센터를 기준으로 View의 센터에 맞게 해주시면 원하시는대로 나오실거에요
    2017.11.01 11:41 신고
  • 프로필사진 fivehome mediacodec의 getOutPutBuffer(index)에 대해 문의가 있습니다.
    여기서 나오는 ByteBuffer를 byte[]에 넣어 yuv데이터를 받아 저장하려고 합니다.
    현재 cctv 1920*1080 프리뷰를 surface view에 하고 있는 viewer가 동작하고 있습니다.
    이 뷰어에서 디코딩된 데이터를 받고 싶은데

    byte[ ] temp = new byte[1920*1080*2];
    ByteBuffer ret;

    ret = codec.getOutputBuffer(index);
    ret.get(temp, 0, temp.length); 또는 ret.get(temp, 0, ret.reaming()) 하면 데이터를 가져
    오지 못합니다.
    ret의 capacity가 8로 나오는데 제 생각에는 1920*1080*2 만큼 또는 전체 데이터 사이즈 만큼 capacity가
    나와야 될 것으로 보이는데 8이 나와 정상동작하지 않습니다.

    이부분 도움 부탁 합니다.
    2017.11.28 15:22 신고
  • 프로필사진 BlogIcon taehwan 이미 surfaceView에 그리고 있으시면 byte을 추가로 받으실 수 없을거에요(최근에 확인 못해봐서 정확하지는 않지만.) 디코딩 데이터에서도 처음에는 head가 포함이구요. 그 head 사이즈는 압축 방식에 따라 다 다를거에요. 그다음 부터 오는 바이트가 한장씩 들어오는거구요.

    사이즈는 YUV(종류가 다양합니다.)/RGB 압축 방식에 따라서 이들이 결정되는데 그에 따른 결과가 나오게됩니다. 그래서 사이즈는 1920*1080*YUV 종류에 따른 계산 결과가 나올겁니다.

    일단 SurfaceView에 바로 하지마시고 yuv로 받으신 다음 중간 처리할껀 처리하신 다음 Surface에 던지시는 방법도 있습니다. 또는 거꾸로 서피스에서 받아오는 방법도 있구요 API 21부터 가능합니다.(이때는 RGB입니다.)
    2017.12.08 11:40 신고
  • 프로필사진 와우 이부분 공부중이였는데 정말 정리가 잘되있어서 놀랐습니다 ㅋㅋㅋ
    내공이 좀 있으신분 같네요 ㅎㅎ 우선 글잘보고 간다는 말씀 드리고싶습니다.
    한가지 궁금한게 있습니다.
    MediaCodec 초기화시 컬러포맷을 COLOR_FormatSurface으로 설정하고 createInputSurface()를 통해 그 Surface가 받아오는 byte값을
    MediaCodec.Callback의 onOutputBufferAvailable() 콜백함수를 통해 받고있습니다.
    아 MIME_TYPE은 video/avc이구요.
    이때 여기서 받아오는 byte[]값이 h.264 raw데이터인지 맞다면 그형식이 yuv인지 궁금하네요.. 혹시 아니면 다른어떤값인지 궁금합니다.
    2018.05.30 00:03 신고
  • 프로필사진 BlogIcon taehwan 안녕하세요. 네 outPutBuffer로 전달되는 데이터는 H.264 - raw 데이터이고, 기본 YUV입니다.
    정확히는 YUV420 의 색상값을 가지고있습니다. YUV 주사 방식이 수십가지가 있는데 모바일에서는 420을 사용합니다.(폰마다 다를 수 있음)

    다만 H.264 스펙에는 RGB도 가능한걸로 나와있습니다. 모바일에서는 그렇게 압축하지는 않구요
    2018.06.03 15:27 신고
  • 프로필사진 와우 우선 답변 감사드립니다.
    그렇다면 H.264-raw이란 단어를
    이 데이터가 H.264로 인코딩된 raw데이터라고 이해해야 하는거겠죠??
    혹시 맞다면 인코딩되기전 raw데이터를 뽑아올수있는 방법이 있을지 여쭙고싶습니다.
    2018.06.08 11:32 신고
  • 프로필사진 BlogIcon taehwan 인코딩 전의 raw 데이터는 RGB/RGBA/ARGB/RGB 등의 색상 정보겠죠. 그냥 bitmap으로 저장하시면 그게 그냥 raw 데이터를 압축하는거죠. 2018.06.08 21:11 신고
  • 프로필사진 와우 어.. 제 질문은 인코딩 되기전 색상정보raw데이터를 배열로 가지고올수있을까 라는 질문이였습니다 ㅎㅎ
    혹시 가능할까요??
    2018.06.11 10:06 신고
  • 프로필사진 BlogIcon taehwan 서피스뷰 사용하시는거면 6.0부터인가 서피스 캡쳐가 가능해요. 그게 아니라면 완전한 RAW는 직접 바이트 인코딩한게 아니라면 어렵죠 2018.06.14 13:20 신고
댓글쓰기 폼
Total
5,172,838
Today
1,158
Yesterday
1,713
«   2018/08   »
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  
글 보관함