Android音视频开发入坑指南

总结一波 Android 音视频点播开发相关的知识(本文内容不包含直播技术,直播技术涉及到音视频录制,编码,由于不在近期需求中故没有去研究)

视频点播技术整体流程

从一个输入(视频源的url地址)到最终Android设备上视频画面的显示都经历了什么?

  • 获取数据
    当我们将视频源的地址作为输入传给播放器后,后者通过网络请求下载服务器端的视频数据或者通过读取文件从本地获取视频数据,此过程将输入Url转换成了视频流媒体数据。
  • 解流媒体协议
    视频在网络上传输按照对应的协议编码成对应的数据格式,我们将视频数据从网络上获取下来后(不需要全部下载下来,动态传输,实时播放)需要按照协议进行对应的解码过程,即是解流媒体协议(如果是读取本地文件则没有此过程),此过程将视频流媒体格式数据转换成了封装格式数据。
  • 解封装
    我们经常所说的avi/mp4/mpeg都是一种视频封装格式,封装格式就是将音频流压缩编码数据和视频流压缩编码数据进行包装时遵循的协议格式,此过程将视频封装格式数据转换成了对应的音频压缩编码数据和视频压缩编码数据。
  • 音视频解码
    通过解码,将压缩编码的视频数据输出成为非压缩的颜色数据,例如YUV420P,RGB等,将压缩编码的音频数据输出成为非压缩的音频抽样数据,例如PCM数据,常见的视频编码标准如 H.264,MPEG2,VC-1 等等,音频编码标准如 MP3 , AC-3 等等 此过程将压缩的音视频数据解压缩成对应的原始数据
  • 音视频同步
    简单来说就是将视频轨的实时画面与音轨的实时声音同步一致
  • 视频数据的显示和音频数据的播放
    将同步后的视频数据显示在手机屏幕上,将音频数据从听筒播放出来

上面的过程会涉及到一些音视频基本概念,如果你不懂可以点击下面我列出的链接去了解他们,或者通过搜索引擎去了解他们,当然你也可以选择先略过这部分,这部分对于我们实际开发的部分影响不大。

百度百科:流媒体协议
百度百科:视频压缩技术
音视频开发技术基础

应用层音视频开发套路

说到套路是因为我们实际 app音视频点播开发其实是整个流程的最后一步,即是视频播放器UI的开发,而这一步大概做的事是相同的,也是相对简单的,而这一步之前所做的事一般我们借助开源项目或者系统本身的接口去做, 站在巨人的肩膀上操作往往是最轻松以及最无脑的,当然你能力强的话也可以自己徒手撸一个性能更好,兼容性更强的底层库。

基于精力有限和自我水平认知,所以现在我所要做的就是两点

  • 播放器的选择
  • 播放器上的一些操作UI和对应的逻辑的开发

播放器的选择

在选择播放器之前我们需要了解一些知识点

1.硬解和软解

音视频的解码可以使用硬解码和软解码两种,硬解码就是直接调用 GPU 进行解码处理,而软解码是使用 CPU 进行运算。两种解码方式都有各自的优缺点,硬解码的效率更高,对 CPU 的消耗较少,但是兼容性较差,硬解码就会涉及到硬件,所以当机器不同,可能会有的机器能解码,有的会失败。而软解码的兼容性更好,但是效率较低,会加重 CPU 的负载。在 Android 的视频处理中,这两种解码方式分别对应于不同的两种实现。

  • MediaCodec
    从API 16开始,Android 提供了 Mediacodec 类以便开发者更加灵活的处理音视频的编解码。 MediaCodec 就属于硬编解码。 MediaCodec 类可用于访问 Android 底层的媒体编解码器。它是 Android 底层多媒体支持基本架构的一部分(通常与 MediaExtractor, MediaSync, MediaMuxer , MediaCrypto, MediaDrm, Image, Surface, 以及 AudioTrack 一起使用)。
    从广义上讲,一个编解码器通过处理输入数据来产生输出数据。MediaCodec 采用异步方式处理数据,并且使用了一组输入输出缓存(buffer)。在简单的层面,你请求或接收到一个空的输入缓存(buffer),向其中填充满数据并将它传递给编解码器处理。编解码器处理完这些数据并将处理结果输出至一个空的输出缓存(buffer)中。最终,你请求或接收到一个填充了数据的输出缓存(buffer),使用完其中的数据,并将其释放回编解码器。
    优点:功耗低,硬件编解码速度快
    缺点:扩展性不强,不同芯片厂商提供的支持方案不同,导致程序移植性差,实测发现很多手机的YUV码流不一致,还会出现硬编码后偏色的问题
    适用场景:有固定的硬件方案,无需移植(如智能家具产品);需要长时间摄像的场景,如手机摄像头实时取景,这种场景软解码的处理速度跟不上。
  • FFmpeg
    FFmpeg是一套用来记录、转换数字音频、视频,并能将其转化为流的开源程序。FFmpeg 有非常强大的功能,包括视频采集功能、视频格式转换、视频抓图、视频加水印等。FFmpeg在Linux平台下开发,但它也可以在其它操作系统中编译运行,包括 Windows、OS X等。
    FFmpeg 堪称自由软件中最完备的一套多媒体支持库,它几乎实现了所有当下常见的数据封装格式、多媒体传输协议以及音视频编解码器。可以直接通过命令行使用 FFmpeg,它提供了三个命令行工具,ffmpeg 用于转码,ffplay 用于播放,ffprobe 用于查看文件格式。
    实际上,除去部分具备系统软件开发能力的大型公司(Microsoft、Apple等)以及某些著名的音视频技术提供商(Divx、Real等)提供的自有播放器之外,绝大部分第三方开发的播放器都离不开FFmpeg 的支持,像Linux桌面环境中的开源播放器 VLC、MPlayer,Windows下的 KMPlayer、暴风影音以及 Android 下几乎全部第三方播放器都是基于 FFMpeg 的。可以这么说,只要做视频音频开发都离不开 FFmpeg。
    优点:(1)封装了很多的格式,使用起来比较灵活,兼容性好;(2)命令行的方式使用非常方便,比如视频裁剪的步骤:ffmpeg -ss 10 -t 20 -i INPUT -acodec copy -vcodec copy OUTPUT,相对来说比写函数的方式要简单很多;(3)功能十分强大。
    缺点:(1)学习成本有点高(2)软编解码功耗大,占用CPU较多,效率偏低
    适用场景:跨平台使用(如不同芯片厂商的手机);短时间拍摄。
    因此,从整个行业来看,由于 Android 平台的碎片化,以及各个厂商硬件的差异化,直接使用 MediaCodec 进行视频硬编解码的实现会遇到各种兼容性问题,因此大部分都是直接使用 ffmpeg 进行视频处理,或多或少进行一些性能和功能上的优化。

2.格式支持

流媒体协议、音视频封装格式、视频编码格式和音频编码格式的支持:

  • 原生 MediaPlayer
    在 Android 系统中对于视频播放器有原生的实现 MediaPlayer , 以及将 MediaPlayer , SurfaceView 封装在一起的 VideoView, 两者都只是使用硬解播放,基本上只支持本地和 HTTP 协议的视频播放,扩展性都很差,只适合最简单的视频播放需求。
    Google官方文档: 格式支持类型传送门
  • ExoPlayer
    谷歌后来有开源了一个播放器项目, 提供了更好的扩展性和定制能力,并加入了对 DASH 和 HLS 等直播协议的支持,但也只支持硬码,如果项目中只需要支持对H264格式的视频播放,以及流媒体协议比较常规(比如 HTTP,HLS),基于 ExoPlayer 定制也是不错的选择。
  • IjkPlayer
    ijkplayer 是 Bilibili 公司开源的播放器实现,整合了 FFMpeg , ExoPlayer , MediaPlayer 等多种实现(也就是说可以无缝切换上面两种播放器实现),提供了类似于 MediaPlayer的 API,可以实现软硬解码自由切换,自定义 TextureView 实现,同时得益于 FFMpeg 的能力,也能支持多种流媒体协议( RTSP ,RTMP ,HLS 等),多种视频编码格式( h264 , mpeg4 , mjpeg ),具有很高的灵活性,可以定制实现自己特色的播放器(比如支持视频缩放,视频翻转等),相比于前两种格式支持最广,不过官方文档有些简单,吐槽一下。

结果

综合以上两点选用 IjkPlayer 作为我们的底层播放器无疑最好(额外福利可以无缝切换另外两种播放器实现),事实上我反编译了一些app如斗鱼、即刻、开眼都是使用的 Ijkplayer。

播放器UI的开发

视频画面渲染

在 Android 上面显示视频画面需要用到的几个常见类:

SurfaceView/TextureView

SurfaceView, GLSurfaceView, SurfaceTexture以及TextureView的定义和区别传送门

简单来说 SurfaceView 运行在独立的线程中,不在当前视图树中,不能执行一些动画操作。TextureView 在当前视图树中,可以执行动画操作,但需要运行在硬件加速的窗口中,例如:如果需要实现列表播放那么只好使用 TextureView ,如果使用 SurfaceView ,页面会有滑动卡顿的问题,需要视页面需求来选择,一般来说使用 TextureView.

控制UI的开发

事实上 github 上面有一些开发的基于 Ijkplayer 的开源项目,看了一圈后我不打算使用它们,这些项目都不是封装的最佳姿势,它们有一个相同的缺点就是耦合太重,职责不单一,如果需要自定义控制UI 需要阅读大量相关的源码去做出修改,这点我显然是抗拒的,还不如我动手写一个,虽然它们的star有上千,可见平时大家开发都比较忙,伸手党居多。

需求整理

  • 控制操作相关的UI和播放器UI分离,可灵活自定义控制操作UI
  • 暂停/播放
  • 标题/loading图/封面图显示
  • 全屏切换
  • 音量调节
  • 亮度调节
  • 锁屏
  • 播放速率显示
  • 关键帧图像缩略图显示(拖动或滑动进度时)
  • 滑动快进快退及拖动进度条快进或快退
  • 视频源清晰度切换
  • 视频画面显示比例切换
  • 播放器切换
  • 错误重试,整体鲁棒性

实现:

关于第一点和系统自带的 VideoView 实现相似:控制操作UI和播放器UI分离 分别用两个接口定义行为,然后去做相应的实现

public interface IMediaController {

    void setEnabled(boolean enabled);

    void setMediaPlayerView(IMediaPlayerView playerView);

    void show(int timeout);

    void show();

    void hide();

    boolean isShowing();

}

public interface IMediaPlayerView {

    // all possible internal states
    int STATE_ERROR = -1;
    int STATE_IDLE = 300;
    int STATE_PREPARING = 301;
    int STATE_PREPARED = 302;
    int STATE_PLAYING = 303;
    int STATE_PAUSED = 304;
    int STATE_PLAYBACK_COMPLETED = 305;
    @IntDef({STATE_ERROR,
            STATE_IDLE,
            STATE_PREPARING,
            STATE_PREPARED,
            STATE_PLAYING,
            STATE_PAUSED,
            STATE_PLAYBACK_COMPLETED})
    @interface State {}

    int RENDER_SURFACE_VIEW = 1;
    int RENDER_TEXTURE_VIEW = 2;
    @IntDef({RENDER_SURFACE_VIEW,
            RENDER_TEXTURE_VIEW})
    @interface RenderViewType {}

    int PLAYER_IJKEXO_MEDIA_PLAYER = 0;
    int PLAYER_ANDROID_MEDIA_PLAYER = 1;
    int PLAYER_IJK_MEDIA_PLAYER = 2;
    @IntDef({PLAYER_IJKEXO_MEDIA_PLAYER,
            PLAYER_ANDROID_MEDIA_PLAYER,
            PLAYER_IJK_MEDIA_PLAYER})
    @interface PlayerType {}

    void setVideoURI(Uri uri);

    int getDuration();

    int getCurrentPosition();

    int getCurrentState();

    boolean isPlaying();

    int getBufferPercentage();

    boolean canPause();

    boolean canSeekBackward();

    boolean canSeekForward();

    /**
     * Get the audio session id for the player used by this VideoView. This can be used to
     * apply audio effects to the audio track of a video.
     *
     * @return The audio session, or 0 if there was an error.
     */
    int getAudioSessionId();

    View getCoverView();

    void start();

    void pause();

    void seekTo(int pos);

    void switchClipStyle(@IRenderView.ClipStyle int clipStyle);

    void switchRenderView(@RenderViewType int renderViewType);

    void switchPlayer(@PlayerType int mediaPlayerType);

    void setOnPreparedListener(IMediaPlayer.OnPreparedListener l);

    void setOnCompletionListener(IMediaPlayer.OnCompletionListener l);

    void setOnErrorListener(IMediaPlayer.OnErrorListener l);

    void setOnInfoListener(IMediaPlayer.OnInfoListener l);

}  

将一些基本控制操作行为定义在播放器UI接口 IMediaPlayerView 中,因为大多数开发中变化的是控制操作相关的UI,而对应的播放控制逻辑是不会变的,控制器类中只做控制器UI的显示不涉及到具体的控制动作,控制动作交由播放器类实现,需要自定义控制UI时只需要实现控制器接口监听UI操作行为并调用播放器对应的控制行为结果即可。

在实现上 除了封面图和 loading 图我放在播放器类中外 其它的所有控制UI都放在了控制器类中。Ijkplayer 中的接口使用的是类 MediaPlayer 接口,所以使用起来还是比较熟悉的。

各个需求的具体实现就不列出了,主要是一些所涉及到的知识点的细节问题。详细代码传送门

需要注意的是最后一点 错误重试,整体鲁棒性
一些常见的问题(坑)和容错处理,这点我们可以借鉴开源播放器项目,别人躺过的坑,我们就没必要再趟一次了。

最后

本人姿势水平有限,如有错误欢迎指出,谢谢阅读!