本篇文章较长,记录自己学习安卓中音视频相关知识的笔记。
视频由许多个帧构成,一个帧相当于一张图片 。
是单位时间内的帧数,决定了视频的流畅度。单位是帧/秒或者frames per second(fps),越大视频越自然流畅。
电影帧率 :24,25fps
游戏帧率:30,60 fps
是在某些标准下用通常可接受的方式对彩色加以描述。用于彩色监视器和一大类彩色视频摄像。
RGB图像具有三个通道R、G、B,分别对应红、绿、蓝三个分量,由三个分量的值决定颜色
是根据一个亮度(Y分量)和两个色度(UV分量)来定义颜色空间
YUV相比于RGB格式最大的好处是可以做到在保持图像质量降低不明显的前提下,减小文件大小
将亮度参量Y和色度参量U/V分开表示的像素格式,主要用于优化彩色视频信号的传输。YUV像素格式来源于RGB像素格式,通过公式运算,YUV三分量可以还原出RGB,YUV转RGB的公式如下:
在YUV家族中, YCbCr
是在计算机系统中应用最多的成员,其应用领域很广泛,JPEG、MPEG均采用此格式。一般人们所讲的YUV大多是指YCbCr
。
模拟信号->采样->量化->编码->数字信号
要大于原声波频率的2倍,人耳能听到的最高频率为20kHz,所以为了满足人耳的听觉要求,采样率至少为40kHz,通常为44.1kHz,更高的通常为48kHz。
量化的时候采用的一个固定的位数来记录这些振幅值,通常有8位、16位、32位。位数越多,还原度越高
声道数是指支持能不同发声的音响的个数,它是衡量音响设备的重要指标之一
一个数据流中每秒钟能通过的信息量,单位bps(bit per second)
码率 = 采样率 * 采样位数 * 声道数
音视频中,其实包含了大量0和1的重复数据,因此可以通过一定的算法来压缩这些0和1的数据。编码可以大大减小音视频数据的大小,让音视频更容易存储和传送。
H26x系列和MPEG系列的编码
H264编码
I帧:帧内编码帧。就是一个完整帧。
P帧:前向预测编码帧。是一个非完整帧,通过参考前面的I帧或P帧生成。
B帧:双向预测内插编码帧。参考前后图像帧编码生成。B帧依赖其前最近的一个I帧或P帧及其后最近的一个P帧
图像组:GOP:Group of picture。指一组变化不大的视频帧。
关键帧:IDR:GOP的第一帧成为关键帧:IDR
DTS(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
PTS(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。
音频数据的承载方式最常用的是脉冲编码调制,即PCM,原始的PCM音频数据也是非常大的数据量,因此也需要对其进行压缩编码。
WAV、MP3、WMA、APE、FLAC等等
ADIF:Audio Data Interchange Format。 音频数据交换格式。只有一个统一的头,所以必须得到所有的数据后解码。
ADTS:Audio Data Transport Stream。 音频数据传输流。可以在任意帧解码,它每一帧都有头信息。
我们平时使用到的视频格式,比如:mp4、rmvb、avi、mkv、mov…其实是包裹了音视频编码数据的容器,用来把以特定编码标准编码的视频流和音频流混在一起,成为一个文件。例如:mp4支持H264、H265等视频编码和AAC、MP3等音频编码。
所谓软解码,就是指利用CPU的计算能力来解码,通常如果CPU的能力不是很强的时候,一则解码速度会比较慢,二则手机可能出现发热现象。但是,由于使用统一的算法,兼容性会很好。
硬解码,指的是利用手机上专门的解码芯片来加速解码。通常硬解码的解码速度会快很多,但是由于硬解码由各个厂家实现,质量参差不齐,非常容易出现兼容性问题。
在Android中使用硬解码APIMediaCodec
实现硬解码
编解码器处理输入数据并产生输出数据,MediaCodec 使用输入输出缓存,异步处理数据。简要地说,一般的处理步骤如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6brDmnKY-1597114956735)(https://kanghanbin.github.io/2020/08/01/音视频开发学习笔记/0.webp)]
MediaCodec 大体上分为三种状态、Stopped、Executing和 Released。
MediaCodec支持三种数据:compressed data, raw audio data 和raw video data
音视频数据提取器,MediaCodec需要我们不断地喂数据给输入缓冲,用来提取音视频文件中数据流。
setDataSource(String path):即可以设置本地文件又可以设置网络文件
getTrackCount():得到源文件通道数
getTrackFormat(int index):获取指定(index)的通道格式
selectTrack(int index):选择通道
getSampleTime():返回当前的时间戳
seekTo(long timeUs, int mode):根据模式seek到给定的时间,有三种模式
SEEK_TO_PREVIOUS_SYNC:跳播位置的上一个关键帧
SEEK_TO_NEXT_SYNC:跳播位置的下一个关键帧
SEEK_TO_CLOSEST_SYNC:距离跳播位置的最近的关键帧
readSampleData(ByteBuffer byteBuf, int offset):把指定通道中的数据按偏移量读取到ByteBuffer中;
advance():读取下一帧数据
release(): 读取结束后释放资源
根据MediaFormat中的编码类型(如video/avc,即H264;audio/mp4a-latm,即AAC)创建MediaCodec
//通过Extractor获取到音视频数据的编码信息MediaFormat
val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
//调用createDecoderByType创建解码器。
mCodec = MediaCodec.createDecoderByType(type)
如果SampleSize返回-1,说明没有更多的数据了。queueInputBuffer的最后一个参数要传入结束标记MediaCodec.BUFFER_FLAG_END_OF_STREAM
。
//查询是否有可用的输入缓冲,返回缓冲索引
var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)
//获取可用的缓冲区,并使用Extractor提取待解码数据,填充到缓冲区中。
val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)
//调用queueInputBuffer将数据压入解码器。
mCodec!!.queueInputBuffer(inputBufferIndex, 0,sampleSize, mExtractor!!.getCurrentTimestamp(), 0)
/**
* 读取视频数据
*/
fun readBuffer(byteBuffer: ByteBuffer): Int {
//【3,提取数据】
byteBuffer.clear()
selectSourceTrack()
var readSampleCount = mExtractor!!.readSampleData(byteBuffer, 0)
if (readSampleCount < 0) {
return -1
}
mCurSampleTime = mExtractor!!.sampleTime
mExtractor!!.advance()
return readSampleCount
}
/**
* 选择通道
*/
private fun selectSourceTrack() {
if (mVideoTrack >= 0) {
mExtractor!!.selectTrack(mVideoTrack)
} else if (mAudioTrack >= 0) {
mExtractor!!.selectTrack(mAudioTrack)
}
}
// 查询是否有解码完成的数据,index >=0 时,表示数据有效,并且index为缓冲区索引
var index = mCodec!!.dequeueOutputBuffer(mBufferInfo, 1000)
when (index) {
//输出格式改变了
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {}
//没有可用数据,等会再来
MediaCodec.INFO_TRY_AGAIN_LATER -> {}
//输入缓冲改变了
MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
mOutputBuffers = mCodec!!.outputBuffers
}
else -> {
return index
}
}
视频的渲染并不需要客户端手动去渲染,只需在config时提供绘制表面surface,如果为编解码器配置了输出表面,则将render设置为true会首先将缓冲区发送到该输出表面。
public void configure (MediaFormat format, Surface surface, MediaCrypto crypto, int flags)
releaseOutputBuffer (int index, boolean render)
config时音频不需要surface,直接传null。需要获取采样率,通道数,采样位数等。需要初始化一个音频渲染器:AudioTrack
AudioTrack只能播放PCM数据流。
//获取最小缓冲区
val minBufferSize = AudioTrack.getMinBufferSize(mSampleRate, channel, mPCMEncodeBit)
mAudioTrack = AudioTrack(
AudioManager.STREAM_MUSIC,//播放类型:音乐
mSampleRate, //采样率
channel, //通道
mPCMEncodeBit, //采样位数
minBufferSize, //缓冲区大小
AudioTrack.MODE_STREAM) //播放模式:数据流动态写入,另一种是一次性写入
mAudioTrack!!.play()
AudioTrack
中有MODE_STATIC
和MODE_STREAM
两种分类。STREAM的意思是由用户在应用程序通过write方式把数据一次一次得写到audiotrack中。这个和我们在socket中发送数据一样,应用层从某个地方获取数据,例如通过编解码得到PCM数据,然后write到audiotrack。这种方式的坏处就是总是在JAVA层和Native层交互,效率损失较大。
而STATIC的意思是一开始创建的时候,就把音频数据放到一个固定的buffer,然后直接传给audiotrack,后续就不用一次次得write了。AudioTrack
会自己播放这个buffer中的数据。这种方法对于铃声等内存占用较小,延时要求较高的声音来说很适用
mCodec!!.releaseOutputBuffer(index, true)
if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
}
释放掉所有的资源。至此,一次解码结束。
mExtractor?.stop()
mCodec?.stop()
mCodec?.release()
/**
* 停止读取数据
*/
fun stop() {
//【4,释放提取器】
mExtractor?.release()
mExtractor = null
}
系统时间作为统一信号源则非常适合,音视频彼此独立互不干扰,同时又可以保证基本一致。
在解码数据出来以后,检查PTS时间戳和当前系统流过的时间差距,快则延时,慢则直接播放
abstract class BaseDecoder(private val mFilePath: String): IDecoder {
//省略其他
......
/**
* 开始解码时间,用于音视频同步
*/
private var mStartTimeForSync = -1L
final override fun run() {
if (mState == DecodeState.STOP) {
mState = DecodeState.START
}
mStateListener?.decoderPrepare(this)
//【解码步骤:1. 初始化,并启动解码器】
if (!init()) return
Log.i(TAG, "开始解码")
while (mIsRunning) {
if (mState != DecodeState.START &&
mState != DecodeState.DECODING &&
mState != DecodeState.SEEKING) {
Log.i(TAG, "进入等待:$mState")
waitDecode()
// ---------【同步时间矫正】-------------
//恢复同步的起始时间,即去除等待流失的时间
mStartTimeForSync = System.currentTimeMillis() - getCurTimeStamp()
}
if (!mIsRunning ||
mState == DecodeState.STOP) {
mIsRunning = false
break
}
if (mStartTimeForSync == -1L) {
mStartTimeForSync = System.currentTimeMillis()
}
//如果数据没有解码完毕,将数据推入解码器解码
if (!mIsEOS) {
//【解码步骤:2. 见数据压入解码器输入缓冲】
mIsEOS = pushBufferToDecoder()
}
//【解码步骤:3. 将解码好的数据从缓冲区拉取出来】
val index = pullBufferFromDecoder()
if (index >= 0) {
// ---------【音视频同步】-------------
if (mState == DecodeState.DECODING) {
sleepRender()
}
//【解码步骤:4. 渲染】
render(mOutputBuffers!![index], mBufferInfo)
//【解码步骤:5. 释放输出缓冲】
mCodec!!.releaseOutputBuffer(index, true)
if (mState == DecodeState.START) {
mState = DecodeState.PAUSE
}
}
//【解码步骤:6. 判断解码是否完成】
if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
Log.i(TAG, "解码结束")
mState = DecodeState.FINISH
mStateListener?.decoderFinish(this)
}
}
doneDecode()
release()
}
}
private fun sleepRender() {
val passTime = System.currentTimeMillis() - mStartTimeForSync
val curTime = getCurTimeStamp()
if (curTime > passTime) {
Thread.sleep(curTime - passTime)
}
}
override fun getCurTimeStamp(): Long {
return mBufferInfo.presentationTimeUs / 1000
}
MediaMuxer有助于音视频分装到指定格式。当前,MediaMuxer支持MP4,Webm和3GP文件作为输出。自从Android Nougat开始,它还支持在MP4中混合B帧。
muxer = new MediaMuxer(outputFile.getAbsolutePath(),
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
audioTrackIndex = muxer.addTrack(audioEncoder.getOutputFormat());
videoTrackIndex = muxer.addTrack(videoEncoder.getOutputFormat());
muxer.start();
开始之后,就可以调用MediaMuxer.writeSampleData()向mp4文件中写入数据了。需要注意每次只能添加一帧视频数据或者单个Sample的音频数据,并且BufferInfo对象的值一定要设置正确
muxer.writeSampleData(audioTrackIndex, encodedData, bufferInfo);
muxer.writeSampleData(videoTrackIndex, encodedData, bufferInfo);
muxer.stop();
muxer.release();
官方文档developer
Android 音视频开发打怪升级:音视频硬解码篇