본문 바로가기

카테고리 없음

미디어코덱 _1 비디오재생

출처 : https://github.com/jeehwan/MediaPlayerWithAndroidMediaFramework 

출처 : https://deview.kr/data/deview/session/attach/1400_T3_%EB%B0%95%EC%A7%80%ED%99%98_%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C_%EB%AF%B8%EB%94%94%EC%96%B4%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC%EB%A5%BC_%ED%99%9C%EC%9A%A9%ED%95%9C_%EB%8F%99%EC%98%81%EC%83%81%ED%94%8C%EB%A0%88%EC%9D%B4%EC%96%B4_%EB%A7%8C%EB%93%A4%EA%B8%B0.pdf

layout# activity_main.xml

 

참고하자 https://goddoro.medium.com/mediacodec%EC%9C%BC%EB%A1%9C-%EC%98%81%EC%83%81-%EC%9E%AC%EC%83%9D%ED%95%98%EA%B8%B0-feat-coroutine-b6aef75e574d

 

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

    <com.navercorp.deview.mediaplayer.AutoFitTextureView
        android:id="@+id/textureView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:layout_editor_absoluteX="0dp"
        tools:layout_editor_absoluteY="0dp" />

</androidx.constraintlayout.widget.ConstraintLayout>

java# MainActivity.kt

package com.navercorp.deview.mediaplayer

import android.graphics.SurfaceTexture
import android.media.MediaCodec
import android.media.MediaExtractor
import android.media.MediaFormat
import android.os.Bundle
import android.view.Surface
import android.view.TextureView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import kotlin.concurrent.thread

// http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_30fps_normal.mp4
private val MEDIA_FILE = "bbb_sunflower_1080p_30fps_normal.mp4"
private val TIMEOUT_US = 10_000L

class MainActivity : AppCompatActivity() {

    private lateinit var view: AutoFitTextureView
    //화면을 정의할 때 사용한다. (  Activity 가 생성될때 사용 )
    //콜백 메소드입니다. Activity의 생성주기에서 생성 단계에 한번 실행되는 메소드이다
    override fun onCreate(savedInstanceState: Bundle?) {//savedInstanceState는 액티비티의 이전상태를 저장하는 메소드이다.
        super.onCreate(savedInstanceState) // 첫단계에서 savedInstanceState는 Null이다.
        setContentView(R.layout.activity_main)

        view = textureView
        view.setAspectRatio(16, 9) //가로세로비
        //TextureView 는 뷰를 SurfaceTexture 와 결합하는 뷰(화면을 구성성하는 모든 요소) 객체입니다.
        //SurfaceTexture 와 뷰가 합되면 리스너에게 알려준다.
        //SurfaceTexture는 미디어App에서 주로 사용되는데, OpenGL을 사용하여 비디오 프레임을 렌더링할 때 사용됩니다.
        //SurfaceTexture는 메모리 복사 없이  OpenGL 텍스처로 직접 렌더링 할 수 있도록 해줌( 끊김없는 재생 가능 )
        //SurfaceTexture는 스레드간 통신을 위한 매커니즘을 제공 비디오스트림처리하고 UI 와 상호작용 하면서 랜더링이 부드럽게 수행 될 수 있도록 도와줌.
        view.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
            override fun onSurfaceTextureSizeChanged(
                surface: SurfaceTexture,
                width: Int,
                height: Int
            ) = Unit

            override fun onSurfaceTextureUpdated(surface: SurfaceTexture) = Unit
            //특정 SurfaceTexture 가 Destroyed 될 참일때
            override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean = true
            //TextureView의 SurfaceTexture 가 사용 준비완료시
            override fun onSurfaceTextureAvailable(
                surface: SurfaceTexture,
                width: Int,
                height: Int
            ) {
                createVideoThread(Surface(surface))  //비디오 처리를 위한 쓰레드 만든다.
            }
        }
    }

    private fun createVideoThread(surface: Surface) = thread {
        //파일시스템이 아닌 App의 Asset폴더에 있는 파일에 대해 fileDescription을 생성합니다.
        val extractor = assets.openFd(MEDIA_FILE).use {//fileDescription는 FileInputStream, FileOutputStream을 쓸수 있습니다.
            MediaExtractor().apply { setDataSource(it) } //FileDescription을 setDataSource() 메소드에 전달하여 해당 미디어 파일을 재생합니다
        }

        val trackIndex = extractor.firstVideoTrack
            ?: error("This media file doesn't contain any video tracks")

        extractor.selectTrack(trackIndex)
        //MediaFormat은
        // 안드로이드에서 비디오나 오디오 미디어 데이터의 형식을 지정하는 데 사용되는 클래스입니다.
        // 미디어 데이터에 대한 인코딩, 디코딩, 트랜스코딩등의 작업을 수행하는 미디어 프로세싱  API에서 사용됩니다.
        val format = extractor.getTrackFormat(trackIndex)
        val mime = format.getString(MediaFormat.KEY_MIME)
            ?: error("Video track must have the mime type")
        //asset의 데이터에서 가져온 파일이 압축된 형식을 따내고, 디코더를 만듭니다.
        //onCreate() 에서 뽑아낸 surface,  asset파일을 통해 뽑아낸 fileDescription 에서 첫 곡에서 뽑아낸 extractor.getTrackformat() 을 넣어줍니다.
        val decoder = MediaCodec.createDecoderByType(mime).apply {
            configure(format, surface, null, 0)
            start()
        }

        doExtract(extractor, decoder)

        decoder.stop()
        decoder.release()
        extractor.release()
    }
    //디먹싱, 디코딩
    //디먹싱 : 다양한 미디어 스트림(오디오,비디오,자막 등)을 분리한다. 디코딩하는데 첫 단계
    //      : 미디어컨테이너( AVI ) 등
    private fun doExtract(extractor: MediaExtractor, decoder: MediaCodec) {
        //extractor 는 assets 에 있는 파일을 읽어서 fileDescription 을 만들어둔 것이다.
        //fileDescription은 인풋스트림,아웃풋 스트림 등을 제공한다.
        val info = MediaCodec.BufferInfo()

        var inEos = false
        var outEos = false

        while (!outEos) {
            if (!inEos) {
                //MediaCodec의 dequeueInputBuffer는
                //비어있는 inputBuffer중 하나의 index를 가져오는 메소드입니다.
                //MediaCodec 객체를 생성한 후에 입력 데이터를 전달하기 위해 생성됨
                //비디오나 오디오 데이터를 인코딩하기 위해 미디어 데이터를 읽어 입력 버퍼에 쓰고,
                //다시 MediaCodec 클래스의 queueInputBuffer() 메소드를 호출하여 코덱에 데이터를 전달합니다.
                when (val inputIndex = decoder.dequeueInputBuffer(TIMEOUT_US)) {//입력 버퍼를 가져오기 위해 대기할 시간(마이크로초 단위). 0을 전달하면 즉시 반환되며, -1을 전달하면 무한 대기합니다.
                    in 0..Int.MAX_VALUE -> {
                        //인풋버퍼 가져오기
                        val inputBuffer = decoder.getInputBuffer(inputIndex)!!
                        val chunkSize = extractor.readSampleData(inputBuffer, 0)//현재 추출 위치에서 버퍼에 추출된 데이터를 읽어옵니다
                        if (chunkSize < 0) { //데이터가 더이상 없을 때
                            decoder.queueInputBuffer(inputIndex, 0, 0, -1,
                                MediaCodec.BUFFER_FLAG_END_OF_STREAM) //inputbuffer에 queue할떄 미디어의 마지막 프레임에 도착했음을 알려준다..
                            inEos = true
                        } else { //데이터 넣기
                            val sampleTimeUs = extractor.sampleTime
                            decoder.queueInputBuffer(inputIndex, 0, chunkSize, sampleTimeUs, 0)
                            extractor.advance()
                        }
                    }
                    else -> Unit
                }
            }

            if (!outEos) {
                when (val outputIndex = decoder.dequeueOutputBuffer(info, TIMEOUT_US)) {
                    in 0..Int.MAX_VALUE -> {
                        //데이터를 소비한다.
                        if ((info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                            //디코딩 된 미디어 샘플의 출력 버퍼를 디코더로 반환하여 다음 샘플의 디코딩을 계속할 수 있도록 합니다.
                            //디코더가 다시 outputBuffer를 사용할 수 있게 해줌
                            decoder.releaseOutputBuffer(outputIndex, false)
                            outEos = true
                        } else {
                            decoder.releaseOutputBuffer(outputIndex, true)
                        }
                    }
                    MediaCodec.INFO_TRY_AGAIN_LATER -> Unit
                    MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> Unit
                    MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> Unit
                    else -> error("unexpected result from decoder.dequeueOutputBuffer: $outputIndex")
                }
            }
        }
    }
}

fun MediaExtractor.findFirstTrackFor(type: String): Int? {
    for (i in 0 until trackCount) {
        val mediaFormat = getTrackFormat(i)
        if (mediaFormat.getString(MediaFormat.KEY_MIME)!!.startsWith(type)) {
            return i
        }
    }

    return null
}

val MediaExtractor.firstVideoTrack: Int? get() = findFirstTrackFor("video/")
val MediaExtractor.firstAudioTrack: Int? get() = findFirstTrackFor("audio/")

 

AutoFixTextureView.class

 

// from https://github.com/googlearchive/android-Camera2Video/blob/master/kotlinApp/Application/src/main/java/com/example/android/camera2video/AutoFitTextureView.kt

/*
 * Copyright 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.navercorp.deview.mediaplayer

import android.content.Context
import android.util.AttributeSet
import android.view.TextureView
//화면을 보여주고, 사진을 찍거나 등의 역할을 함
class AutoFitTextureView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : TextureView(context, attrs, defStyle) {

    private var ratioWidth = 0
    private var ratioHeight = 0

    /**
     * Sets the aspect ratio for this view. The size of the view will be measured based on the ratio
     * calculated from the parameters. Note that the actual sizes of parameters don't matter, that
     * is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result.
     *
     * @param width  Relative horizontal size
     * @param height Relative vertical size
     */
    fun setAspectRatio(width: Int, height: Int) {
        if (width < 0 || height < 0) {
            throw IllegalArgumentException("Size cannot be negative.")
        }
        ratioWidth = width
        ratioHeight = height
        requestLayout()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val width = MeasureSpec.getSize(widthMeasureSpec)
        val height = MeasureSpec.getSize(heightMeasureSpec)
        if (ratioWidth == 0 || ratioHeight == 0) {
            setMeasuredDimension(width, height)
        } else {
            if (width < ((height * ratioWidth) / ratioHeight)) {
                setMeasuredDimension(width, (width * ratioHeight) / ratioWidth)
            } else {
                setMeasuredDimension((height * ratioWidth) / ratioHeight, height)
            }
        }
    }

}