RxJava - combineLatest

컴퓨터공부/안드로이드 2020. 6. 28. 17:44 Posted by 아는 개발자

 

combineLatest 함수는 모든 Observable 소스에서 이벤트를 받은 후 마지막으로 받은 소스의 시점에서 새로운 이벤트를 만들고 싶을 때 사용한다. 아래 그림을 보면 두개의 소스를 받고 있고 각각 마지막으로 추가된 작업에 대해서 마다 새로운 이벤트를 형성해주고 있다.

 

 

위와 같은 구조는 모든 데이터를 받고 난 다음에 한번에 업데이트 하고 싶은 경우나 하나의 소스가 변경될 때마다 최종적으로 입력된 Observable 소스를 불러오고 싶은 경우 유용하다. 추상적으로 서술해서 감이 잘 오지 않는데 각 경우의 예를 생각해보면 이해하기 쉬울 것 같다. 

 

첫번째의 예로는 서버에서 받은 정보를 앱의 화면에 노출할 때로 볼 수 있을 것 같다. 컨텐츠 페이지에서 페이지의 정보와 댓글 리스트를 화면에 뿌려줘야 하는데 이 리스트가 다른 api로 분기되어 있다면 각각을 분리해서 호출해야 하고 화면 업데이트도 한번에 이뤄지지 않아 따닥따닥 이뤄질 수 있다. 이렇게 분리된 경우는 로딩 아이콘 표시하는 작업도 각 호출별로 설정해줘야해서 번거로운데 이럴 때는 combineLatest 구조를 이용해서 모든 소스를 받고 난 다음에 업데이트 하도록 두 소스를 묶어줄 수 있다.

 

코드로 표현하면 이렇다. Flowable의 combineLatest 함수로 묶어서 댓글과 컨텐츠 리스트를 Flowable 소스로 받고 모든 소스가 도달 했을 때 loading 이 종료되었음을 false로 알릴 수 있다.

 

Flowable.combineLatest(
    getReplyList(),
    getContentList(),
    BiFunction<List<String>, List<String>, Pair<List<String>, List<String>>> {
        replyList, contentList ->
        Pair(replyList, contentList)
    }
)
    .doOnNext { loading.onNext(false) }
    .doOnNext { pair -> 
        val replyList = pair.first
        val conentList = pair.second
    }
    .subscribe()
    
private fun getReplyList(): Flowable<List<String>> = Flowable.fromCallable {
    listOf("Hello", "World", "RxJava", "Flowable")
}

private fun getContentList(): Flowable<List<String>> = Flowable.fromCallable {
    listOf("This", "is", "developer")
}

 

두번째 경우로는 웹사이트에서 로그아웃이 된 경우 새로고침시 파라미터로 넣을 정보가 Observable 소스로 된 경우다. 로컬 저장소에 유저의 id를 이용해 로그인 유무를 판단하고 유저 상태 변경시 홈 api를 호출해야 하는데 이때 Observable 소스로 된 로컬 토큰 정보가 필요하면 id 가 변경될 때마다 token 정보를 새롭게 받도록 combineLatest로 묶어줄 수 있다. 코드로 표현하면 아래와 같다. 

 

Flowable.combineLatest(
    id().distinctUntilChanged(),
    token(),
    BiFunction<Long, String, Pair<Long, String>> { id, token ->
        Pair(id, token)
    }
)
    .flatMap { pair -> 
        val id = pair.first
        val token = pair.second
    
        home(id, token)
    }
    .subscribe()

 

combineLatest로 묶을 수 있는 소스의 개수는 최대 9개까지 가능하다. 그런데 이정도로 많은 소스를 combineLatest로 묶는 경우는 거의 희박할 뿐더러 관리하기도 어렵다. 가능하면 3, 4개 까지 사용하는게 코드 유지 관리 측면에서도 좋다.

 

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

RxJava - debounce  (0) 2020.07.04
RxJava - observeOn, subscribeOn  (0) 2020.06.28
RxJava - combineLatest  (0) 2020.06.28
status bar 영역 덮는 view 만들기  (0) 2020.06.24
Lottie 라이브러리  (0) 2020.06.24
MediaCodec - Encoding  (0) 2020.06.21

status bar 영역 덮는 view 만들기

컴퓨터공부/안드로이드 2020. 6. 24. 22:57 Posted by 아는 개발자

 

안드로이드 런처 화면 상단을 보면 status bar 영역을 배경 영역이 덮고 있는 것을 볼 수 있다. 이렇게 status bar 영역까지 확장할 경우 유저에게 꽉찬 느낌을 줄 수 있어서 틱톡을 비롯한 몇몇 앱에서도 활용하고 있다. 이번 포스트에서는 이렇게 status bar 영역까지 확장하는 방법을 소개한다.

 

 

1. AppTheme 설정

 

windowActionBar, windowNoTitle 속성을 추가하고 이 style 값을 activity에서 사용하도록 한다.

 

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
	<!-- Customize your theme here. -->
	<item name="colorPrimary">@color/colorPrimary</item>
	<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
	<item name="colorAccent">@color/colorAccent</item>

	<item name="windowActionBar">false</item>
	<item name="windowNoTitle">true</item>
</style>

 

2. FullScreen 형태로 변경 

 

activity 또는 fragment에서 현재 window의 systemUiVisibility 플래그 값을 View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 로 변경하고 statusBarColor를 투명으로 바꿔준다. 

this.window?.apply {
    this.statusBarColor = Color.TRANSPARENT
    decorView.systemUiVisibility =
        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN

}

 

이렇게 두가지 작업만 적용해도 아래 처럼 status bar 영역까지 차지하게 되는 것을 볼 수 있다. 그런데 앱에서 차지하고 있는 공간이 거의 흰색에 가깝다 보니까 status bar 아이콘들이 잘 보이지 않는 문제점이 있다. 

 

3. Status Bar Icon 색 변경

 

이런 경우에 직접 status bar 아이콘의 색깔을 어둡게 설정해줄 수 있다. View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 플래그를 추가하면 아이콘이 light 배경일 때 사용할 수 있도록 어두운 아이콘으로 나오게 된다.

 

this.window?.apply {
    this.statusBarColor = Color.TRANSPARENT
    decorView.systemUiVisibility =
        View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN

}

 

적용하면 아래의 그림처럼 아이콘 색이 변한다

 

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

RxJava - observeOn, subscribeOn  (0) 2020.06.28
RxJava - combineLatest  (0) 2020.06.28
status bar 영역 덮는 view 만들기  (0) 2020.06.24
Lottie 라이브러리  (0) 2020.06.24
MediaCodec - Encoding  (0) 2020.06.21
MediaCodec - Decoding  (0) 2020.05.24

Lottie 라이브러리

컴퓨터공부/안드로이드 2020. 6. 24. 19:18 Posted by 아는 개발자

 

서비스 출시 막판 디자인 작업에선 다이나믹하고 아기자기한 애니메이션 효과를 주고 싶어하는 디자이너와 물리적 효과 구현의 어려움과 잠재적인 에러 때문에 주저하는 개발자 사이에 보이지 않는 갈등이 존재하곤 했었는데, 손쉽게 애니메이션 효과를 줄 수 있는 Lottie 라이브러리가 나오면서 조금이나마(?) 해소됐다. Lottie 라이브러리는 기존 Animator 클래스에 있는 함수와 속성 변수를 거의 그대로 가져다 쓰면서도 애니메이션 효과로 디자이너 분이 작업해둔 json 파일만 붙여두면 되기 때문에 사용하기가 정말 쉽다. 이번 포스트에서는 lottie json 파일을 안드로이드에서 사용하는 방법에 대해서만 간단히 소개하려고한다.

 

1. 라이브러리 임포트 

 

현재 최신 버전은 3.4.0 까지 릴리즈 됐다. build.gradle에 추가해준다

dependencies {
  implementation 'com.airbnb.android:lottie:$lottieVersion'
}

 

2. XML 파일에 Lottie 추가

 

새롭게 추가된 속성은 app:lottie_fileName 정도다. 여기에 Lottie 애니메이션 효과를 줄 파일을 넣으면 된다. 이 파일은 프로젝트에 app/src/main/assets 에 넣어두면 된다. 여기에 안넣어두면 이 파일 못찾는다고 앱이 크래쉬 날 수 있으니 주의하자.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <com.airbnb.lottie.LottieAnimationView
        android:id="@+id/fr_lottie_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:lottie_fileName="img_lottie.json"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

 

3. 자바 파일에서 애니메이션 실행 

 

앞서 XML 파일에서 선언해둔 뷰 오프젝트를 직접 실행 해주면 된다. 상황에 따라서 어느 시점에 이 애니메이션을 실행할지 결정할 수 있다. 메인쓰레드에서 playAnimation() 함수만 실행하면 자동적으로 동작한다.

 

class LottieFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        fr_lottie_view.playAnimation()
    }

실행해보면 아래 gif 처럼 실행된다.

4. 여러가지 옵션 

 

단순히 애니메이션을 실행하는 것 뿐만 아니라 여러가지 옵션을 줄 수 있다. 옵션도 무수히 많은데 자주쓰는 것들만 소개해보면 이런 것들이 있다.

 

4.1 무한 재생하기

 

애니메이션을 몇번 재생할지 결정하는 변수인데 이 값을 -1로 주면 무한대로 재생하게 할 수 있다. 로딩 애니메이션을 표현할때 유용하다

 

fr_lottie_view_infinite.apply { repeatCount = -1 }

 

4.2 거꾸로 재생하기

 

애니메이션 스피드를 조정할 수 있는 변수인데 이 값을 -1f로 설정하면 거꾸로 재생할 수 있다. 값은 -1f ~ 1f 사이이고 물론 속도도 조절 가능 하다.

fr_lottie_view_reverse.apply { speed = -1f }

 

두 효과를 적용한 결과 아래 그림처럼 하나는 무한대로, 다른 하나는 반대로 돌아가게 된다

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

RxJava - combineLatest  (0) 2020.06.28
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

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까지 해줘야한다. 안해준다고 비디오가 안생성되는 것은 아니지만 메모리 정보는 잊지 않고 해제해주는것이 좋다.

 

 

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

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

MediaCodec - Decoding

컴퓨터공부/안드로이드 2020. 5. 24. 13:09 Posted by 아는 개발자


MediaCodec을 사용하기 위해선 기본적인 비디오 영상 처리에 대한 지식이 필요하다. 영상처리도 깊이 들어가면 한도 끝도 없을 것 같은데 이 포스트에서는 MediaCodec 을 이용해 Decoding 할 때 반드시 알고 있어야 하는 지식 정도로만 간추려서 소개하려고 한다.

 

여기서 사용한 예제 코드는 grafika https://github.com/google/grafika 저장소의 MoviePlayer.java 코드에서 deprecated 된 부분만 바꿨다. 실제로 동작하는 코드를 확인하고 싶다면 여기서 프로젝트를 받아 실행해보면 될 것 같다

 

1. Definition

 

Decoding은 영상 파일이 가지고 있는 정보를 추출해내는 작업이다. 우리가 흔히 볼 수 있는 영상플레이어들은 모두 비디오 파일을 읽고 이 정보를 화면에 뿌리는 디코딩 작업을 거친다. 디코딩 작업에는 화면 프레임 정보 뿐만 아니라 비디오가 가지고 있는 음성 파일도 포함한다. 영상에서 긁어온 정보들을 컨트롤해서 화면에 뿌려주는게 영상플레이어의 역할이다. 안드로이드에서는 MediaCodec 라이브러리를 통해 영상과 음성에 대해서 디코딩을 할 수 있다.

 

2. Create

 

MediaCodec 라이브러리를 이용해서 디코딩 작업을 담당하는 객체를 생성할 수 있다. 아래 코드에서 createDecoderByType 함수가 Decoder를 생성하는 함수다. 생성 전에 수행하는 작업을 볼 필요가 있는데, 앞의 MediaExtractor 클래스가 하는 역할은 소스 파일에서 비디오의 Meta 정보를 가져오는 역할을 한다. 비디오가 가지고 있는 Meta 정보로는 비디오의 bitrate, width, size 등등을 가져올 수 있는데 디코딩 작업을 할때는 비디오의 현재 압축 방식인 MIME이 필요하다. 이 압축방식은 비디오의 확장자마다 다른데 거의 모든 영상을 mp4로 담고 있는 현재는 대부분 h264 방식을 따르고 있다. 정확히 이게 어떤 방식으로 압축되는지는 나도 잘 모른다. 아무튼 이 정보를 통해 비디오 디코더를 읽어올 수 있다.

 

extractor = new MediaExtractor();
extractor.setDataSource(mSourceFile.toString());
int trackIndex = selectTrack(extractor);
if (trackIndex < 0) {
	throw new RuntimeException("No video track found in " + mSourceFile);
}
extractor.selectTrack(trackIndex);

MediaFormat format = extractor.getTrackFormat(trackIndex);

// Create a MediaCodec decoder, and configure it with the MediaFormat from the
// extractor.  It's very important to use the format from the extractor because
// it contains a copy of the CSD-0/CSD-1 codec-specific data chunks.
String mime = format.getString(MediaFormat.KEY_MIME);
decoder = MediaCodec.createDecoderByType(mime);
decoder.configure(format, mOutputSurface, null, 0);
decoder.start();

 

decoder.configure 함수의 첫번째 인자로는 아까 추출한 압축 방식을 전달했고 두번째 인자로 mOutputSurface라는 값을 전달하고 있다. mOutputSurface는 decoder로 받은 정보를 화면에 뿌려줄 도화지 역할을 하는데 위와같이 configure함수에 두번째 인자로 넣으면 디코딩 된 정보를 자동으로 화면에 뿌려줄 수 있게 된다. 

 

2. Extract Data

 

다음으로 할 작업은 디코더를 이용해 실제로 비디오 파일에서 영상 정보를 추출하는 것이다. 아래 그림은 MediaCodec에 관한 구글 개발자 문서에서 가져온 것인데, 코덱의 일종인 디코더는 영상을 가져올때 크게 input 작업과 output 작업을 거친다. 초록색으로 칠해진 클라이언트는 비디오에서 정보를 읽는 작업이고, 작은 정사각형을 채워넣는 작업은 앞서 클라이언트에서 읽어온 정보를 디코더 버퍼에 읽어온 채워넣는 작업이다. 이것 모두 개발자가 해야하는 일이다. 이 작업도 아래의 그림처럼 크게 두가지 단계로 정리해볼 수 있을 것 같다.

 

 

 

2.1 Input Phase

 

비디오에서 읽어온 정보를 Input 버퍼에 채워넣는 일이다. 코드를 하나하나 살펴보자. dequeueInputBuffer 함수는 받아온 나중에 받아올 정보를 채워너 넣을 수 있는 공간을 할당받는 함수다. 리턴값으로 index를 주는데 이 index 값은 엄청 길다란 배열의 index 값으로 생각하면 된다. 이 index 값을 받아서 데이터를 쓸 위치를 넣을 수 있다.

 

다음 작업으로는 비디오에서 데이터를 읽어오는 작업이다. 여기서 사용된 extractor 변수는 앞서 생성 작업에서 선언한 변수와 동일하다. 객체 내에 읽은 부분에 대한 iterator가 포함되어 있어서 어디까지 읽었는지 정보를 담고 있다. 코드 맨 마지막에 advance 함수를 통해 읽을 위치를 변경하는 것이 가능하다.

 

마지막으로 extractor에서 뽑아온 정보를 Input 버퍼에 넣어야한다. 앞어 읽어온 sample data의 리턴 값에 따라서 Input 버퍼에 넣는 정보가 다른데 이 값이 마이너스인 경우에는 모든 데이터를 읽은 경우이기 때문에 Input Buffer에 플래그 값으로 END_OF_STREAM을 넣어준다. 그 외의 경우에는 유효한 데이터인 것으로 보고 Input Buffer에 넣고 플래그 값을 0으로 넣는다. 함수의 네번째 인자로 시간 정보 값을 주는데 이 값은 현재 읽어온 버퍼가 비디오에서 몇초대에 위치하고 있는지에 대한 정보다.

 

int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);
if (inputBufIndex >= 0) {
    if (firstInputTimeNsec == -1) {
        firstInputTimeNsec = System.nanoTime();
    }

    ByteBuffer inputBuf = decoder.getInputBuffer(inputBufIndex);
    // Read the sample data into the ByteBuffer.  This neither respects nor
    // updates inputBuf's position, limit, etc.
    int chunkSize = extractor.readSampleData(inputBuf, 0);
    if (chunkSize < 0) {
        // End of stream -- send empty frame with EOS flag set.
        decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L,
                MediaCodec.BUFFER_FLAG_END_OF_STREAM);
        inputDone = true;
        if (VERBOSE) Log.d(TAG, "sent input EOS");
    } else {
        if (extractor.getSampleTrackIndex() != trackIndex) {
            Log.w(TAG, "WEIRD: got sample from track " +
                    extractor.getSampleTrackIndex() + ", expected " + trackIndex);
        }
        long presentationTimeUs = extractor.getSampleTime();
        decoder.queueInputBuffer(inputBufIndex, 0, chunkSize,
                presentationTimeUs, 0 /*flags*/);
        if (VERBOSE) {
            Log.d(TAG, "submitted frame " + inputChunk + " to dec, size=" +
                    chunkSize);
        }
        inputChunk++;
        extractor.advance();
    }
} else {
    if (VERBOSE) Log.d(TAG, "input buffer not available");
}

 

2.2 Output Phase

 

Output Phase에서는 방금전 Input Phase에서 넣어둔 input buffer 정보를 추출하는 일을 한다. 각 비즈니스 로직에 따라서 추출한 정보를 화면에 뿌려주기도 하고 아니면 새로운 비디오를 만드는 작업으로 사용할 수도 있을 것 같다. dequeueOutputBuffer는 아까 dequeueInputBuffer 함수에서 넣어둔 정보를 가져오는 역할을 한다. 첫번째 인자는 out 타입으로 받아온 정보를 저장하고 리턴 값으로는 현재 디코더의 상태 값을 나타낸다.

 

TRY_AGAIN_LAYER는 현재 읽을 수 있는 Input buffer가 없을 때 발생한다. Input Buffer를 분명히 넣어 줬는데도 이 플래그 값이 발생하는데 input buffer를 여러차례 넣고 나면 제대로 읽을 수 있게 된다.  FORMAT_CHANGED는 디코더의 output format에 변화가 생겼다는 뜻인데 디코딩 작업에서는 딱히 중요한 점이 없다.

 

mBufferInfo 에서 받아온 정보의 플래그 값을 보게 되는데 END_OF_STREAM 가 포함되어 있으면 버퍼는 마지막인 것이다. 전에 Input Phase에서 END_OF_STREAM 플래그를 넣었던 바로 그녀석이 맞다. 

int decoderStatus = decoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
    // no output available yet
    if (VERBOSE) Log.d(TAG, "no output from decoder available");
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
    MediaFormat newFormat = decoder.getOutputFormat();
    if (VERBOSE) Log.d(TAG, "decoder output format changed: " + newFormat);
} else if (decoderStatus < 0) {
    throw new RuntimeException(
            "unexpected result from decoder.dequeueOutputBuffer: " +
                    decoderStatus);
} else { // decoderStatus >= 0
    if (firstInputTimeNsec != 0) {
        // Log the delay from the first buffer of input to the first buffer
        // of output.
        long nowNsec = System.nanoTime();
        Log.d(TAG, "startup lag " + ((nowNsec-firstInputTimeNsec) / 1000000.0) + " ms");
        firstInputTimeNsec = 0;
    }
    boolean doLoop = false;
    if (VERBOSE) Log.d(TAG, "surface decoder given buffer " + decoderStatus +
            " (size=" + mBufferInfo.size + ")");
    if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
        if (VERBOSE) Log.d(TAG, "output EOS");
        if (mLoop) {
            doLoop = true;
        } else {
            outputDone = true;
        }
    }

    boolean doRender = (mBufferInfo.size != 0);

    // As soon as we call releaseOutputBuffer, the buffer will be forwarded
    // to SurfaceTexture to convert to a texture.  We can't control when it
    // appears on-screen, but we can manage the pace at which we release
    // the buffers.
    if (doRender && frameCallback != null) {
        frameCallback.preRender(mBufferInfo.presentationTimeUs);
    }
    decoder.releaseOutputBuffer(decoderStatus, doRender);
    if (doRender && frameCallback != null) {
        frameCallback.postRender();
    }

    if (doLoop) {
        Log.d(TAG, "Reached EOS, looping");
        extractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
        inputDone = false;
        decoder.flush();    // reset decoder state
        frameCallback.loopReset();
    }
}

 

doRender 변수를 결정하는 요인은 mBufferInfo.size 가 0보다 클 때 인데, 이 정보는 받아온 정보가 화면에 뿌려줄 영상 정보인지 아닌지를 의미한다. 그래서 이 값이 유효하다면 각 비즈니스 로직에 따라서 화면에 뿌려주거나 음성을 재생하면 된다. 아래 코드에서는 frameCallback 함수에서 읽어온 정보에서 시간 정보만 추출해 가져가고 있다. 경우에 따라선 비디오 디코딩 정보를 담고 있는 mOutputSurface를 OpenGL에 그려주어 인코더의 input에 넣어주기도 한다. CTS 테스트 코드를 보면 추출한 오디오 버퍼 정보를 바로 인코더에 넣는 것을 볼 수 있다.

 

읽어온 정보에 대한 처리가 끝나면 releaseOutputBuffer를 통해 이 정보에 대한 처리가 완료 됐음을 처리한다.

 

2.3 release 

 

EndOfStream에 도달해 디코딩 작업이 완료되면 사용한 Decoder를 반드시 릴리즈 시켜줘야한다. 이것은 release 함수로 가능하다.

 

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

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
안드로이드 그림자(Shadow) 효과 넣기  (0) 2020.04.18