출처 : https://github.com/jeehwan/MediaPlayerWithAndroidMediaFramework
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)
}
}
}
}