본문 바로가기

안드로이드

미디어코덱 - 비디오,오디오 디코딩 및 재생

package com.navercorp.deview.mediaplayer

import android.graphics.SurfaceTexture
import android.media.*
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.view.Surface
import android.view.TextureView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import java.util.concurrent.TimeUnit

// 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_MS = 10L

class MainActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {

    private lateinit var view: AutoFitTextureView

    private lateinit var demuxThread: HandlerThread
    private lateinit var audioDecodeThread: HandlerThread
    private lateinit var videoDecodeThread: HandlerThread
    private lateinit var demuxHandler: Handler
    private lateinit var audioDecodeHandler: Handler
    private lateinit var videoDecodeHandler: Handler

    private var audioTrackIndex: Int? = null
    private lateinit var audioExtractor: MediaExtractor
    private lateinit var audioDecoder: MediaCodec
    private lateinit var audioTrack: AudioTrack

    private var videoTrackIndex: Int? = null
    private lateinit var videoExtractor: MediaExtractor
    private lateinit var videoDecoder: MediaCodec

    private var audioInEos = false
    private var audioOutEos = false
    private var videoInEos = false
    private var videoOutEos = false
    private val audioBufferInfo = MediaCodec.BufferInfo()
    private val videoBufferInfo = MediaCodec.BufferInfo()
    //액티비티 vs 뷰
    //최초 시작될때이다. savedInstanceState 는 액티비티의 직전 상태를 이야기한다.
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //textureView 는 SurfaceTexture와 view가 합쳐진것이다.
        //SurfaceTexture는 부드러운 영상 재생을 위해서  메모리 복사없이  OpenGL 텍스쳐로 직접 렌더링 할 수 있도록 한다.
        view = textureView //레이아웃에 있는 AutoFitTextureView이다.
        view.setAspectRatio(16, 9)
        view.surfaceTextureListener = this
    }

    override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) = Unit

    override fun onSurfaceTextureUpdated(surface: SurfaceTexture) = Unit

    override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean = true
    //surfaceTexture와 View 가 합쳐지면
    override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
        //디먹싱, 디코딩 시작
        //openFd는 파일시스템에 있지 않은 app구역의 assets에 있는 파일을 읽어서 FileDescription 을 제공합니다.
        //FileDescription 은 인풋스트림, 아웃풋스트림 등을 제공합니다.
        val createExtractor = {
            assets.openFd(MEDIA_FILE).use {
                // Extractor가 FileDescription 을 제공하는데 이것을 setDataSource에 넣어서 해당 미디어 파일을 재생합니다. // 미디어 파일의 경로를 지정하거나, 파일 디스크립터(FileDescriptor)를 전달하거나, URL 주소를 전달하여 해당 미디어를 처리할 데이터 소스를 설정합니다.
                //setDataSource(String path): 미디어 파일의 경로를 지정합니다.
                //setDataSource(Context context, Uri uri): 애플리케이션 내부 또는 외부의 미디어 파일을 Uri 형태로 지정합니다.
                //setDataSource(FileDescriptor fd): 파일 디스크립터를 전달하여 해당 미디어 파일을 처리합니다.
                //setDataSource(FileDescriptor fd, long offset, long length): 파일 디스크립터와 파일의 오프셋(offset)과 길이(length)를 지정하여 해당 미디어 파일을 처리합니다.
                //setDataSource(MediaDataSource dataSource): MediaDataSource 객체를 전달하여 해당 미디어 파일을 처리합니다.
                MediaExtractor().apply { setDataSource(it) }
             }
        }
        //0. 컨테이너에서 오디오,비디오의 첫번째 트랙의 인덱스를 가져온다.
        val extractor = createExtractor()
        audioTrackIndex = extractor.firstAudioTrack
        videoTrackIndex = extractor.firstVideoTrack
        extractor.release()

        if (audioTrackIndex == null || videoTrackIndex == null) {
            error("We need both audio and video")
        }
        //1. 첫번째 트렉에 대한 Extractor를 제공한다.  ( selectTrack 은 int로 제공된 i번쨰 트랙을 선택하도록 한다. )
        audioExtractor = createExtractor().apply { selectTrack(audioTrackIndex!!) }
        videoExtractor = createExtractor().apply { selectTrack(videoTrackIndex!!) }
        //2-1. 디코더를 생성한다. Extractor,TrackIndex, Surface를 변수로 제공한다.
        audioDecoder = createDecoder(audioExtractor, audioTrackIndex!!)
        //surface(surface)는 Android에서 Surface 객체를 생성하는 메소드 중 하나입니다. Surface 객체는 픽셀 데이터의 출력 대상이 되는 디스플레이 화면을 나타냅니다. Surface 객체는 비디오, 그래픽, 카메라 미리보기 등 다양한 용도로 사용될 수 있습니다.
        //surface(surface) 메소드는 SurfaceHolder나 SurfaceTexture와 같은 클래스를 사용하여 Surface 객체를 생성하는 경우에 주로 사용됩니다. 예를 들어, MediaPlayer를 사용하여 동영상을 재생하는 경우, setSurface(Surface surface) 메소드를 호출하여 Surface 객체를 지정하면 동영상이 해당 Surface에 표시됩니다.
        //surface(surface) 메소드는 일반적으로 UI 스레드에서 호출되며, 메소드 파라미터로 Surface 객체를 전달합니다. Surface 객체는 SurfaceView나 TextureView와 같은 뷰에서 사용되며, 이를 통해 해당 뷰에서 미디어를 재생하거나, 그래픽을 출력할 수 있습니다.
        videoDecoder = createDecoder(videoExtractor, videoTrackIndex!!, Surface(surface))


        //2-2.오디오 트랙을 만든다.
        val audioFormat_ = audioExtractor.getTrackFormat((audioTrackIndex!!))
        audioTrack = createAudioTrack(audioFormat_)
        //3. 디먹싱, 디코딩은 각 쓰레드에서 처리하도록 한다 // 쓰레드 시작
        //HandlerThread 는  looper 와 handle을 사용해서 메시지 처리 기능을 사용 할 수 있게 해주었다. ( 스레드와 메시지 처리 기능을 모두 포함하는)
        //사용법은 HandlerThread 를 만들고, 그내부에 있는 looper를 이용해서 handler를 만든다.
        demuxThread = HandlerThread("DemuxThread").apply { start() }
        audioDecodeThread = HandlerThread("AudioDecodeThread").apply { start() }
        videoDecodeThread = HandlerThread("VideoDecodeThread").apply { start() }
        //3-1 쓰레드 핸들러를 만든다. ( 백그라운드쓰레드에서 사용하고  postDelay로 UI 쓰레드와 통신한다.)
        demuxHandler = Handler(demuxThread.looper) // looper는 메세지 큐를 관리하고, 이 큐관리자를 받아서 Handler가 처리해줍니다.
        audioDecodeHandler = Handler(audioDecodeThread.looper)
        videoDecodeHandler = Handler(videoDecodeThread.looper)
        //디코더 시작 및 오디오트랙 시작
        audioDecoder.start()
        videoDecoder.start()
        audioTrack.play()  // createAudioTrack(audioFormat_)
        //InputBuffer를 채움
        postExtractAudio(0) // 큐를 돌면서 인풋
        postExtractVideo(0)
        postDecodeAudio(0)  // 쿠를 돌면서 아웃풋
        postDecodeVideo(0)
    }

    //디코더를 만들때에는 track의format, trackIndex , surface가 필요합니다.
    private fun createDecoder(
        extractor: MediaExtractor,
        trackIndex: Int,
        surface: Surface? = null
    ): MediaCodec{
        // trackIndex를 통해서 format을 가져옵니다. (디코더만들때 Type넣어줌)
        val format = extractor.getTrackFormat(trackIndex)
        //MediaFormat.KEY_MIME을 사용하여 MediaFormat 객체를 생성할 때, MediaExtractor를 사용하여 추출한 미디어 파일의 포맷 정보를 확인한 후, 이를 기반으로 미디어 코덱을 초기화합니다. 이때 MediaFormat.KEY_MIME 상수에는 미디어 파일의 MIME 타입이 지정됩니다.
        //예를 들어, MediaFormat 객체를 생성하여 H.264 비디오 코덱을 초기화하려면, MediaFormat.KEY_MIME 상수에 video/avc 문자열을 전달해야 합니다.
        val mime = format.getString(MediaFormat.KEY_MIME)!!

        return MediaCodec.createDecoderByType(mime).apply {
            configure(format, surface, null, 0)
        }
    }

    private fun createAudioTrack(format: MediaFormat): AudioTrack {
        val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
        val channels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
        val channelMask = when (channels) {
            1 -> AudioFormat.CHANNEL_OUT_MONO
            2 -> AudioFormat.CHANNEL_OUT_STEREO
            else -> error("AudioTrack doesn't support $channels channels")
        }
        val minBufferSize = AudioTrack.getMinBufferSize(
            sampleRate, channelMask, AudioFormat.ENCODING_PCM_16BIT)

        return AudioTrack.Builder()
            .setAudioAttributes(AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .build())
            .setAudioFormat(AudioFormat.Builder()
                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                .setSampleRate(sampleRate)
                .setChannelMask(channelMask)
                .build())
            .setTransferMode(AudioTrack.MODE_STREAM)
            .setBufferSizeInBytes(minBufferSize * 10)
            .build()
    }

    private fun postExtractAudio(delayMillis: Long) {
        demuxHandler.postDelayed({ //Handler에게 지정된 시간이 경과한 후 실행될 작업(Runnable)을 추가합니다
            if (!audioInEos) {
                when (val inputIndex = audioDecoder.dequeueInputBuffer(0)) {
                    in 0..Int.MAX_VALUE -> {
                        val inputBuffer = audioDecoder.getInputBuffer(inputIndex)!!

                        //읽어와서, 크기, 현재 타임스탬프 를  queueInputBuffer에 넣어준다.
                        val chunkSize = audioExtractor.readSampleData(inputBuffer, 0)
                        if (chunkSize < 0) {
                            audioDecoder.queueInputBuffer(inputIndex, 0, 0, -1,
                                MediaCodec.BUFFER_FLAG_END_OF_STREAM)
                            audioInEos = true
                        } else {
                            val sampleTimeUs = audioExtractor.sampleTime // 오디오 샘플을 추출하는 과정에서, 현재 추출된 샘플의 타임스탬프를 가져옴
                            audioDecoder.queueInputBuffer(inputIndex, 0, chunkSize,
                                sampleTimeUs, 0)
                            audioExtractor.advance() //다음 샘플을 읽어올 준비를 합니다.
                        }

                        postExtractAudio(0)
                    }
                    else -> postExtractAudio(TIMEOUT_MS)
                }
            }
        }, delayMillis)
    }

    private fun postDecodeAudio(delayMillis: Long) {
        audioDecodeHandler.postDelayed({
            if (!audioOutEos) {
                when (val outputIndex = audioDecoder.dequeueOutputBuffer(audioBufferInfo, 0)) {
                    in 0..Int.MAX_VALUE -> {
                        if ((audioBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                            audioDecoder.releaseOutputBuffer(outputIndex, false)
                            audioOutEos = true
                        } else {
                            val outputBuffer = audioDecoder.getOutputBuffer(outputIndex)!!
                            outputBuffer.position(audioBufferInfo.offset)  // 디코딩된 음성 데이터가 저장되는 버퍼의 위치를 지정한다.
                            outputBuffer.limit(audioBufferInfo.offset + audioBufferInfo.size)
                            //outputBuffer에서 audioBufferInfo.size만큼의 데이터를 읽어와 AudioTrack으로 재생합니다. AudioTrack.WRITE_BLOCKING은 새로운 데이터를 추가할 수 없을 때 write 메소드가 차단됨을 의미합니다.
                            audioTrack.write(outputBuffer, audioBufferInfo.size,
                                AudioTrack.WRITE_BLOCKING)

                            audioDecoder.releaseOutputBuffer(outputIndex, false)
                        }

                        postDecodeAudio(0)
                        return@postDelayed
                    }
                    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")
                }

                postDecodeAudio(TIMEOUT_MS)
            }
        }, delayMillis)
    }

    private fun postExtractVideo(delayMillis: Long) {
        //일반적으로 백그라운드 스레드에서 작업을 수행하고
        //UI업데이트를 위해서  post() 를 이용해서 UI스레드와 통신하는 것이 일반적입니다.
        demuxHandler.postDelayed({
            if (!videoInEos) {
                when (val inputIndex = videoDecoder.dequeueInputBuffer(0)) {
                    in 0..Int.MAX_VALUE -> {
                        val inputBuffer = videoDecoder.getInputBuffer(inputIndex)!!
                        val chunkSize = videoExtractor.readSampleData(inputBuffer, 0)
                        if (chunkSize < 0) {
                            videoDecoder.queueInputBuffer(inputIndex, 0, 0, -1,
                                MediaCodec.BUFFER_FLAG_END_OF_STREAM)
                            videoInEos = true
                        } else {
                            val sampleTimeUs = videoExtractor.sampleTime
                            videoDecoder.queueInputBuffer(inputIndex, 0, chunkSize,
                                sampleTimeUs, 0)
                            videoExtractor.advance()
                        }


                        postExtractVideo(0)
                    }
                    else -> postExtractVideo(TIMEOUT_MS)
                }
            }
        }, delayMillis)
    }

    private fun postDecodeVideo(delayMillis: Long) {
        videoDecodeHandler.postDelayed({
            if (!videoOutEos) {
                when (val outputIndex = videoDecoder.dequeueOutputBuffer(videoBufferInfo, 0)) {
                    in 0..Int.MAX_VALUE -> {
                        if ((videoBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                            videoDecoder.releaseOutputBuffer(outputIndex, false)
                            videoOutEos = true
                        } else {
                            videoDecoder.releaseOutputBuffer(outputIndex, true)
                        }

                        postDecodeVideo(0)
                        return@postDelayed
                    }
                    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")
                }

                postDecodeVideo(TIMEOUT_MS)
            }
        }, delayMillis)
    }
}

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/")

 

[큰 과정은 아래와 같습니다.] 틀릴확률 99퍼센트 이지만 일단 블로깅 해봅니다. 틀린부분  댓글 부탁드려요

파일을 이용해  FileDescription을 만들어 Extractor를 만들어준다.

트랙별로 Decoder에 넣어주고, 디코딩된 프레임들을 Surface(랜더링되는 화면)에 넣어준다. Surface는 메모리복사 없이 native 버퍼를 바로 이용할 수 있어 부르더운 렌더링이 가능하다.

Surface Texture 를 만든다.( 특정 TextureID(렌더링에 사용되는 이미지데이터 객체) + Surface 연동 )

 

이 SurfaceTexture를 OpenGL에 넣어서 TextureView에 뿌려준다.