class MainActivity : AppCompatActivity() {
// 定义一个阻塞队列,用于存储视频帧数据
private val videoQueue = LinkedBlockingQueue<ByteArray>()
// 声明一个 SurfaceView,用于显示视频
private lateinit var surfaceView: SurfaceView
// 声明一个 Surface 对象,用于与 MediaCodec 交互
private var surface: Surface? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
surfaceView = findViewById(R.id.surface)
surfaceView.holder.addCallback(object: SurfaceHolder.Callback {
// 当 Surface 被创建时调用
override fun surfaceCreated(holder: SurfaceHolder) {
// 将 holder 的 surface 赋值给 surface 变量
surface = holder.surface
// 创建一个线程来读取视频文件数据
thread {
//使用 assets.open 方法打开应用资源中的 “video.h264” 文件,并创建一个 InputStream 对象来读取文件内容。
val inputStream = assets.open("video.h264")
//创建一个大小为 1024 字节的字节数组 videoData,用于存储从输入流中读取的数据块
val videoData = ByteArray(1024)
//声明一个变量 remainData 并初始化为一个空的字节数组。这个变量用于存储上一次调用 pullFrame 函数后剩余的不完整帧数据。
var remainData = byteArrayOf()
// 循环读取视频数据直到文件结束
while (inputStream.read(videoData) != -1) {
// 将剩余数据和当前读取的数据合并
val videoData = byteArrayOf(*remainData, *videoData)
// 调用 pullFrame 函数,传入合并后的数据块 videoData,以提取视频帧。pullFrame 函数返回剩余的不完整帧数据,这些数据被赋值给 remainData 变量,以供下一次循环使用
remainData = pullFrame(videoData)
}
}
// 创建另一个线程来处理解码和显示视频帧
thread {
// 启动解码器
mediaCodec.start()
// 无限循环处理输入和输出缓冲区
mediaCodec.start()
while (true) {
// 调用 dequeueInputBuffer 方法来获取输入缓冲区的索引。参数 10000 是超时时间(以微秒为单位),表示如果当前没有可用的输入缓冲区,该方法将最多等待 10000 微秒。
val inputBufferIndex = mediaCodec.dequeueInputBuffer(10000)
//检查 dequeueInputBuffer 方法返回的索引是否有效(非负数)。如果有效,则表示获得了可用的输入缓冲区
if (inputBufferIndex >= 0) {
// 获取输入缓冲区
val buffer = mediaCodec.getInputBuffer(inputBufferIndex)
//清空缓冲区,准备填充新的数据
buffer?.clear()
// 从 videoQueue 队列中取出一个视频帧数据。take 方法是一个阻塞操作,如果队列为空,它将等待直到有数据可用
val data = videoQueue.take()
// 将数据放入缓冲区
buffer?.put(data, 0, data.size)
// 将填充好的输入缓冲区提交给 MediaCodec 进行解码。参数分别是缓冲区索引、数据偏移量、数据大小、时间戳和标志位
mediaCodec.queueInputBuffer(inputBufferIndex, 0, data.size, 0, 0)
} else {
continue
}
// 获取解码后的输出缓冲区的索引。MediaCodec.BufferInfo() 是用于存储输出缓冲区信息的对象
val outputBufferIndex = mediaCodec.dequeueOutputBuffer(MediaCodec.BufferInfo(), 10000)
if (outputBufferIndex >= 0) {
// 等待一段时间,模拟帧率
Thread.sleep(10)
// 释放输出缓冲区并渲染到 Surface
mediaCodec.releaseOutputBuffer(outputBufferIndex, true)
}
}
}
}
override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) {
}
override fun surfaceDestroyed(p0: SurfaceHolder) {
}
})
}
// 初始化 MediaCodec
private fun initMediaCodec(): MediaCodec {
// 创建一个解码器
val mediaCodec = MediaCodec.createDecoderByType("video/avc")
// 创建视频格式
val mediaFormat = MediaFormat.createVideoFormat("video/avc", 1920, 1080)
// 配置解码器
mediaCodec.configure(mediaFormat, surface, null, 0)
// 返回解码器
return mediaCodec
}
//这个变量用于跟踪数据中当前帧的开始索引
private var beginIndex = 0;
//这个函数用于从输入数据中提取视频帧
private fun pullFrame(data: ByteArray): ByteArray {
beginIndex = 0
//使用 forEachIndexed 方法遍历 data 数组。对于数组中的每个字节,都会执行闭包中的代码,同时传递当前字节的位置索引 index 和字节值 byte
data.forEachIndexed { index, byte ->
/** 这是一个条件判断,检查当前索引是否满足以下条件:
*当前索引大于 0(确保不是数组的第一个元素)。
*当前索引小于 data.size - 3(确保至少有四个字节可以检查)。
*当前字节和接下来的三个字节是否分别是 0x00 0x00 0x00 0x01,这是 H.264 视频帧分隔符。
**/
if (index > 0 && index < data.size - 3 && data[index].toInt() == 0 && data[index + 1].toInt() == 0 && data[index + 2].toInt() == 0 && data[index + 3].toInt() == 1) {
//如果找到了帧分隔符,使用 copyOfRange 方法从 beginIndex 到 index(不包括)提取帧数据,并将其赋值给 frameData 变量。
val frameData = data.copyOfRange(beginIndex, index)
videoQueue.put(frameData)
beginIndex = index
}
//如果当前索引是 data 数组的最后一个索引,并且 beginIndex 小于 index,这意味着数组末尾有未处理的数据。
if (index == data.size - 1 && beginIndex < index) {
//从 beginIndex 到 data.size(不包括)提取剩余的数据,并将其赋值给 outData 变量
val outData = data.copyOfRange(beginIndex, data.size)
return outData
}
}
return byteArrayOf()
}
}