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에 뿌려준다.
'안드로이드' 카테고리의 다른 글
Media Player에 대한 설명 (0) | 2023.04.07 |
---|---|
주말공부 리스트 (0) | 2023.02.24 |
JNI ExceptionCheck , ExceptionOccurred (0) | 2023.02.07 |
[JNI] C++코드로 JAVA 의 Priavate를 접근할 수 있는가? (0) | 2023.02.07 |
MediaCodec OPENGL 관련 영상 및 게시글 (0) | 2023.02.04 |