Android App项目中音视频开发杂谈

2020-04-15  本文已影响0人  耗子_wo

既然是杂谈,那么这一篇想必是阅读起来轻松的,因为不会有很多的代码片段,按照常规我们会分别写两篇,一篇Android一篇IOS的,今天我们来谈谈如果是Android新手上手App音视频开发的学习步骤路线应该是什么样的;最后我们介绍下Android项目中音视频实际开发会遇到的一些事情以及解决方案,我们今天只谈思路涉及具体细节可能在接下来的文章里面会具体体现,好的让我们开始吧:

首先我认为音视频作为一个比较垂直的行业,里面的技术背景肯定也是垂直的,这就导致了如果是做通用型App(比如说互联网里面的电商,O2O等)的工程师一开始上手的时候无从下手,找不到方向,那么我们从一些基础概念入手今天给大家掰开了,抡圆了说,我们说的音视频或者说流媒体一般包含以下几个知识点:

首先需要了解的是音视频处理的流程:

image.png

这是一个比较完整的过程,一般来说我们做播放器的时候处理媒体文件(例如Mp4)会完整的经历过这个过程,如果是自定义的流媒体数据可能没有上面的 解协议,解封装 步骤

其次是了解音频PCM数据格式:

再次了解视频YUV数据:

既然是音视频肯定要涉及压缩编码,那么首先应该要了解编码标准:

image.png image.png

说了编码当然要有解码:

解码以后怎么播放,音频播放:

解码后视频播放:

其中OpenGLES 特别是可以作为一个分支来进行加强:

什么是封包:

音视频流媒体在网络上怎么传输:

音视频应用层框架有哪些:

额外需要掌握哪些技能:

以上是我认为作为音视频工程师入门应该掌握的知识点,我觉得掌握了这些不敢说成为了一个高手,但应该是成为一个合格的音视频工程师的 基本功

PS:基本功重要吗?我认为非常重要,往小了说基本功显示了一个人的技能扎实,拥有了扎实的基础才能往更深的方向发展;往大了说基本功显示了一个人可靠,处事沉稳可以做到了解一个事物的本质能做到万变不离其中

有了这些基本功那么我们可以接触一些实际的案例了,如果你想要更进阶那么我推荐一本我认为音视频内容比较全,而且里面有很多实战例子作为参考的书:

image.png

这本书我认为有几点比较好的:

说了这么多好的再说说这本书的一些不好的地方:

但是瑕不掩瑜如果你是有基础的话,那么这本书肯定能给你带了项目中的帮助。

有了前两步骤作为基础的话,那么我们接下来就要实战着手下项目,以及聊一聊项目中实现音视频以及相关的功能想要用到哪些技术方案:

先介绍下我们是做智能硬件,当然少不了App以及硬件,今天介绍的主要是我们的智能产品在音视频开发中的解决方案以及技术选型,由于今天是技术选型我们不会涉及具体的实现细节,因为技术选项定下来以后细节实现网上有很多文章,或者接下来我再分开把一些细节给大家写出来,欢迎大家给我留言,让我们开始吧:

产品实现的功能:

App音视频的数据怎么传输:

typedef struct
{
    HLE_U8 sync_code[3];    /*帧头同步码,固定为0x00,0x00,0x01*/
    HLE_U8 type;            /*帧类型, */
    HLE_U8 enc_std;         //编码标准,0:H.264 ; 1:H.265
    HLE_U8 framerate;       //帧率(仅I帧有效)
    HLE_U16 reserved;       //保留位
    HLE_U16 pic_width;      //图片宽(仅I帧有效)
    HLE_U16 pic_height;     //图片高(仅I帧有效)
    HLE_SYS_TIME rtc_time;  //当前帧时间戳,精确到秒,非关键帧时间戳需根据帧率来计算(仅I帧有效)8字节
    HLE_U32 length;         //帧数据长度
    HLE_U64 pts_msec;       //毫秒级时间戳,一直累加,溢出后自动回绕
} P2P_FRAME_HDR; //32字节

App实现实时音视频解码:

硬件码优势:更加省电,适合长时间的移动端视频播放器和直播,手机电池有限的情况下,使用硬件解码会更加好。减少CPU的占用,可以把CUP让给别的线程使用,有利于手机的流畅度。

软解码优势:具有更好的适应性,软件解码主要是会占用CUP的运行,软解不考虑社备的硬件解码支持情况,有CPU就可以使用了,但是占用了更多的CUP那就意味着很耗费性能,很耗电,在设备电量充足的情况下,或者设备硬件解码支持不足的情况下使用软件解码更加好!

Android 音频硬解码的话那么当时是首先使用 MediaCodec 来实现,首先初始化MediaFormat 做一些解码前的配置,里面包含了解析SPS,PPS的参数,然后想 MediaCodec 填充 MediaFormat 以及数据Buffer等待解码,

int i = mMC.dequeueInputBuffer(BUFFER_TIMEOUT);

然后等待解码输出到Buffer即可:

int i = mMC.dequeueOutputBuffer(mBI, BUFFER_TIMEOUT);

很简单,只要处理好dequeueInputBuffer,dequeueOutputBuffer的顺序以及Buffer变量的数据就可以实现这个功能了,如果你的数据源是MP4文件那么只需要通过 MediaExtractor 来获取音频/视频的轨道,单独来进行解码即可

Android音视频的软解码:
软解码首推的就是ffmpeg,ffmpeg的使用还是很简单的,简单的来说你只需要一开始初始化 编解码格式对象 AVCodecContext 与编解码器 AVCodec ,然后把数据填充
AvPacket ,然后解码成 AvFrame 就可以了

值得一说的是:ffmpeg自3.1版本加入了android mediacodec硬解支持,
使用方法:

--enable-jni
--enable-mediacodec
--enable-decoder=h264_mediacodec
--enable-hwaccel=h264_mediacodec(不知道有什么用,还是开了)

App 音视软解的播放:

int len = swr_convert(actx,outArr,frame->nb_samples,(const uint8_t **)frame->data,frame->nb_samples);

主要是通过 swr_convert 来进行转换

/** Convert audio.
 *
 * in and in_count can be set to 0 to flush the last few samples out at the
 * end.
 *
 * If more input is provided than output space, then the input will be buffered.
 * You can avoid this buffering by using swr_get_out_samples() to retrieve an
 * upper bound on the required number of output samples for the given number of
 * input samples. Conversion will run directly without copying whenever possible.
 *
 * @param s         allocated Swr context, with parameters set
 * @param out       output buffers, only the first one need be set in case of packed audio
 * @param out_count amount of space available for output in samples per channel
 * @param in        input buffers, only the first one need to be set in case of packed audio
 * @param in_count  number of input samples available in one channel
 *
 * @return number of samples output per channel, negative value on error
 */
int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,
                                const uint8_t **in , int in_count);

out表示的是输出buffer的指针;
out_count表示的是输出的样本大小;
in表示的输入buffer的指针;
in_count表示的是输入样品的大小;

转换成功后输出的音频数据再拿来播放就可以在指定的条件进行指定的播放

    //设置回调函数,播放队列空调用
    (*pcmQue)->RegisterCallback(pcmQue,PcmCall,this);
    //设置为播放状态
    (*iplayer)->SetPlayState(iplayer,SL_PLAYSTATE_PLAYING);
    //启动队列回调
    (*pcmQue)->Enqueue(pcmQue,"",1);
this.audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 44100,
                AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT,
                audioData.length, AudioTrack.MODE_STATIC);
                this.audioTrack.write(audioData, 0, audioData.length);
        audioTrack.play();

App 视频的播放:

        sh.GetTexture(0,width,height,data[0]);  // Y
        if(type == XTEXTURE_YUV420P)
        {
            sh.GetTexture(1,width/2,height/2,data[1]);  // U
            sh.GetTexture(2,width/2,height/2,data[2]);  // V
        }
        else
        {
            sh.GetTexture(1,width/2,height/2,data[1], true);  // UV
        }
        sh.Draw();
mDecoder.configure(sps_nal, pps_nal,surfaceViewDecode.getHolder().getSurface());

然后再配置解码器的时候把这个配置进去,接完码以后他就会把解码数据填充到 Surface 来进行播放显示

mMC.configure(mMF, surface, null, 0);

App实现截图拍照:

    /**
     * 把一帧yuv数据保存为bitmap
     * @param yuv 数据流
     * @param mWidth 图片的宽
     * @param mHeight 图片的高
     * @return bitmap 对象
     *
     */
    public Bitmap saveYUV2Bitmap(byte[] yuv, int mWidth, int mHeight) {
        YuvImage image = new YuvImage(yuv, ImageFormat.NV21, mWidth, mHeight, null);
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        image.compressToJpeg(new Rect(0, 0, mWidth, mHeight), 100, stream);
        Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
        try {
            stream.flush();
            stream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return bmp;
    }

也可以把YUV数据按照公式转换为RGB,然后利用RGB再生成你想要的东西

App实现录制视频:
录制视频说白了就是封包,把编码过的音频AAC,视频H.264封装为一个数据格式,常见的格式Mp4,TS等等

avformat_open_input():打开输入文件。
avcodec_copy_context():赋值AVCodecContext的参数。
avformat_alloc_output_context2():初始化输出文件。
avio_open():打开输出文件。
avformat_write_header():写入文件头。
av_compare_ts():比较时间戳,决定写入视频还是写入音频。这个函数相对要少见一些。
av_read_frame():从输入文件读取一个AVPacket。
av_interleaved_write_frame():写入一个AVPacket到输出文件。
av_write_trailer():写入文件尾。

mVideoTrackIndex = mMediaMuxer.addTrack(format);

然后构造Buffer数据,以及做好音频数据以及视频数据的先后顺序,分别写入 MediaMuxer

mMediaMuxer.writeSampleData(mVideoTrackIndex, buffer, info);
(这种情况我用得比较少,如果有疑问大家可以给我留言)

App实现音视频同步:

将视频同步到音频上:就是以音频的播放速度为基准来同步视频。
将音频同步到视频上:就是以视频的播放速度为基准来同步音频。
将视频和音频同步外部的时钟上:选择一个外部时钟为基准,视频和音频的播放速度都以该时钟为标准。

这三种是最基本的策略,考虑到人对声音的敏感度要强于视频,频繁调节音频会带来较差的观感体验,且音频的播放时钟为线性增长,所以一般会以音频时钟为参考时钟,视频同步到音频上,音频作为主导视频作为次要,用视频流来同步音频流,由于不论是哪一个平台播放音频的引擎,都可以保证播放音频的时间长度与实际这段音频所代表的时间长度是一致的,所以我们可以依赖于音频的顺序播放为我们提供的时间戳,当客户端代码请求发送视频帧的时候,会先计算出当前视频队列头部的视频帧元素的时间戳与当前音频播放帧的时间戳的差值。如果在阈值范围内,就可以渲染这一帧视频帧;如果不在阈值范围内,则要进行对齐操作。具体的对齐操作方法就是:如果当前队列头部的视频帧的时间戳小于当前播放音频帧的时间戳,那么就进行跳帧操作(具体的跳帧操作可以是加快速度播放的实现,也可以是丢弃一部分视频帧的实现 );如果大于当前播放音频帧的时间戳,那么就进行等待(重复渲染上一帧或者不进行渲染)的操作。其优点是音频可以连续地播放,缺点是视频画面有可能会有跳帧的操作,但是对于视频画面的丢帧和跳帧,用户的眼睛是不太容易分辨得出来的

一般来说视频丢帧是我们常见的处理视频慢于音频的方式,可以先计算出需要加快多少时间,然后根据一个GOP算出每一帧的时间是多少,可以得出需要丢多少帧,然后丢帧的时候要注意的是必须要判断,不能把I帧丢了,否则接下来的P帧就根本用不了,而应该丢的是P帧,也就是一个GOP的后半部分,最合适的情况就是丢一整个GOP,如果是丢GOP后半部分的话你需要一开始播放GOP的时候弄一个变量记录当前是第几个P帧了,然后计算出需要丢几个P帧才能和音频同步,然后到了那一个需要丢的帧到来的时候直接抛弃,即到下一个I帧到来的时候才进行渲染(这里面有可能丢的不是那么准确,可能需要经过几个的丢帧步骤才能准确同步)

好了,我们Android音视频开发杂谈就介绍到这了,如果大家喜欢欢迎留言讨论,如果需求强烈的话我会再把一些细节的部分整理写出来,😁下一篇我们会接着侃侃IOS平台的音视频内容,想看IOS的出门右转即可

《IOS App项目中音视频开发杂谈》

···

上一篇下一篇

猜你喜欢

热点阅读