Android音视频开发知识点

码率

val audioSource = MediaRecorder.AudioSource.MIC
val sampleRateInHz = 8000
val channelConfig = AudioFormat.CHANNEL_IN_MONO
val audioFormat = AudioFormat.ENCODING_PCM_16BIT
val minBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
val audioRecord = AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, minBufferSize)

在Android中可以使用AudioRecord来录制麦克风的声音,如上代码是创建了一个AudioRecord对象,通过这个对象就可以采集到麦克风的声音了,采集到的声音格式为pcm格式,也就是无压缩的原始音频数据格式。上面代码中有三个重要参数,如下:

  • sampleRateInHz 采样频率(个人理解:声音传播就像一条线一样源源不断地传进麦克风,麦克风收到了这条线,而我们的并没有完整的去保存这整条线,而是在这条线上采一些点,打个比方:假如一秒钟进来1万个点,但我只在这1万个点里面采集8000个点,这样的话我们就说采样频率是8000Hz,代表每秒钟采集8000个声音点,1000可用1K表示,8000则为8K,所以一般说8KHz采样率。相应的,44.1KHz就代表每秒钟采集44100个声音点)
  • channelConfig 通道配置,手机一般配置为单声道采集即可
  • audioFormat 声音格式,用于配置采样位数(个人理解:用于指定采集到的一个声音点是使用8bit存储或是用16bit存储。有点类似颜色,一个颜色由红绿蓝和透明度组成,那一个颜色用多少bit保存呢,它也会有8位、16位、32位等的多种选择)

音视频开发是比较难,上面也是我自己的理解,可能是错误的,这里提供一篇别人的文章,仅供参考:https://www.jianshu.com/p/86e1b1017564

如果使用采样频率为8000Hz,采样位数为16位,通道为单声道,则一秒种采集到的声音的数据量为:

采样频率 x 采样位数 x 通道数 = 8000 x 16 x 1 = 128000bit

注:这个单位是bit(比特位)可以简写为b,8个bit为1个字节,1024字节为1KB,所以128000位换成KB等于:

128000 ÷ 8 ÷ 1024 = 15.625KB

如果采集1分钟,则采集到的PCM音频大小为:15.625KB x 60秒 = 937.5KB,差不多1MB。如果录3分钟(差不多一首歌曲的时长),则需要不到3MB的存储空间,注意,这是原始PCM音频数据需要的存储空间,算是非常小的了,平时我们下载的mp3音频是经过压缩的,也是3M左右,所以1分钟的PCM音频才3MB是非常小的。

在压缩的时候,有一个压缩参数叫码率(也叫做比特率),码率的单位是bit,表示1秒钟的PCM压缩后的大小是多少,比如我们设置码率为32000bit,假如1秒钟的PCM数据大小为128000bit(也就是15.625KB),压缩成AAC后,音频大小就变成了32000bit(大约4KB),看到了吧,原本15KB的音频,压缩后变成4KB,这个压缩比例为:128000 ÷ 32000 = 4倍,也就是说,压缩后的音频大小为原来的4分之一,小了很多,方便传输或存储。

如果按照前面例子的参数,录制1分钟,并按32kb的码率压缩为AAC,则1分钟的AAC文件大小约234KB,天哪,怎么这么小!因为我们使用的采样率比较低,所以AAC就小,相应的声音质量就低,8KHz的采样频率适合人的通话声音,如果要录制音乐,则需要使用更高的采样频率,相对的压缩时使得的码率也得跟着提高才行,比如,你使用44100Hz的采样频率,则1秒钟的PCM大小为:

44100 x 16 x 1 = 705600bit

如果你还使用32kb的码率,则比原来小了22倍(705600 ÷ 32000 = 22.05),那太恐怖了,声音质量肯定会大大下降的,705600bit大约为86KB,32000bit大约为4KB,86KB的音频变成4KB,你品,你细品!实际测试时,我发现44.1KHz + 32kb码率录制的音乐比8KHz + 32kb的音质好很多,而我使用的是动态码率,44.1KHz录1分钟为254K,8KHz录1分钟为239K,也没大多少,但是音质却好很多,神奇哈,一个压缩了22倍的音质竟然比压缩了4倍的好,只因一个采样率高,一个采样率低。

那AAC的码率选多少合适呢?我也不知道,百度上也找不到文章介绍说什么采样频率应该使用什么码率的。所以也只能靠自猜了,压缩比例为4倍肯定是没问题的,倍数太大了声音失真,倍数太小了音频文件太大,所以选 4 ~ 7倍的压缩率是比较适中的(这是我自己乱猜的),而我们平时常见的码率有320kb,256kb,192kb,128kb,64kb,32kb,那我们就使用这些常见码率的其中一个即可,挑选时自己计算一下压缩率,如果压缩率在4 ~ 7倍则是合适的,希望音质好一点则压缩率就调小一点,希望文件小一点,则把压缩率调大一点,比如,44100Hz采样率的音频,我希望用6倍的压缩率,则44100 x 16 x 1 ÷ 6 = 117600bit,就是说用6倍的6压缩率,压缩后大小为117600bit(约为117kb),然后我们看它与128kb这个常见码率接近,则可以使用128kb作为压缩码率。

需要注意的是:

  • 码率是使用bit(比特位)来作为单位的,8b(8位),8kb(8千位),8mb(8兆位)
  • 我们平时是使用Byte(字节)来作为单位的,8B(8字节),8KB(8千字节),8MB(8兆字节)
  • 1kb、1mb可以简写为1k、1m,1KB、1MB也可简写为1K、1M
  • 小写的b、kb、mb之间的换算是要乘1000,如1000b = 1kb,1000kb = 1mb
  • 大写的B、KB、MB之间的换算是要乘1024,如1000B = 1KB,1000KB = 1MB
  • B和b也是可以换算的,1B = 8b,所以bit(位)单位可以和byte(字节)相互转换,示例如下:

比如32kb的码率,把位单位(千位:kb)换成我们熟悉的字节单位(千字节:KB),步骤如下:

  1. 把32kb换成bit:32 x 1000 = 32000b
  2. 把bit换成对应千字节(KB):32000 ÷ 8 = 4000Byte,4000 ÷ 1024 = 3.9KB

一般表示比特率时,会用bps来表示 ,如32kbps。bps的意思为:bit per second,即每秒钟传输的比特数量,32kbps即表示每秒传输的比特位数量为32kb。

注意:网络供应商,如电信,在介绍宽带时,一般使用形如4Mbps的方式来表示网速(注意,这里的M是大写而b是小写),则它最初是这样转变过来的:b -> Kb -> Mb,前面有介绍到,大写的转换是要乘1024的,所以1024b = 1Kb,1024Kb = 1Mb。把位单位(兆位:mb)换成我们熟悉的字节单位(兆字节:MB),如下:

  1. 把码率换成bit:4 * 1024Kb = 4096Kb(因为M大写所以乘1024),4096Kb * 1024 = 4194304b
  2. 把bit换成对应的字节单位(兆字节:MB):4194304b ÷ 8 = 524288byte,524288byte ÷ 1024 = 512KB,512KB ÷ 1024 = 0.5MBps

由此可见,当你拉了一条4Mbps的宽带时,你下载文件的速度最大就是0.5MB每秒,不要以为是4M每秒哦!注意:看上面的换算,步骤1是乘两个1024,而步骤2是除两个1024,还多除了一个8,所以两个1024可以化掉,直接除8好可,如4Mbps = 4 / 8 = 0.5MBps。

总结:

  • bps 表示每秒多少个位
  • Bps 表示每秒多少个字节,换成位就是8bps,所以bps与Bps是8倍的关系(在单位相同的情况下),如:Bps是bps的8倍(8b = 1B),KBps是Kbps的8倍,KB对Kb,两者的K都代表1024可以化掉,剩下B和b自然就是8倍的关系了。MBps是Mbps的8倍。所以100Mbps的网线下载速度为100 ÷ 8 = 12.5MBps
  • b -> kb -> mb,每个转变乘1000,如1000b = 1kb,1000kb = 1mb
  • b -> Kb -> Mb,每个转变乘1000,如1024b = 1Kb,1024Kb = 1Mb
  • B -> KB -> MB,每个转变乘1024,如1024B = 1KB,1024KB = 1MB。MB与Mb,M相同可以化掉,剩下B和b,1B=8b,所以1MBps = 8Mbps
  • mBps,应该没人使用这种形式的。
  • mbps一般用来表示码率(用于音视频压缩)
  • Mbps和MBps一般用来表示网速
  • 采样频率KHz中的K是大写,但是它表示1000

对应的视频也有码率,码率设置多少合适也可参照音频这里的方法,比如yuv视频压缩为h264视频,一般压缩比例是多少,然后根据你的实际yuv大小除以压缩比例,就得到一个码率,然后再去找找一些觉见的视频压缩码率,找一个接近的码率即可。

打印MediaCodec支持的H264编码器

object H264Util {

    /** 打印支持的H264编码器 */
    @Suppress("DEPRECATION")
    fun printSupportedH264Encoder() {
        val codecCount = MediaCodecList.getCodecCount()
        for (i in 0 until codecCount) {
            val codecInfo = MediaCodecList.getCodecInfoAt(i)
            if (!codecInfo.isEncoder) continue              // 如果不是编码器,则找下一个
            val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC   // H264的mime类型
            val supportedH264 = codecInfo.supportedTypes.any { type -> type.equals(mimeType, true) }
            if (supportedH264) {
                val colorFormats = codecInfo.getCapabilitiesForType(mimeType).colorFormats
                // 通过int值,找到它是在MediaCodecInfo.CodecCapabilities中的哪个变量
                val colorFormatsConvert = Array(colorFormats.size) { index ->
                    ReflectUtil.getPublicStaticIntFieldNameByValue(MediaCodecInfo.CodecCapabilities::class.java, colorFormats[index])
                }
                Timber.i("H264编码器:${codecInfo.name}, 支持的颜色格式:${colorFormatsConvert.contentToString()}")
            }
        }
    }
}

object ReflectUtil {

    /** 获取指定类中的所有public static int类型的变量 */
    fun getAllPublicStaticIntField(clazz: Class<*>): Map<Int, String> {
        val fieldsMap: MutableMap<Int, String> = HashMap()
        clazz.fields.filter {
            Modifier.isPublic(it.modifiers)
                    && Modifier.isStatic(it.modifiers)
                    && it.genericType === Int::class.javaPrimitiveType
        }.forEach {
            fieldsMap[it.get(null) as Int] = it.name
        }
        return fieldsMap
    }

    /** 返回指定的值对应的是指定类的哪个变量,如果没有对应的变量,则返回value自身 */
    fun getPublicStaticIntFieldNameByValue(clazz: Class<*>, value: Int): String {
        return getAllPublicStaticIntField(clazz)[value] ?: value.toString()
    }
    
}

如,在我的一个手机上运行H264Util.printSupportedH264Encoder(),结果如下:

H264编码器:OMX.qcom.video.encoder.avc, 支持的颜色格式:[2141391876, COLOR_FormatSurface, COLOR_FormatYUV420Flexible, COLOR_FormatYUV420SemiPlanar]
H264编码器:OMX.google.h264.encoder, 支持的颜色格式:[COLOR_FormatYUV420Flexible, COLOR_FormatYUV420Planar, COLOR_FormatYUV420SemiPlanar, COLOR_FormatSurface]

其中:

OMX.qcom.video.encoder.avc   是硬件编码器,支持NV12
OMX.google.h264.encoder      是软件编码器,支持NV12I420

通过如下方法创建的是硬件编码器:

MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)

MediaCodec编码方式

1、获取缓冲区,往里面填入原始视频数据,再把缓冲区还给编码器
2、编码器把缓冲区中的数据进行编码,再往入缓冲区中,我们从缓冲区中取出编码好的压缩数据

需要注意的是:并不是放入一帧就立马编码一帧的数据,比如放入第一帧的时候,我们从输出缓冲区中并不能拿到数据,放入第二帧后,从输出缓冲区可以拿到3帧的数据(包含有一个配置帧),所以这是为什么在输出缓冲中使用while循环来获取数据的原因。

每次从编码器缓冲区取出来的数据都是正好一帧的数据,第一次出来的数据是配置帧,包含sps和pps的数据,第二帧肯定是一个关键帧,也称为IDR帧(视频的第一个着键帧称为IDR帧,其它关键帧不能称为IDR帧),后面就是P帧,多个P帧后又是I帧(关键帧),没看到过B帧,不知道要怎么样配置编码器可以出来B帧。除了前面的帧特殊一点之外,后面的都是放入一帧视频就会立马编码出一帧视频,所以那时候的while循环就显得有点多余,不知道去掉while循环能有多少优化。不过官方都是这样写的,说明while循环的影响不大,还是得按官方的写。当使用异步模式的时候,发现放入5帧才开始出数据,而且也不是一次出5帧的,有时候会连续往入两帧,有时候会连续出两帧,在编码结束的时候会一次出四五个普通帧。

I帧间隔:比如设置为2秒,则每2秒就会出来一个I帧,如果我们给编码器设置帧速为25帧/秒,即使我们并没有每秒给编码器25帧也没关系,反正编码器会统计给的帧数,每50帧的时候就会编码一个I帧出来,因为设置I帧间隔是2秒,每秒25帧,所以是每50帧出一个I帧,即第1、51、101、151帧是I帧。

在保存本地视频时,在视频的开头保存一次配置帧就可以了,配置帧中包含有视频的分辨率、帧速等信息,这样播放器才知道怎样播放。

而在网络发送中,一般每个关键帧前面都要加上配置帧再发送,或者每隔多少秒加一个配置帧到关键帧前面,预防最开始的配置帧网络发送失败,那后面的帧如果没有了配置帧则网络播放器就不知道如何解析这个视频流了。

var outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10_000)

outputBufferIndexMediaCodec.INFO_TRY_AGAIN_LATER,千万别一直死循环取数据了,或者睡眠一会再取数据也是不可取的,因为编码器里面没数据了也会返回这个,也如果没有往编码器放入新的数据,就是取等多久再取也是取不出数据的。

MediaCodec编码器的坑(超时时间的设置)

var outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10_000)

如上代码,取输出缓冲的索引时,有个超时参数,设置为负数则无限等待直到有数据,这里我们设置为 10_000,官方Demo用的也是这个值,它的单位为微秒,1000微秒 == 1毫秒,所以 10_000微秒 对应为 10毫秒,这有点浪费了,这样的话每编码一帧音频或者视频都会浪费10毫秒的等待时间(因为我们是用while循环来调用这个函数的,当有数据的时候它的返回正常来说是很快的,但是while循环取完数据后肯定就没有数据了,要结束循环了,此时必定会浪费最少10毫秒来等待),而且还会有偏差,比如我们设置为0,也就是说不等了,但是测试函数的调用时间发现也可能需要0 ~ 2毫秒,所以设置超时为10毫秒时,它的调用时间为10 ~ 12毫秒,每编一帧数据就浪费12毫秒的话确实太大了,既然它的单位是微秒的话,是否设置为100微秒就够了。但是我们又说到设置为0它的函数调用也可能会需要2毫秒,所以这个超时设置多少比较合适呢?看心情吧,总之设置为10毫秒的话肯定是太大了。

另外,在工作中,海能达提供的一款执法仪设备,是Android 11系统的,超时设置为10毫秒,正常情况下它的调用时间确实是10 ~ 12毫秒,但是当我熄屏之后,它就出Bug了,远远大于10毫秒,高的时候能上到50毫秒,所以时间就不够用了,导致丢帧,然后我把数值调小,设置为100微秒(即0.1毫秒),但是函数的调用时间依然能上到50毫秒。然后相同的代码,我运行到我的小米手机(Android13系统)和公司的一些Android测试机上测试都没问题,设置的超时为10毫秒,则熄屏后函数的调用时间也是10毫秒左右的,所以可以肯定的说,海能达的这款产品是有Bug的,一开始没发现是这个地方有Bug,以为是编码效率降低了,跟海能达公司扯了好久,他们一直说是我们的代码有问题,说别的公司对接的都没听说有问题的,我也是醉了,后来我发了我写的简单Demo给他们,他们估计也看不出哪里有问题,然后才肯把他们的Demo发给我,因为他们的Demo测试了是没问题的。经过对比我才发现它这个超时时间设置为0,设置为0的话就不会有问题,但正常来说的话,这个参数不应该设置为0的,要不然官方为什么要提供这个超时参数呢!

后面经过测试,发现AudioRecord给数据是有时间间隔的,就像视频帧率为25帧,则给的每一帧也是有间隔是一样的原因。而且MediaCodec有两个队列,一个存的队列,一个放的列队,每个大队的大小为4,最坏的情况就是队列全装满,即使这样也能一次有8帧的音频放到编码器中,比如先放4帧,由于给音频有时间间隔,这个间隔足够编码器进行编码的了,所以放第5帧时,前面的4帧肯定都已经编码完成了,编码完的会被放入到输出的队列中,然后输入的队列就相当于又可以用来存了,此时可以再存入4帧音频,但是这会编码后没有输出队列可用了(假设我们一直不去取的话),所以由次可知,有8个缓冲区让我们可以进行缓冲,即使我们在设置 timeoutUs参数时,设置为0也是不会有问题的,而且这样的话能节省更多的时间,因为没缓冲可用时我们无需浪费等待时间。那什么时候需要设置超时时间呢?比如我们读取一个pcm文件来转为aac,此时就需要设置获取put队列的超时时间了,因为读取文件非常快,不像录音,每次获取录音数据会有间隔,这些间隔足已让编码器完成前面数据的编码工作,而读文件是一直读,且速度快,所以就有可能导致编码器编码比读取文件快,那编码器的缓冲就会不够用,则此时设置超时是有用的,比如我编码器取数据的超时时间设置为0,则我们很快就能放满4帧音频,此时put队列就满了,但是编码器来不及很快的把这4帧编码为aac,所以我们从编码器取数据时会取不到,然后又到放数据,此时就没有可用的put缓冲了,如果设置超时为0的话,则立马返回-1,则这一帧的音频就会被丢掉了,所以应该设置等待时间,以便编码器能完成编码工作清出可用缓冲,等获取put队列处 等待状态时,读文件的操作也应该是阻塞状态,假设读文件的内容是要放到一个队列中的,则应该是用会阻塞的放数据到队列的函数,因为这样的话如果队列满了,读文件调用put时就会阻塞,直到编码器这边从队列里消费了数据有空了才能再继续。

这么看的话,好像从编码器取数据是不管什么情况都是不需要设置超时的。只有放编码器放数据时才需要设置超时。比如put队列放满了,此时再想数据到put列队就需要超时等待,设置一个合理的等待时间,让编码器最少能完成一帧的编码,则此时put中的数据就会有一帧编码完成并放到out队列中,此时put队列就有可用的了,放完这一帧时再到out队列取肯定就能立马取到数据了。也就是说只要是因为put队列满了处于等待,然后等到有可用的了,说明此时out队列肯定也装有编码好的数据了。所以out队列无需设置等待,只设置put的即可,来源快,编码慢,只让put加超时就能完成平衡状态了。

总结:

  • 最多只需要设置put队列的超时,out队列什么情况都不用设置超时。
  • 数据来源比编码速度慢时无需设置超时,比如录音每40ms给一次音频,而编码一次音频所需要的时间远远小于40ms
  • 数据来源比编码速度快时需要设置超时,比如直接读pcm文件并编码为aac,读一帧pcm的速度远比编码器编码一帧pcm为aac的速度快。

当然,我说的这些也是全凭空想,没有实际去做实验验证,大家如果有这些应用场景的应该要自己去实验一下,我的推断不一定正确。

2023-12-20 续:
刚想到,其实设置timeoutUs效率是更高的,设置为0反而效率会降低,比如摄像头采集视频并编码为H264,假设帧率为25帧,则大概每40ms给一帧,然后编码器又是在子线程上编码的,我们把timeoutUs设置为0没有好处,因为没数据你不等待虽然可以立马结束,然后你就可以再继续放入第二帧视频了,但是Camera是每40ms才给一次的呀,所以你需要等Camera给你数据了你才能放到编码器呀,所以此时也是要等的,既然都要等,那我何不在编码器这边等呢?假设因为我等了一下编码器就有编码好的数据了,我就可以即使取出来进行网络发送或者本地保存了,同时也不会影响Camera给数据的,除非你编码器处理的非常慢,但是现在编码器都有硬编码的,处理一帧视频我算你花20ms了,Camera每40ms才给一帧,那我还有20ms可用呢,我在编码器这等10ms怎么了!所以,总结timeoutUs还是不要设置为0了,这会降低效率的,我就说嘛,人家Android官方示例代码都是设置10ms的,官方的一般是正确做法。

另外音频的编码也一样,我们要搞清楚音频多久给一帧数据,然后再看编码器处编码一帧音频需要多久,这样还剩多少时间,然后就能知道如何设置一个合理的等待时间了。

刚想了个点子,把timeoutUs设置时间大一些,但是又不能太大,起码要限制在不超过数据输入的时间间隔,比如Camera每40ms给一帧,那我们的timeoutUs肯定不能大于40ms,然后要减去一些其它的消耗时间。然后计算放入编码器一帧,到从编码器可以出一 帧时的时间是多少。编码器的输入和输出队列都是4,大不了我们把队列装满了,然后再取,输出队列是4,那我们就看取出第5帧时的时间是多少,等有时间我再做实验。

MediaCodec编码过程探索

	private val timeoutUs = 100L

	/** 把PCM格式的音频数据编码为AAC数据 */
    fun encodePCMToAAC(pcmByteBuffer: ByteBuffer) {
        // 1、把pcm数据放入编码器中进行编码
        putDataToEncoder(pcmByteBuffer)
        // 2、从编码器中取出编码后的数据
        getDataFromEncoder()
    }

    private var count = 0

    /** 把pcm数据放入编码器中进行编码 */
    private fun putDataToEncoder(pcmByteBuffer: ByteBuffer) {
        val flags = if (pcmByteBuffer.capacity() == 0) MediaCodec.BUFFER_FLAG_END_OF_STREAM else 0
        inputBufferIndex = mMediaCodec.dequeueInputBuffer(timeoutUs)
        count++
        if (inputBufferIndex >= 0) {
            inputBuffer = mMediaCodec.getInputBuffer(inputBufferIndex)!!
            inputBuffer.put(pcmByteBuffer)
            mMediaCodec.queueInputBuffer(inputBufferIndex, 0, pcmByteBuffer.limit(), System.nanoTime() / 1000, flags) // 这个函数调用时间:1~ 5毫秒
            Timber.i("${count} 已放入数据,putIndex = $inputBufferIndex")
        }        
    }

    /** 从编码器中取出编码后的数据 */
    private fun getDataFromEncoder() {
        Timber.i("${count}准备取数据")
        while (mMediaCodec.dequeueOutputBuffer(mBufferInfo, timeoutUs).also { outputBufferIndex = it } >= 0) {
            if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
                Timber.i("已经到达流的终点了,outIndex = $outputBufferIndex")
                mMediaCodec.releaseOutputBuffer(outputBufferIndex, false)
                mMediaCodec.stop()
                mMediaCodec.release()                
                break
            } else if (mBufferInfo.flags != MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
                Timber.i("${count}有音频数据了,outIndex = $outputBufferIndex")
                outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex)!!
                Timber.i("${count}取到音频数据,outIndex = $outputBufferIndex")

                if (!::aacByteBufferPool.isInitialized) {
                    aacByteBufferPool = ByteBufferPool(outputBuffer.capacity(), "aacSaveThread")
                    aacSaveThread = AACSaveThread(aacByteBufferPool, sampleRateInHz, channelCount).also { it.start() }
                }

                // TODO 使用outputBuffer
            } else {
                Timber.i("${count}有配置数据了,outIndex = $outputBufferIndex")
                outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex)!!
                Timber.i("${count}取到配置数据,outIndex = $outputBufferIndex")
                Timber.i("下面输出配置信息,共${mBufferInfo.size}个字节-------------")
                while (outputBuffer.hasRemaining()) {
                    Timber.i(outputBuffer.get().toString(2))
                }
                Timber.i("---------------------------")
            }

            mMediaCodec.releaseOutputBuffer(outputBufferIndex, false) // 把缓存对象还给MediaCodec
        }
        Timber.i("${count}无数据可取,outIndex = $outputBufferIndex")
    }

运行在一各Android设备上的部分日志如下:

2023-12-19 09:54:40.462 1 已放入数据,putIndex = 0
2023-12-19 09:54:40.462 1准备取数据
2023-12-19 09:54:40.464 1无数据可取,outIndex = -2
2023-12-19 09:54:40.502 2 已放入数据,putIndex = 1
2023-12-19 09:54:40.503 2准备取数据
2023-12-19 09:54:40.504 2有配置数据了,outIndex = 0
2023-12-19 09:54:40.505 2取到配置数据,outIndex = 0
2023-12-19 09:54:40.506 下面输出配置信息,共2个字节-------------
2023-12-19 09:54:40.514 10100
2023-12-19 09:54:40.515 1000
2023-12-19 09:54:40.515 ---------------------------
2023-12-19 09:54:40.521 2有音频数据了,outIndex = 1
2023-12-19 09:54:40.522 2取到音频数据,outIndex = 1
2023-12-19 09:54:40.543 2无数据可取,outIndex = -1
2023-12-19 09:54:40.547 3 已放入数据,putIndex = 2
2023-12-19 09:54:40.548 3准备取数据
2023-12-19 09:54:40.549 3无数据可取,outIndex = -1
2023-12-19 09:54:40.582 4 已放入数据,putIndex = 3
2023-12-19 09:54:40.584 4准备取数据
2023-12-19 09:54:40.585 4有音频数据了,outIndex = 0
2023-12-19 09:54:40.586 4取到音频数据,outIndex = 0
2023-12-19 09:54:40.589 4无数据可取,outIndex = -1
2023-12-19 09:54:40.622 5 已放入数据,putIndex = 0
2023-12-19 09:54:40.623 5准备取数据
2023-12-19 09:54:40.625 5有音频数据了,outIndex = 0
2023-12-19 09:54:40.627 5取到音频数据,outIndex = 0
2023-12-19 09:54:40.629 5无数据可取,outIndex = -1
2023-12-19 09:54:40.664 6 已放入数据,putIndex = 1
2023-12-19 09:54:40.665 6准备取数据
2023-12-19 09:54:40.667 6无数据可取,outIndex = -1
2023-12-19 09:54:40.703 7 已放入数据,putIndex = 2
2023-12-19 09:54:40.703 7准备取数据
2023-12-19 09:54:40.705 7有音频数据了,outIndex = 0
2023-12-19 09:54:40.705 7取到音频数据,outIndex = 0
2023-12-19 09:54:40.707 7无数据可取,outIndex = -1
2023-12-19 09:54:40.742 8 已放入数据,putIndex = 3
2023-12-19 09:54:40.743 8准备取数据
2023-12-19 09:54:40.744 8有音频数据了,outIndex = 0
2023-12-19 09:54:40.745 8取到音频数据,outIndex = 0
2023-12-19 09:54:40.747 8无数据可取,outIndex = -1
2023-12-19 09:54:40.784 9 已放入数据,putIndex = 0
2023-12-19 09:54:40.786 9准备取数据
2023-12-19 09:54:40.788 9无数据可取,outIndex = -1
2023-12-19 09:54:40.820 10 已放入数据,putIndex = 1
2023-12-19 09:54:40.821 10准备取数据
2023-12-19 09:54:40.822 10无数据可取,outIndex = -1

这里我是编码pcm为aac,采样率为16000,单声道。

代码很简单,总的来说pcm编码为acc就两步:putDataToEncodergetDataFromEncoder,即把pcm放入编码器,然后从编码器取出编好的aac。

日志分析:

上面是连续的日志,比较长,这里截取一小段一小段的分析:

2023-12-19 09:54:40.462 1 已放入数据,putIndex = 0
2023-12-19 09:54:40.462 1准备取数据
2023-12-19 09:54:40.464 1无数据可取,outIndex = -2

给MediaCodec放入第1帧音频后,立马取时是无数据的,因为我们的 timeoutUs参数设置为100微秒,这编码器不可能在100微秒内就编码好第一帧的数据了, 返回的索引是-2,-2为常量 MediaCodec.INFO_OUTPUT_FORMAT_CHANGED,日志接着往下看:

2023-12-19 09:54:40.502 2 已放入数据,putIndex = 1
2023-12-19 09:54:40.503 2准备取数据
2023-12-19 09:54:40.504 2有配置数据了,outIndex = 0
2023-12-19 09:54:40.505 2取到配置数据,outIndex = 0
2023-12-19 09:54:40.506 下面输出配置信息,共2个字节-------------
2023-12-19 09:54:40.514 10100
2023-12-19 09:54:40.515 1000
2023-12-19 09:54:40.515 ---------------------------
2023-12-19 09:54:40.521 2有音频数据了,outIndex = 1
2023-12-19 09:54:40.522 2取到音频数据,outIndex = 1
2023-12-19 09:54:40.543 2无数据可取,outIndex = -1
2023-12-19 09:54:40.547 3 已放入数据,putIndex = 2
2023-12-19 09:54:40.548 3准备取数据
2023-12-19 09:54:40.549 3无数据可取,outIndex = -1

这里放入第帧音频,然后取的时候发现有配置数据可取,之前我们放第一帧的时候putIndex是0,取的时候outIndex是0,但是它是配置数据,并不是aac数据,这应该可以说明put的列队和out的列队是两个队列,因为它们的索引是分开的,否则的话你放索引 0 放入pcm,那取的时候应该能在索引 0 取出aac。从日志上看,配置数据只有两个字节,这两个字节是表示什么东西目前我还不知道,有时间再研究。取了配置数据后,因为是while循环的,再取发现还有数据可取,此时的数据就是由pcm编码后的aac数据了,通过对比放入的第1帧时间和现在可取的时间,如下:

2023-12-19 09:54:40.462 1 已放入数据,putIndex = 0
2023-12-19 09:54:40.502 2 已放入数据,putIndex = 1
2023-12-19 09:54:40.521 2有音频数据了,outIndex = 1

从这里日志看时间,放入第2帧数据的时候,距离放入第1帧已经过去了40ms了,所以此时第一帧的pcm肯定编码完成了,所以此时出的 outIndex = 1的aac数据是 putIndex = 0时放入的pcm编码出来的数据。取完这一帧之后又没数据了,因为第2帧音频刚刚放进来,也没那么快编码好。

从这里也可以了解到为什么取编码数据时要用 while 循环,因为有可能你取的时候可以一次取出几帧数据的,比如上面的取了配置数据之后立马又能出现一帧aac数据。

从这个日志分析来看的话,mMediaCodec.dequeueOutputBuffer(mBufferInfo, timeoutUs)中的第二个参数timeoutUs感觉是可以设置为0,因为设置为0好像也不会丢失数据,因为编码器内部是有队列的,比如你调用这个函数时编码器还没有编码好的数据,如果我们timeoutUs设置为0,则立马结束这次取数据的操作了,假设此时过了1毫秒就有数据了,也没事啊,这个编码好的数据就让它在队列里放着先罢,因为存和取这两个函数我们是在同一个线程中调用的,所以取的操作结束了才能调用放数据的操作,当然这个放数据也不是立马就能放的,因为放数据我们是从队列里面取数据的,队列中的数据又是来自录音机,录音的时候每次一次数据也是有一些时间间隔的,就像视频一样,假设你帧率为25帧,则Camera每给我们一帧肯定是要隔了一些时间的,按1秒平均除的话每帧的相隔时间应该40ms。所以对于音频处理也是一个,每放一帧音频也是会有一些时间间隔的,这些间隔就让编码器对之前放入的数据有足够的时间进行编码,所以前面放入的数据虽然不能立马就能取到,但是放两帧或更多帧的时候,最前面放入的肯定就已经编码好了,这么想的话,这个timeoutUs参数确实可以设置为0,这样还能节省时间,不然的话,比如我设置为10毫秒,则每编码一帧都会浪费10毫秒来等待,这会是非常大的浪费。

音频数据是非常小的,编码一帧需要的时间很小(10ms以内),所以如果我timeoutUs设置为10毫秒的话第一帧数据放入后,我是否可以放完数据就能先取数据了,然后再放第二帧数据,经实验不是这样的,即使我设置timeoutUs设置为30毫秒也不行,因为第一次取的时候,它压根就不会等,也就是第一次调用 mMediaCodec.dequeueOutputBuffer(mBufferInfo, 30_0000)的时候,这个函数可以说是立马就返回了,返回的是-2。日志如下:

2023-12-19 11:52:28.767 1 已放入数据,putIndex = 0
2023-12-19 11:52:28.768 1准备取数据
2023-12-19 11:52:28.771 1无数据可取,outIndex = -2
2023-12-19 11:52:28.804 2 已放入数据,putIndex = 1
2023-12-19 11:52:28.804 2准备取数据
2023-12-19 11:52:28.806 2有配置数据了,outIndex = 0
2023-12-19 11:52:28.806 2取到配置数据,outIndex = 0
2023-12-19 11:52:28.807 下面输出配置信息,共2个字节-------------
2023-12-19 11:52:28.810 10100
2023-12-19 11:52:28.810 1000
2023-12-19 11:52:28.811 ---------------------------
2023-12-19 11:52:28.820 2有音频数据了,outIndex = 0
2023-12-19 11:52:28.820 2取到音频数据,outIndex = 0
2023-12-19 11:52:28.871 2无数据可取,outIndex = -1
2023-12-19 11:52:28.876 3 已放入数据,putIndex = 2
2023-12-19 11:52:28.878 3准备取数据
2023-12-19 11:52:28.910 3无数据可取,outIndex = -1
2023-12-19 11:52:28.916 4 已放入数据,putIndex = 3
2023-12-19 11:52:28.918 4准备取数据
2023-12-19 11:52:28.923 4有音频数据了,outIndex = 0
2023-12-19 11:52:28.925 4取到音频数据,outIndex = 0
2023-12-19 11:52:28.960 4无数据可取,outIndex = -1
2023-12-19 11:52:28.963 5 已放入数据,putIndex = 0
2023-12-19 11:52:28.965 5准备取数据
2023-12-19 11:52:28.966 5有音频数据了,outIndex = 0
2023-12-19 11:52:28.968 5取到音频数据,outIndex = 0
2023-12-19 11:52:29.001 5无数据可取,outIndex = -1
2023-12-19 11:52:29.004 6 已放入数据,putIndex = 1
2023-12-19 11:52:29.005 6准备取数据
2023-12-19 11:52:29.037 6无数据可取,outIndex = -1

截取其中两行日志:

2023-12-19 11:52:28.767 1 已放入数据,putIndex = 0
2023-12-19 11:52:28.871 2无数据可取,outIndex = -1

这是放入第二帧之后,时间距离已经过去了100ms, 只取到了一帧配置帧(两个字节)和一帧音频帧,100ms的时间不够编码两帧pcm吗?应该是够的,但是实际我们只拿到了一帧编码后的数据。

再看下一段日志:

2023-12-19 11:52:28.804 2 已放入数据,putIndex = 1
2023-12-19 11:52:28.871 2无数据可取,outIndex = -1
2023-12-19 11:52:28.876 3 已放入数据,putIndex = 2
2023-12-19 11:52:28.878 3准备取数据
2023-12-19 11:52:28.910 3无数据可取,outIndex = -1

这里第2帧放入数据到放入第三帧且准备取之前编好的第2帧时,发现依然是无数据可取,此时已经过了100ms了,这说明设置等30ms似乎也没什么用啊,也不能实现放入一帧就取出这一帧的编码结果。

需要注意的是,在个日志我是在海能达的执法仪设备上测试的,没在别的手机上测试过,现在也比较忙,没时间测试这么多了,就先这样吧!如果按照这个日志结果的话,说明 timeoutUs 参数设置大了也没什么用,反而会更浪费时间

MediaCodec的队列有多大

测试队列话的看我们看它的索引即可,在上面的示例代码中,我们可以修改为只往MediaCodec中放数据,但是不取,这样就能看到它存数据的队列有多大了,如下:

/** 把PCM格式的音频数据编码为AAC数据 */
fun encodePCMToAAC(pcmByteBuffer: ByteBuffer) {
    // 1、把pcm数据放入编码器中进行编码
    putDataToEncoder(pcmByteBuffer)
    // 2、从编码器中取出编码后的数据
    //getDataFromEncoder()
}

private var start = 0L

/** 把pcm数据放入编码器中进行编码 */
private fun putDataToEncoder(pcmByteBuffer: ByteBuffer) {
    val flags = if (pcmByteBuffer.capacity() == 0) MediaCodec.BUFFER_FLAG_END_OF_STREAM else 0
    inputBufferIndex = mMediaCodec.dequeueInputBuffer(0L)
    Timber.i("putIndex = $inputBufferIndex,距离上一帧时间:${System.currentTimeMillis() - start}")
    start = System.currentTimeMillis()

    if (inputBufferIndex >= 0) {
        inputBuffer = mMediaCodec.getInputBuffer(inputBufferIndex)!!
        inputBuffer.put(pcmByteBuffer)
        mMediaCodec.queueInputBuffer(inputBufferIndex, 0, pcmByteBuffer.limit(), System.nanoTime() / 1000, flags) // 这个函数调用时间:1~ 5毫秒
    }

}

运行结果如下:

2023-12-19 17:46:29.093 putIndex = 0,距离上一帧时间:1702979189093
2023-12-19 17:46:29.133 putIndex = 1,距离上一帧时间:40
2023-12-19 17:46:29.173 putIndex = 2,距离上一帧时间:39
2023-12-19 17:46:29.214 putIndex = 3,距离上一帧时间:40
2023-12-19 17:46:29.253 putIndex = 0,距离上一帧时间:39
2023-12-19 17:46:29.294 putIndex = 1,距离上一帧时间:39
2023-12-19 17:46:29.336 putIndex = 2,距离上一帧时间:41
2023-12-19 17:46:29.374 putIndex = 3,距离上一帧时间:37
2023-12-19 17:46:29.415 putIndex = -1,距离上一帧时间:40
2023-12-19 17:46:29.457 putIndex = -1,距离上一帧时间:40
2023-12-19 17:46:29.494 putIndex = -1,距离上一帧时间:37
2023-12-19 17:46:29.535 putIndex = -1,距离上一帧时间:39

从结果可以看到,MediaCodec的放pcm的列队大小为4,因为它的有效索引是 0 ~ 3,为什么这里 0 ~ 3出现了两次,因为它把pcm编码好之后会放到输出的队列中,从这里也可以看出输出的队列大小也是4,因为第一次使用 0 ~ 3 的输入队列装了4帧pcm数据,都编码为aac后会放到输出队列中,因为输出队列大小 也为4,所以此时输出队列就被用完了,输入的4帧因为编码完成并把结果放到了输出队列,所以输入队列就可以空出来了,所以我们就看到了第二次的 0 ~ 3的 putIndex被使用,等这4个也被用完之后就真的没有可用的了,所以后面看到拿到的 putIndex都是-1,-1对应MediaCodec.INFO_TRY_AGAIN_LATER常量,意思为稍后再试,其实它就是等我们去取输出的队列中的数据,取了之后则输入的就有可用的了。当然了,这里测试的队列大小为4我只是在海能达的设备上测试的,不知道别的Android设备是不是都是这个大小,有可能不同的设备它的队列大小不一样。

从这个日志也能看到,AudioRecord是差不多每40ms给我们一次音频数据,当然这多久给一次音频数据也要看设置录音参数是多少而变的,比如采样率、从AudioRecord中取数据时指定的最大大小参数,比如采样频率为44100,位深为16位,则音频采样1秒钟需要的存储空间为:44100 x (16 / 8) = 88200字节,假设从AudioRecord中取数据时指定每次最大取3584字节,则取完一秒的音频需要的次数为:88200 / 3584 = 24.61次,这是在1秒钟内完成的,则每次的间隔为:1000ms / 24.61 = 40.63 ms。

MediaCodec编码PCM为AAC时得到的配置帧是什么东西?

这个配置信息称为:audio specific configuration,具体看百度结果,或者这两篇文章:https://www.jianshu.com/p/1a6f195863c7https://blog.csdn.net/free555/article/details/98469653

如果是编码yuv为H264,则得到的配置数据为sps + pps,具体可百度。

MediaCodec 编码时间计算

override fun run() {
    try {
        while (needRun) {
            mYUVDatasQueue.poll(30L, TimeUnit.MILLISECONDS)?.let {
                if (needRun) {
                    val start = System.currentTimeMillis()
                    mH264Encoder.encodeYuvToH264(it)
                    Timber.i("编码一帧时间:${System.currentTimeMillis() - start}")
                }
            }
        }
    } catch (e: Exception) {
        Timber.fe(e,"把YUV编码为H264时出现异常")
    }
}

如上代码,我想获取编码YUV为H264的时间,这样测其实是不准的,因为编码器是运行在另一个线程上的,所以我们无法去计算编码器编码一帧视频的时间。即使是这样,我发现运行日志有时候也时间也很长,日志如下:

编码一帧时间:13
编码一帧时间:10
编码一帧时间:11
编码一帧时间:7
编码一帧时间:35
编码一帧时间:9
编码一帧时间:6
编码一帧时间:26
编码一帧时间:8
编码一帧时间:18
编码一帧时间:21
编码一帧时间:53
编码一帧时间:70
编码一帧时间:70
编码一帧时间:8
编码一帧时间:7

即然编码器在另外的线程进行编码,为什么我的这个函数调用有时需要的时间这么长呢,这个有时间需要搞清楚。测试的是1080P的分辨率,timeoutUs参数设置为0。mH264Encoder.encodeYuvToH264(it)函数比较简单,就放入数据和取数据,无非就是把yuv数据拷贝到编码器的put队列,编码结束后再把out队列中的数据拷贝到我们的ByteBuffer中,然后放到其它的队列中交给其它的线程使用(比如网络发送、保存mp4等),理论上来说这个拷贝数据应该是使用很少的时间,为什么会这么久,这真的需要好好测试一下搞明白的!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
为了满足广大Android开发爱好者与从业者的学习需求,我们精心整理并上传了一份全面而实用的Android项目资源包。这份资源包内容丰富,涵盖了从基础知识到实战应用的全方位内容,旨在为开发者们提供一个便捷、高效的学习平台。 一、文件手册 资源包中的文件手册部分,详细记录了Android开发的核心知识点和常用技术。无论是初学者还是有一定经验的开发者,都能从中找到所需的学习资料。手册采用了简洁明了的排版方式,使得查阅更加方便快捷。同时,手册内容深入浅出,既适合新手入门,也能为老手提供有价值的参考。 二、项目实战与练习 为了让学习者能够将理论知识与实践相结合,我们特别准备了项目实战与练习部分。这部分内容包含了多个精心设计的Android项目案例,从需求分析、设计思路到实现过程,都有详细的讲解和代码示例。学习者可以通过实际操作,深入了解Android开发的整个流程,提升自己的实战能力。 此外,我们还提供了一系列练习题,旨在巩固所学知识,检验学习成果。这些练习题既有基础题,也有难度较高的挑战题,适合不同层次的学习者进行练习。 三、Android开发工具集 在Android开发过程中,选择合适的工具能够大大提高开发效率。因此,我们整理了常用的Android开发工具集,包括开发工具、测试工具、性能优化工具等。这些工具都是经过我们精心筛选和测试的,能够帮助开发者们更加高效地进行Android开发工作。 总的来说,这份Android项目资源包是一份不可多得的学习资料,无论你是初学者还是有一定经验的开发者,都能从中受益匪浅。我们希望通过这份资源包,为广大Android开发爱好者与从业者提供一个更加便捷、高效的学习平台,共同推动Android开发领域的发展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

android_cai_niao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值