Android音视频开发学习——硬解码

本篇文章较长,记录自己学习安卓中音视频相关知识的笔记。

视频

视频由许多个帧构成,一个帧相当于一张图片 。

帧率

是单位时间内的帧数,决定了视频的流畅度。单位是帧/秒或者frames per second(fps),越大视频越自然流畅。

电影帧率 :24,25fps

游戏帧率:30,60 fps

色彩空间

是在某些标准下用通常可接受的方式对彩色加以描述。用于彩色监视器和一大类彩色视频摄像。

RGB

RGB图像具有三个通道R、G、B,分别对应红、绿、蓝三个分量,由三个分量的值决定颜色

YUV

是根据一个亮度(Y分量)和两个色度(UV分量)来定义颜色空间

YUV相比于RGB格式最大的好处是可以做到在保持图像质量降低不明显的前提下,减小文件大小

将亮度参量Y和色度参量U/V分开表示的像素格式,主要用于优化彩色视频信号的传输。YUV像素格式来源于RGB像素格式,通过公式运算,YUV三分量可以还原出RGB,YUV转RGB的公式如下:

  • R = Y + 1.403V
  • G = Y - 0.344U - 0.714V
  • B = Y + 1.770U

在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等等

AAC编码

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

工作流程

编解码器处理输入数据并产生输出数据,MediaCodec 使用输入输出缓存,异步处理数据。简要地说,一般的处理步骤如下

  1. 请求一个空的输入 input buffer
  2. 填入数据、并将其交给 MediaCodec
  3. MediaCodec 处理数据后,将处理后的数据放在一个空的 output buffer
  4. 获取填充数据了的 output buffer,得到其中的数据,然后将其返还给 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

MediaExtractor

音视频数据提取器,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(): 读取结束后释放资源

解码流程

  1. 初识化,启动解码器

    根据MediaFormat中的编码类型(如video/avc,即H264;audio/mp4a-latm,即AAC)创建MediaCodec

    //通过Extractor获取到音视频数据的编码信息MediaFormat
    val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
    //调用createDecoderByType创建解码器。
    mCodec = MediaCodec.createDecoderByType(type)
    
    
  2. 将数据压入解码器输入缓冲

    如果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)
            }
        }
    
    
  3. 将解码好的数据从缓冲区拉取出来
            // 查询是否有解码完成的数据,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
                }
            }
          
    
    
    
  4. 渲染
    视频渲染

    ​ 视频的渲染并不需要客户端手动去渲染,只需在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_STATICMODE_STREAM两种分类。STREAM的意思是由用户在应用程序通过write方式把数据一次一次得写到audiotrack中。这个和我们在socket中发送数据一样,应用层从某个地方获取数据,例如通过编解码得到PCM数据,然后writeaudiotrack。这种方式的坏处就是总是在JAVA层和Native层交互,效率损失较大。

    STATIC的意思是一开始创建的时候,就把音频数据放到一个固定的buffer,然后直接传给audiotrack,后续就不用一次次得write了。AudioTrack会自己播放这个buffer中的数据。这种方法对于铃声等内存占用较小,延时要求较高的声音来说很适用

  5. 释放输出缓冲
    mCodec!!.releaseOutputBuffer(index, true)
    
  6. 判断解码是否完成
      if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
        }
    
  7. 释放解码器

    释放掉所有的资源。至此,一次解码结束。

     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有助于音视频分装到指定格式。当前,MediaMuxer支持MP4,Webm和3GP文件作为输出。自从Android Nougat开始,它还支持在MP4中混合B帧。

  • 首先通过new MediaMuxer(String path, int format)指定视频文件输出路径和文件格式
muxer = new MediaMuxer(outputFile.getAbsolutePath(),
                       MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
  • 接着,就是addTrack(MediaFormat format),添加媒体通道,该函数需要传入MediaFormat对象,通常从MediaExtractor或者MediaCodec中获取。
audioTrackIndex = muxer.addTrack(audioEncoder.getOutputFormat());
videoTrackIndex = muxer.addTrack(videoEncoder.getOutputFormat());
  • 添加完所有track后调用start方法,开始音视频合成
muxer.start();

开始之后,就可以调用MediaMuxer.writeSampleData()向mp4文件中写入数据了。需要注意每次只能添加一帧视频数据或者单个Sample的音频数据,并且BufferInfo对象的值一定要设置正确

muxer.writeSampleData(audioTrackIndex, encodedData, bufferInfo);
muxer.writeSampleData(videoTrackIndex, encodedData, bufferInfo);
  • 合成结束之后关闭以及释放资源
muxer.stop();
muxer.release();

学习文章

官方文档developer

Android 音视频开发打怪升级:音视频硬解码篇

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值