本位着重介绍 MediaCodec 的多媒体解码,并输出数据的使用过程
整体数据流转说明
MediaCodec 的数据分为两个部分,从数据的输入到编解码后的数据的输出:
- input : MediaCodec 会通过getInputBuffer(int bufferId) 去拿到一个空的 ByteBuffer , 用来给客户端去填入数据(比如解码,编码的数据),MediaCodec 会用这些数据进行解码/编码处理
- output : MediaCodec 会把解码/编码的数据填充到一个空的 buffer 中,然后把这个填满数据的buffer给到客户端,之后需要释放这个 buffer,MediaCodec 才能继续填充数据。
状态变化过程:
实现过程: 1、使用 SurfaceView 作为容器
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="0dp"
android:layout_height="250dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
2、将 SurfaceView 中的 Surface 作为参数,传递给 mediaCodec 作为画布 整个流程就是解码一帧,渲染一帧的过程,mediaCodec 内部会维护一个队列进行数据的入队出队
class VideoDecoder(val surface: Surface, val videoPath: String) {
var mediaExtractor: MediaExtractor? = null
var mediaCodec: MediaCodec? = null
var isRunning = false
fun start() {
mediaExtractor = MediaExtractor().apply {
setDataSource(videoPath)
}
// 查找视频轨道
val videoTrackIndex = (0 until mediaExtractor!!.trackCount)
.firstOrNull { i ->
mediaExtractor!!.getTrackFormat(i).getString(MediaFormat.KEY_MIME)?.startsWith("video/") ?: false
} ?: throw IllegalStateException("No video track found")
mediaExtractor!!.selectTrack(videoTrackIndex)
val videoFormat = mediaExtractor!!.getTrackFormat(videoTrackIndex)
// 初始化解码器
mediaCodec = MediaCodec.createDecoderByType(videoFormat.getString(MediaFormat.KEY_MIME)
!!).apply {
configure(videoFormat, surface, null, 0)
start()
}
isRunning = true
CoroutineScope(Dispatchers.IO).launch {
decodeLoop()
}
}
suspend fun decodeLoop() {
val codec = mediaCodec ?: return
val extractor = mediaExtractor ?: return
val bufferInfo = MediaCodec.BufferInfo()
var startTime = System.nanoTime()
while (isRunning) {
// 提交数据到解码器输入缓冲区
val inputBufferId = codec.dequeueInputBuffer(1000)
if (inputBufferId >= 0) {
val inputBuffer = codec.getInputBuffer(inputBufferId)!!
val sampleSize = extractor.readSampleData(inputBuffer, 0)
if (sampleSize >= 0) {
codec.queueInputBuffer(
inputBufferId,
0,
sampleSize,
extractor.sampleTime,
extractor.sampleFlags
)
extractor.advance()
} else {
// 输入结束
codec.queueInputBuffer(
inputBufferId,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
}
}
// 处理解码器输出
val outputBufferId = codec.dequeueOutputBuffer(bufferInfo, 1000)
when {
outputBufferId >= 0 -> {
// 渲染到 Surface
val ptsUs = bufferInfo.presentationTimeUs
val nowUs = (System.nanoTime() - startTime) / 1000
if (ptsUs > nowUs) {
delay((ptsUs - nowUs)/1000)
}
codec.releaseOutputBuffer(outputBufferId, true)
}
outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
// 格式变化(通常首次回调)
val newFormat = codec.outputFormat
}
}
// 检查结束标志
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
break
}
}
release()
}
fun release() {
isRunning = false
mediaCodec?.stop()
mediaCodec?.release()
mediaExtractor?.release()
}
}
3、注意渲染到界面的时候,不能来一帧数据就渲染一帧数据,否则会造成画面快进的现象。 因为解码的速度会比视频的时间轴更快,所以渲染画面的实际应该跟随视频的时间轴进行渲染
以下这段逻辑就是判断时间轴的时间是否大于解码的时间,如果是,则等待,以时间轴的时间为准进行画面渲染
var startTime = System.nanoTime()
val ptsUs = bufferInfo.presentationTimeUs
val nowUs = (System.nanoTime() - startTime) / 1000
if (ptsUs > nowUs) {
delay((ptsUs - nowUs) / 1000)
}