Android MediaCodeC 探索

772 阅读10分钟

前言

在介绍 MediaCodeC 之前,我们先来了解一下音视频相关知识,包括音视频格式、比特率、分辨率等,以便更好的理解音视频编解码。

音视频格式

音频格式

常见的音频格式有 MP3、WAV、FLAC、AAC 等,其中 MP3 最流行的有损压缩音频格式,适合网络分享和便携式播放器,提供相对较高的音质与较小的文件体积。WAV 无损音频格式,由微软开发,常用于专业音频编辑,文件较大,能保存原始音频数据。

视频格式

常见的视频格式有 MP4、AVI、MKV、FLV 等,其中 MP4 使用 MPEG-4 Part 14容器格式,广泛应用于互联网视频分享,支持多种视频和音频编码。 MKV(Matroska) 容器格式,开放标准,支持多种视频、音频及字幕轨道,适合高清视频存储。

MPEG系列

H.264 ,又称为 MPEG-4 第 10 部分,高级视频编码(英语:MPEG-4 Part 10, Advanced Video Coding,缩写为MPEG-4 AVC)是一种面向块,基于运动补偿的视频编码标准,广泛用于高清视频和流媒体服务。

查看视频格式

查看视频格式有很多种方式,这里强烈推荐使用 FFMPEG,点击按照官方指示就可安装。FFMPEG 还有很多功能这里就不一一列举,感兴趣的同学点击链接即可享用。

安装好 FFMPEG 之后,我们执行如下命令。

ffprobe -show_format 11.webm

执行结果如下,里面包含视频了的各个详细信息,下面将一些关键参数进行讲解。 75ff4856-62ec-447e-81cd-5231177210b9.jpeg

名字释义

比特率

比特率(Bitrate)是指单位时间内传输或处理的比特数量,它是衡量数据传输速度的一个关键指标,单位通常是比特每秒(bit/s 或 bps)。比特率可以应用于不同的通信和数字媒体领域,包括网络数据传输、音频文件、视频文件等。

分辨率

分辨率是指屏幕上像素的数量,通常以水平像素数×垂直像素数来表示,比如1920×1080。分辨率越高,表示的图像或视频中的细节就越丰富,画面看起来就越清晰。

FPS

FPS (Frames Per Second,每秒帧数)表示一秒钟内连续播放的画面数量。帧数越高,意味着单位时间内显示的画面越多,动画看起来就越连贯和流畅。

位深度

位深度(Bit Depth),有时也称为颜色深度(Color Depth),是一个表示图像或视频中每个像素可以表示的颜色数量的度量单位。它决定了数字图像中色彩的丰富程度和精度。文中的位深度是 yuv420p 表示每个像素由三个分量组成(YUV),其中Y分量是亮度,U和V是色度,且都采用了色度下采样,每个分量通常都是 8 位,因此总颜色深度是 8 位 × 3 = 24 位。

如何计算视频大小

通过上述名词释义,我们已经知道视频文件包含的关键数据,接下来我们计算一下未经压缩处理的视频大小(不包含音频大小)。首先我们计算一下单帧的视频大小,原始视频大小的计算公式是:

数据量 = 分辨率 × 位深度

所以单帧视频大小就是: 2208 x 1840 × 3 = 12188160 B = 11.62 MB

接下来计算视频总大小:总帧数 × 单帧大小 = 6.2 x 24 x 11.62 = 1.69 GB

未经压缩处理的视频大小竟然有 1.69 GB,视频时长仅仅是 6.2s。如果是长达 1 个小时的电影,需要的物理存储那得多大。所以,这个时候需要视频编解码技术来提供支持。

编解码释义

Android 中的视频和音频编解码是指,将视频或音频数据从一种格式转换为另一种格式,这个转换过程称为编解码,其目的是便于视频和音频的存储、传输以及播放。通常包含视频和音频的编解码,下面会详细介绍。

视频编解码

  • 编码:将原始的视频数据(通常是连续的图像帧序列)压缩成更紧凑的格式,这个过程会去除冗余信息和利用人类视觉系统的特性来减少文件大小,同时保持视觉质量可接受。常见的视频编码格式有 H.264、H.265(HEVC)、VP9 等。

  • 解码:在播放时,将压缩的视频数据还原为原始的图像帧序列,以便显示设备能够识别并显示出来。

音频编解码

  • 编码:将原始的音频波形数据(如 PCM,即脉冲编码调制数据)转换成压缩的数字格式,这个过程同样是为了减小文件大小,便于存储和传输。常用的音频编码格式有 AAC、MP3、Opus 等。

  • 解码:在音频播放前,将压缩的音频数据解压恢复成原始波形数据,随后送入数模转换器(DAC)转换成模拟信号,通过扬声器输出声音

编解码意义

音视频编解码是现代数字媒体传播的基础,它平衡了音视频内容的质量、存储、传输效率和兼容性之间的关系,是实现现代化多媒体信息高效利用的关键技术。

优势

  • 数据压缩:未经处理的原始音视频数据量非常庞大,如果不进行压缩,会占用很大的存储空间,并且在有限的网络带宽下难以实现实时传输。编解码技术通过消除数据冗余和利用人类视觉、听觉感知特性,通过大幅度减少音视频数据的大小,使得文件更易于存储和传输。
  • 提高效率:通过编解码,可以有效提升数据处理和传输的效率。
  • 兼容性与标准化:不同的设备和平台支持不同的音视频格式,编解码技术可以实现在不同系统和设备间内容的转换,具备良好的兼容性,确保跨平台的一致性和互操作性。

开始

MediaCodec

简介

MediaCodec 是 Android 底层多媒体处理和编解码提供的一个 API,用于访问底层的多媒体硬件和软件编解码器,可以用于对音频和视频数据进行编码和解码。

原理

mediacodec_buffers.svg

上图展示了 MediaCodeC 是如何工作的, 首先 MediaCodeC 创建一个空的输入缓冲区,用数据填充它并将其发送给 MediaCodeC 进行处理。MediaCodeC 用完数据之后并将其转换为输出缓冲区,之后请求数据并填充到输出缓冲区,MediaCodeC 开始使用数据,使用完之后将其释放回 MediaCodeC,至此 MediaCodeC 整个工作流程结束。

生命周期状态

mediacodec_states.svg

MediaCodec 包含以下生命周期状态,这些状态之间的转换描绘了 MediaCodec 的完整生命周期,理解这些状态有助于开发者正确地管理和控制编解码过程。

  • Uninitialized (未初始化): 当 MediaCodec 对象被创建后,这时它是处于未初始化状态。

  • Configured (已配置): 通过给 MediaCodec 设置配置信息 ,并成功调用 configure 方法后,编解码器开始进入已配置状态。

  • Running (运行中): 当调用 start 方法后,MediaCodec 真正开始工作,这时进行数据读写操作。通过 queueInputBuffer 添加待编码或待解码的数据,并通过 dequeueOutputBuffer 获取处理完成的数据。

  • End of Stream (流结束): 当所有的输入数据都被处理完毕,并且所有输出数据都被消费后,编解码器会进入 End of Stream 状态。

  • Flushed (刷新): 调用 flush 方法会将编解码器重置到初始状态,但不释放资源,可以继续接收新的数据进行处理。注意,这个状态用于在不重新配置编解码器的情况下重新开始处理数据。

  • Error (错误): 如果在处理过程中发生错误,MediaCodec 会进入 Error 状态。此时,需要调用 reset 方法重置编解码器,使其可以从 Error 状态恢复到 Uninitialized 或 Configured 状态,或者直接通过 release 方法释放资源。

  • Released (已释放): 当不再需要使用 MediaCodec 时,需要及时调用 release 方法释放资源,以免造成性能问题。

API介绍

MediaCodeC 比较重要的常用方法如下所示,简单介绍一下。

方法描述
createDecoderByType(String type)创建解码器
createEncoderByType(String type)创建编码器
configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)配置MediaCodec相关参数,format 是一个 MediaFormat 对象,包含了媒体数据的详细参数。surface 是一个 Surface 对象,用于显示解码的视频。crypto 是一个 MediaCrypto 对象,用于处理加密的媒体数据。flags 是一个标志位,用于指定是编码还是解码。
start()启动 MediaCodec
stop()停止 MediaCodec
release()释放 MediaCodec
getInputBuffers()获取输入缓冲区。输入缓冲区是一个 ByteBuffer 数组,你可以将媒体数据填充到这些缓冲区。
getOutputBuffers()获取输出缓冲区。输出缓冲区是一个 ByteBuffer 数组,你可以从这些缓冲区读取编解码后的数据。
dequeueInputBuffer(long timeoutUs)获取一个可用的输入缓冲区的索引。timeoutUs 是等待输入缓冲区可用的超时时间(以微秒为单位)。
queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags)将填充了数据的输入缓冲区提交给编解码器。index 是输入缓冲区的索引,offset 和 size 指定了有效数据在缓冲区中的位置,presentationTimeUs 是这个缓冲区的呈现时间戳,flags 是标志位。
dequeueOutputBuffer(MediaCodec.BufferInfo info, long timeoutUs)获取一个填充了编解码后数据的输出缓冲区的索引。info 是一个 BufferInfo 对象,用于接收关于输出缓冲区的信息,timeoutUs 是等待输出缓冲区可用的超时时间(以微秒为单位)。
releaseOutputBuffer(int index, boolean render)将输出缓冲区返回给编解码器。index 是输出缓冲区的索引,render 指定是否应该显示这个缓冲区的内容。

实战

播放视频

下面就是播放视频的核心代码示例,也有详细的注释。

11.gif

class MxMediaPlayer(private val filePath: String, private val surface: Surface) {
    // 用于解析视频文件
    private lateinit var mExtractor: MediaExtractor

    // 用于解码视频文件
    private lateinit var mMediaCodeC: MediaCodec

    fun play() {
        mExtractor = MediaExtractor()
        // 设置视频文件路径
        mExtractor.setDataSource(filePath)
        // 获取视频文件的轨道数
        for (i in 0 until mExtractor.trackCount) {
            // 获取视频文件的轨道格式
            val format = mExtractor.getTrackFormat(i)
            // 获取视频文件的MIME类型
            val mime = format.getString(MediaFormat.KEY_MIME)
            if (mime != null) {
                if (mime.startsWith("video/")) {
                    // 选择视频轨道
                    mExtractor.selectTrack(i)
                    // 创建解码器
                    mMediaCodeC = MediaCodec.createDecoderByType(mime)
                    // 配置解码器
                    mMediaCodeC.configure(format, surface, null, 0)
                    break
                }
            }
        }
        // 开始解码
        mMediaCodeC.start()
        // 获取缓冲区数据
        val bufferInfo = MediaCodec.BufferInfo()
        // 是否结束
        var isEnd = false
        while (true) {
            if (!isEnd) {
                // 获取输入缓冲区的索引
                val inIndex = mMediaCodeC.dequeueInputBuffer(10000)
                if (inIndex >= 0) {
                    // 获取输入缓冲区
                    val buffer = mMediaCodeC.getInputBuffer(inIndex)
                    // 读取视频文件数据
                    val sampleSize = mExtractor.readSampleData(buffer!!, 0)
                    if (sampleSize < 0) {
                        // 将结束标志传递给解码器
                        mMediaCodeC.queueInputBuffer(
                            inIndex,
                            0,
                            0,
                            0,
                            MediaCodec.BUFFER_FLAG_END_OF_STREAM
                        )
                        isEnd = true
                    } else {
                        // 将数据传递给解码器
                        mMediaCodeC.queueInputBuffer(
                            inIndex,
                            0,
                            sampleSize,
                            mExtractor.sampleTime,
                            0
                        )
                        // 移动到下一帧
                        mExtractor.advance()
                    }
                }
            }
            // 获取输出缓冲区的索引
            val outIndex = mMediaCodeC.dequeueOutputBuffer(bufferInfo, 10000)
            if (outIndex >= 0) {
                // 更新视图并返回给MediaCodeC进行渲染视频
                mMediaCodeC.releaseOutputBuffer(outIndex, bufferInfo.presentationTimeUs)
            } else if (outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // 获取输出格式
                mMediaCodeC.outputFormat
            }
            // 判断是否结束
            if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                break
            }
        }
        // 释放资源
        mMediaCodeC.stop()
        mMediaCodeC.release()
        mExtractor.release()
    }
}

总结

通过阅读本篇文章,让大家对音视频有个初步的了解和认识,以及如何使用 MediaCodeC 对视频进行编解码。其实还有很多知识点文中并未描述,包括视频压缩算法、音频相关处理、如何对音视频进行编码等。本人认知有限,欢迎大家畅所欲言,有不对的地方请指教。