ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • MediaCodec - Encoding
    개발/안드로이드 2020. 6. 21. 19:48

     

    디코딩이 비디오 정보를 분해하는 작업이었다면 인코딩은 역으로 새로운 비디오를 만드는 작업이다. 포토샵이나 비타 같은 비디오 에디터를  이용해 기존의 비디오의 화질을 줄이고 영상에 자막을 입히거나 스티커를 붙이는 작업 모두 인코딩 작업의 일환이라고 볼 수 있다. 비디오 파일마다 갖고 있는 고유의 속성인 FPS, Bitrate, 가로 세로 크기 모두 인코딩 작업에서 설정된다.

     

    앞서 말한 것 처럼 인코딩 작업은 광범위한데 이번 포스트에서는 인코딩의 가장 기본적이라 할 수 있는 예제인 기존에 디코딩한 비디오를 다시 똑같은 비디오로 인코딩하는 작업으로 설명해보려고 한다. 앞선 포스트에서 인코딩에 대해 설명했었는데 인코딩에서도 디코딩과 비슷한 작업이 많아 중복되는 내용에 대해서는 생략하고 이 포스트의 주제인 인코딩에 대해서 중점적으로 설명하려고 한다. 예제코드로 구글 미디어코덱 CTS 에 사용한 코드를 참고했다. 구글 코드 답지 않게 리팩토링도 덜되어있고(테스트코드니까) 알아보기가 쉽지는 않으니 이 글이 이 코드를 분석하려는 분들에게 많은 도움이 됐으면 좋겠다.

     

    1. Create 

     

    디코딩 작업과 비슷하게 인코딩도 MediaCodec을 이용해서 인코딩을 담당할 객체를 생성한다. MediaCodec.createByCodecName() 을 통해 객체를 생성하고 configure를 통해 구체적인 정보를 설정한다. 옛날 코드에서는 createByCodecName에 넣는 인자는 인코딩해서 생성할 비디오의 압축 방식을(대표적으로 mpeg인 video/avc 가 있다) 설정한다. configure의 첫번째 인자로 outputVideoFormat이 들어가는데 outputVideoFormat의 세팅 작업을 보면 대충 무엇을 하고 있는지 감이 올 것이다. 비디오의 비트레이트, 프레임레이트를 설정해주는 단계다. 이렇게만 설정을 해주면 알아서 이 값에 맞게 비디오가 만들어진다.

     

    outputVideoFormat.setInteger(
    	MediaFormat.KEY_COLOR_FORMAT, OUTPUT_VIDEO_COLOR_FORMAT);
    outputVideoFormat.setInteger(MediaFormat.KEY_BIT_RATE, OUTPUT_VIDEO_BIT_RATE);
    outputVideoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, OUTPUT_VIDEO_FRAME_RATE);
    outputVideoFormat.setInteger(
    	MediaFormat.KEY_I_FRAME_INTERVAL, OUTPUT_VIDEO_IFRAME_INTERVAL);
    if (VERBOSE) Log.d(TAG, "video format: " + outputVideoFormat);
    // Create a MediaCodec for the desired codec, then configure it as an encoder with
    // our desired properties. Request a Surface to use for input.
    AtomicReference<Surface> inputSurfaceReference = new AtomicReference<Surface>();
    videoEncoder = createVideoEncoder(
    	videoCodecInfo, outputVideoFormat, inputSurfaceReference);
        
    private MediaCodec createVideoEncoder(
        MediaCodecInfo codecInfo,
        MediaFormat format,
        AtomicReference<Surface> surfaceReference)
        throws IOException {
        MediaCodec encoder = MediaCodec.createByCodecName(codecInfo.getName());
        encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        // Must be called before start() is.
        surfaceReference.set(encoder.createInputSurface());
        encoder.start();
        return encoder;
    }

     

    2. Handle Buffer

     

    2.1 Get Input Buffer

     

    디코딩 작업과 비슷하게 버퍼를 처리하는 루틴을 가진다. 디코딩에서는 비디오 파일을 뽑아와서 OutputSurface 와 같은 뷰에 넣어주었다면 인코딩 작업에서는 새롭게 비디오로 만들 비디오 프레임 버퍼 정보를 핸들링 하게 된다. 인코더가 버퍼를 받아오는 부분은 코드로 바로 설명하는 것은 어려우니 먼저 아래 그림을 참고하도록 하자.

     

     

    분석이 쉽지 않았다

     

     

    인코더는 Surface를 통해서 비디오에 인코딩할 버퍼 정보를 받아오게 된다. 아래 그림 오른쪽 상단위 InputSurface가 인코더에 넣을 정보를 전송하는 곳이다. 전반적인 흐름을 설명하자면 디코더에서 받아온 정보는 잘게잘게 쪼게져서 OutputSurface로 이동하고 이 정보를 OutputSurface 내의 객체에서 호출한 OpenGL 코드를 통해 OpenGL Thread 메모리 영역에 저장한다. 여기서 그려지는 정보는 TextureRender를 거쳐서 인코더가 버퍼를 받을 수 있도록 매핑된 객체는 InputSurface로 이동하게 되는데 인코더는 여기서 받아온 정보를 MediaMuxer를 통해서 비디오 파일을 생성하게 된다.

     

    아래 코드는 위 그림에서 인코더에게 버퍼를 전달하는 부분만 추출한 것이다. drawImage는 현재 디코더에서 받아온 정보를 실제 그림으로 그리는 코드다. 이 그림 버퍼는 앞서 설명한 것 처럼 고유한 OpenGL Thread 메모리 영역에 저장된다. 바로 다음에 이뤄지는 setPresentationTime 함수는 현재 프레임 버퍼가 차지하게 되는 시간대를 설정하는 함수다. 디코더 정보에 마이크로 세컨드 정보가 포함되어 있어서 이 정보를 통해 어디에 위치해야할 지 알 수 있다. 최종적으로 swapBuffers를 통해서 버퍼 정보를 인코더에 전달한다.

     

    if (VERBOSE) Log.d(TAG, "output surface: await new image");
    outputSurface.awaitNewImage();
    if (VERBOSE) Log.d(TAG, "output surface: draw image");
    outputSurface.drawImage();
    inputSurface.setPresentationTime(
    	videoDecoderOutputBufferInfo.presentationTimeUs * 1000);
    if (VERBOSE) Log.d(TAG, "input surface: swap buffers");
    inputSurface.swapBuffers();
    if (VERBOSE) Log.d(TAG, "video encoder: notified of new frame");

     

    * 여기서 OpenGL에 해당하는 클래스와 함수는 설명을 생략했다. 기초적인 OpenGL 지식이 있어야하고 설명하려면 밑도 끝도 없을 것 같아서.... 무엇보다 필자가 아직 OpenGL을 잘 모르는 것이 문제다. 간단한 정보만 설명하자면 CTS 코드에서는 받아온 비디오 정보랑 똑같이 입력할 수 있도록 구현해둔 상태다. 여기 있는 값을 잘만 이용하면 비디오에 자막과 워터 마크도 입힐 수 있고 비디오 크롭, 스케일 값도 조정하는 이펙트도 넣을 수 있다. 이런 기능을 구현해보고 싶으신 분은 OpenGL 코드를 공부해보면 좋을 것 같다.

     

    * 꼭 OpenGL을 이용해서 정보를 전달하지 않는 방법도 있다. 대표적으로 오디오에서는 디코더에서 직접 인코더의 InputBuffer에 값을 넣어준다. 예제로 사용한 파일에서 오디오에대한 인코딩 작업도 있으니 관심있는 분은 참고해보시길!

     

    2.2 Handle Buffer 

     

    버퍼를 처리하는 부분은 디코더랑 꽤 비슷하다. 디코더에서는 빼낸 정보를 output surface에다가 넣었다면 인코더에서는 비디오를 생성할 수 있는 muxer라는 객체에 넣는 점만 다르다. 코드를 한번 살펴보자. swapBuffers로 넘어온 정보는 videoEncode Output 버퍼에 쌓여 있는데 dequeueoutputBuffer를 통해서 이 정보가 저장된 인덱스 정보를 얻어오고 인덱싱을 통해 ByteBuffer로 구체적인 정보를 받아온다. 디코딩처럼 Index 정보가 유효하지 않는 경우에는 무시하고 작업을 진행하도록 한다. 

     

    받아온 ByteBuffer에는 버퍼의 크기와 플래그가 포함되어 있는데 플래그 비트 중 CODEC_CONFIG 값이 포함되면 무시하도록 한다. 이 값을 muxer에 포함시키면 비디오가 실행이 안되니 주의하도록 하는게 좋다. 사이즈 값이 0이 아니라면 비디오에 포함될 수 있는 유효한 정보라고 본다. writeSampleData 함수를 통해 인코딩할 비디오 정보를 넣은 후 버퍼 메모리를 release 해준다.

     

    int encoderOutputBufferIndex = videoEncoder.dequeueOutputBuffer(
    	videoEncoderOutputBufferInfo, TIMEOUT_USEC);
    if (encoderOutputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
    	if (VERBOSE) Log.d(TAG, "no video encoder output buffer");
    		break;
    }
    if (encoderOutputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
    	if (VERBOSE) Log.d(TAG, "video encoder: output buffers changed");
    	videoEncoderOutputBuffers = videoEncoder.getOutputBuffers();
    	break;
    }
    if (encoderOutputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
    	if (VERBOSE) Log.d(TAG, "video encoder: output format changed");
    	if (outputVideoTrack >= 0) {
    		fail("video encoder changed its output format again?");
    	}
    	encoderOutputVideoFormat = videoEncoder.getOutputFormat();
    	break;
    }
    
    ByteBuffer encoderOutputBuffer = videoEncoderOutputBuffers[encoderOutputBufferIndex];
    if ((videoEncoderOutputBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG)	!= 0) {
    	// Simply ignore codec config buffers.
    	videoEncoder.releaseOutputBuffer(encoderOutputBufferIndex, false);
    	break;
    }
    if (videoEncoderOutputBufferInfo.size != 0) {
    	muxer.writeSampleData(
    		outputVideoTrack, encoderOutputBuffer, videoEncoderOutputBufferInfo);
    }
    if ((videoEncoderOutputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM)!= 0) {
    	if (VERBOSE) Log.d(TAG, "video encoder: EOS");
    	videoEncoderDone = true;
    }
    videoEncoder.releaseOutputBuffer(encoderOutputBufferIndex, false);
    videoEncodedFrameCount++;

     

    2.3 Muxer 

     

    Muxer에 대한 설명을 빼뜨렸는데, MediaMuxer는 새로운 비디오를 만들어줄 수 있는 클래스다. 첫번째 인자로 비디오로 생성될 파일의 경로와 이름을 파일 클래스를 통해서 넣어주고 두번째 인자로 생성될 비디오 파일의 확장자(mp4)를 설정해준다. 이것만 해주면 된다. 이렇게 선언만 해두고 인코더에서 받아온 버퍼 정보를 writeSampleData로 넣어주기만 하면 된다.

    private MediaMuxer createMuxer() throws IOException {
    	return new MediaMuxer(mOutputFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
    }

     

    2.4 release 

     

    디코딩과 마찬가지로 인코딩에 사용한 작업들도 release 해주는 단계가 필요하다. muxer와 encoder 객체 뿐만 아니라 인코더에 버퍼를 전달할 때 사용한 inputSurface까지 해줘야한다. 안해준다고 비디오가 안생성되는 것은 아니지만 메모리 정보는 잊지 않고 해제해주는것이 좋다.

     

     

    '개발 > 안드로이드' 카테고리의 다른 글

    status bar 영역 덮는 view 만들기  (0) 2020.06.24
    Lottie 라이브러리  (0) 2020.06.24
    MediaCodec - Decoding  (0) 2020.05.24
    MediaCodec - Getting Started  (1) 2020.05.24
    Navigator - Getting Started  (0) 2020.04.20

    댓글

Designed by Tistory.