Search

'Encoder'에 해당되는 글 2건

  1. 2021.04.30 AudioTrack: Discontinuity detected
  2. 2020.06.21 MediaCodec - Encoding

AudioTrack: Discontinuity detected

카테고리 없음 2021. 4. 30. 17:53 Posted by 아는 개발자

몇몇 영상에 대해 Exoplayer 라이브러리에서 AudioTrack: Discontinuity detected [expected 61128344, got 60909659] 에러를 뿜고 있었다. 영상도 렉이 걸리고 소리도 끊김이 있어서 한참 Exoplayer 라이브러리를 디버깅했었는데 이건 플레이어의 문제가 아니라 영상 파일이 문제였다. 사운드 인코딩 과정에서 버퍼 처리를 잘못해서 생긴 문제였는데 자세한 설명을 위해 아래 그림을 참조해보자.

 

 

트랜스코딩 과정은 Decoder에서 출력되는 Decoder Buffer를 Encoder Buffer에 복사하고 다시 Encoder에 입력하는 과정으로 이뤄진다. 위 그림에서 보면 Encoder Buffer와 Decoder Buffer모두 사이즈가 2048이기 때문에 Encoder에서는 빈 공간 없이 사운드 스트림을 채울 수 있다.

 

그런데 몇몇 기종에서는 Encoder Buffer의 크기와 Decoder Buffer 크기가 다르기도한다.  아래 그림은 encoder buffer의 기본 크기가 4096이고 Decoder Buffer의 기본 크기는 2048이다. 별다른 처리를 하지 않으면 Encoder Buffer는 앞부분에만 버퍼를 채우게 된다.

 

 

그러면 최종적으로 인코딩된 사운드의 스트림은 아래 그림처럼 중간중간에 빈 공간이 남아있게 되고, 결과적으로 불연속 오디오 로그가 띄게 되는 것이다. 플레이어에서는 받은 그대로 처리하기 때문에 소리도 이상하게 출력될 수 밖에 없다.

 

 

그러면 해결하는 방법은? 간단하다. Encoder Buffer를 꽉꽉 채워서 인코딩 하는 것이다. 아래 그림처럼 encoder와 decoder 버퍼의 크기가 다르다면 다음 것을 받아서 채워 넣은 다음 Encoder에 넣는다. 버퍼의 크기는 2의 제곱으로 떨어지기 때문에 나눠떨어지는 문제는 염려하진 않아도 된다. 

 

반대로 Decoder Buffer가 Encoder Buffer가 더 큰 경우도 있다. 이런 경우 불연속 에러는 발생하지 않겠지만 소리가 이상하게 들리게된다. 해결 방법은 비슷하다. 앞에선 Encoder Buffer에 두개의 decoder 버퍼를 담았다면 이번엔 Decoder Buffer를 쪼개서 넣는다. 아래 그림을 보면 Decoder 버퍼 앞부분은 Encoder Buffer 1, 뒷부분은 Encoder Buffer 2에 담아서 넣는다. 이런 방식이다.

 

 

버그를 많이 경험하다보니 강제로 인코딩 디코딩을 많이 배우게 된 것 같다. 물론 내가 배운게 아직 전부는 아니기 때문에 더 배워야할 것은 많지만 예전처럼 헤메지는 않게 된 것 같아서 기분은 좋다

728x90

MediaCodec - Encoding

개발/안드로이드 2020. 6. 21. 19:48 Posted by 아는 개발자

 

디코딩이 비디오 정보를 분해하는 작업이었다면 인코딩은 역으로 새로운 비디오를 만드는 작업이다. 포토샵이나 비타 같은 비디오 에디터를  이용해 기존의 비디오의 화질을 줄이고 영상에 자막을 입히거나 스티커를 붙이는 작업 모두 인코딩 작업의 일환이라고 볼 수 있다. 비디오 파일마다 갖고 있는 고유의 속성인 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까지 해줘야한다. 안해준다고 비디오가 안생성되는 것은 아니지만 메모리 정보는 잊지 않고 해제해주는것이 좋다.

 

 

728x90

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

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