探索移动端音视频与GSYVideoPlayer之旅 | Agora Talk

大家好,我是 Github 开源 GSY 系列项目的负责人郭树煜,比如 Android 上的 GSYVideoPlayer 就是这个系列里的项目,另外我也是 《Flutter 开发实战详解》 的作者,平时主要活跃在 Github 和掘金平台,这次主要是给大家分享移动端音视频开发和 GSYVideoPlayer 的一些有趣的内容。

基础知识

首先是基础知识,本次分享在这一块会占据很大比例,为什么要和大家聊音视频的基础知识?这就又要考古我很久前的一个经典 issue ,如图所示:

在维护 GSYVideoPlayer 这 5 年多的时间里,关于类似的基础的问题其实收到不少,只是这个比较典型,而处理这些问题的流程都是类似,举个例子,我收到最多的视频播放问题应该是

“播放黑屏,播放失败,xxx能播为什么GSY不能播?”

每次遇到这种问题的时候,我都会问:

“视频编码是什么?”

而很大概率我收到的回复就是 :

MP4

之后就需要进一步去解释: “MP4 不是视频编码,要如何去查看视频编码...” 这样的一个流程。

所以关于音视频的基础常识,也是我最经常科普的内容,这部分内容其实也很多,这里就挑选一部分常见或者我比较经常解释的给大家分享。

封装

如图所示,一般情况下,视频流从加载到准备播放是需要经过解协议、解封装、解编码等这样的过程,其中:

  • 协议指的就是流媒体协议;
  • 封装是的是视频的封装格式;
  • 编码又分为视频编码和音频编码;

常见的协议一般有 HTTP 、RTSP、RTMP 等,其中最常见的就是 HTTP 网络协议,而 RTSP 和 RTMP 一般用于直播流或支持带有控制信令的场景,比如远程监控;当然 HTTP 也有可以支持直播的,比如 HLS (也就是我们常见的 m3u8)。

视频封装协议指的是我们常见的 MP4 、AVI 、RMVB 、MKV、FLV 等常见后缀格式,它们所表示的就是多媒体的封装协议,就是在传输过程中把音频和视频打包都一起的封装,所以播放前是需要把这部分内容解开,提取出对应音频编码和视频编码。

所以可以看到,前面我们说过的 MP4 属于封装格式,它的作用主要是把视频编码和音频编码后的文件塞到一起,这是为了方便传输和存储,所以播放前一般需要解开这层封装,从而得到我们需要的音频轨和视频轨。

音频编码

音频编码指的是音频数据的编码方式,常见的如:MP3、 PCM、WAV、AAC、AC-3 等,因为音频的原始数据大小一般不适合直接传入,比如原始大小一般可以按照 采样率 * 声道数 * 样本格式 去计算,假设视频的音频采样率是 44100 、样本格式是 16 bit 、单声道、24 秒,那么它原始音频大小应该是

44100 * 16 * 1 * 24 / 8 ≈ 2MB

而实际将音频信息提取出来的大小大概只有 200 多K,这就是音频编码的作用。

所以一般都会音频传输会采用各种编码格式进行压缩和去冗余,其中比如没什么压缩的 WAV/PCM 编码的音频质量会比较好,但是体积会比较大;MP3 有损压缩能在音频质量还可以的情况下压缩音频的体积;AAC 也是有损压缩,但是又有分有 LC-AAC、HE-AAC等。

举个例子,正常我们耳感受声音的频率范围是 20Hz-20kHz,在 MP3 编码时,其中就是就截掉了大量的冗余信号和无关的信号。

视频编码

视频编码指的就是画面图像的编码压缩方式,一般有 H263、H264、HEVC(H265)、MPEG-2 、MPEG-4 等,其中H264 是目前比较常见的编码方式。

通常情况下我们理解的画面是 RGB 组合出来,而目前视频领域可能更多使用 YUV 格式,其中 Y 表示的是亮度(灰度),而 U 和 V 表示的是色度(饱和度)。

“Y”表示明亮度,也就是灰阶值,“U”和“V”表示的则是色度,描述影像的色彩及饱和度,用于指定像素的颜色。

最常用的表示形式是Y、U、V都使用8个字节来表示;Y的取值范围都是16~235,UV的取值范围都是16~240,这个取值范围是为了防止传输过程中信号变动造成过载等,也就是后来的 YCbCr 。

举个例子, YUV 最常用的采样格式是 4:2:0 格式, YUV420 可以理解对色度以 2:1 的抽样率进行存储。

YUV 420 并不意味着不采样 V 分量,而是 4个Y 分量共享一组 UV,然后亮度透过色度来显示画面,更多 YUV 的这里就不展开讨论,而为什么使用 YUV 其中有一点因素就是为了兼容以前的黑白电视。

为什么不直接用原始 YUV ?这里假设上面的 MOV 视频直接使用 YUV420 的格式,那么一帧的大小就会是:

1080 * 1920 * 1 + 1080 * 1920 * 0.5 = 2.9MB ,这里省去了换算,简单就是 Y = width * height; U = Y / 4;V = Y / 4;

----------------------
|     Y      | U|V |
----------------------
复制代码

可以看到 YUV 420 采样的图像,已经比 RGB 图像节省了一半的空间,但是如果在这个基础上,算上帧率(30)和一个视频的时长(一小时),那一部视频原始大小就会是天文数字,这样的情况明显不符合网络传输,所以才有了视频编码用于压缩图像。

有了视频编码的存在,视频编码的作用就是对传输图片进行压缩,从而达到尽量还原画面的同时,得到更小的体积。

接下来就介绍最常见的视频编码的基础概念:

  • 1、IPB 帧是一种常见的帧压缩方法,其中 I 帧属于关键帧是每个画面的参考帧; P 帧是前向预测帧;B 帧是双向预测帧。简单来说就是 I 帧自己就可以得到一个完整画面,而 P 帧需要前面的 I 帧或者 P 帧来帮助解码得到一个完整画面,而 B 帧则需要前面的 I/P 帧或者后面的 P 帧来协助形成一个画面。

所以 I 帧是很关键的存在,压缩 I 帧就可以很容易压制掉空间的大小,而压缩 P/B 帧可以压缩掉时间上的冗余信息。而一般在视频 seek 的时候,I 帧很关键,如果视频 seek 之后发生往前的跳动,那很可能就是你的视频压缩得太厉害了。

  • 2 还有一个叫 IDR 帧的概念,因为 H264 采用的是多帧预测,导致 I 帧不能作为独立的观察条件,所以多出一个叫 IDR 帧的特殊 I 帧用于参考,IDR 帧最关键的概念就是:在解码器过程中一旦收到 IDR 帧,就会立即清空参考帧缓冲区,并将IDR帧作为被参考帧。

IDR 帧说白了就是立刻刷新,这样如果出现一些异常画面,也不会让不因为连续导致传播,从 IDR 帧开始重新算一个新的序列开始编码,比如 IDR 帧 之后的 BP 帧都不能引用它之前的 I 帧。所以 IDR 帧一定是 I 帧,但是 I 帧不一定是 IDR 帧。

  • 3、在视频解码里还有一个叫 DTS(Decoding Time Stamp) 和 PTS(Presentation Time Stamp)的存在,DTS主要用于视频的解码,PTS主要用于在解码阶段对视频进行同步和输出。

因为视频包里面数据解码不是连续的,而是需要通过解码数据源得到的 DTS,才决定以包应该在什么时候被解码,而得到的PTS 决定了解码后的图片什么时候被绘制。

  • 4、GOP(Group Of Picture)就是两个 I 帧之间的距离,一般 GOP 设置得越大,图像压缩效率越高,但那时需要解码的时间就会越长。 所以如果码率固定而 GOP 值越大,P/B帧 数量会越多。

可以看到,视频编码很大程度都是在去除冗余,也就是单独的数据包是无法自己组成画面的,它需要对应关联的数据辅助才能实现完整的渲染,所以解码的顺序不代表着画面的顺序,是 I 帧就是最关键的参考点。

讲了上面这些常见的基础知识之后,还有两个特殊又常见的基础点想顺便简单地介绍下:HLS 和 RTSP 。

M3U8

一般情况下 HLS 和 M3U8 是指一个东西,HLS(HTTP Live Streaming) 指的是苹果开发的基于 HTTP 协议的流媒体解决方案,它可以在普通的 HTTP 的应用上直接提供点播和直播的能力。

在 HLS 里会将视流文件切分成小片(ts)并建立索引文件(M3U8),常见情况下视频流编码会是 H.264,音频流编码为 AAC。

M3U8 的特殊在于,它是通过先获取索引文件,之后获取每个 ts 文件进行播放,例如你把一个 M3U8 链接放到PC浏览器,它很大程度可能不会支持播放,而是下载下来一个 M3U8 的文件。

虽然大家一直叫它 M3U8,但是其实它的格式是 M3U,然后编码格式采用的是UTF-8,所以合起来是 M3U8,它是一个纯文本的文件,文件里有许多的 tag 组成,常见的有:

  • #EXTM3U: 必备的tag,标识该文件是 M3U;

  • #EXTINF: 每个 ts 媒体文件的持续时间,单位是秒,仅对其后面的 URL 有效;

  • #EXT-X-TARGETDURATION: M3U文件中最长的 ts 时长,单位是秒,上面 #EXTINF 中的时间长度必须小于或是等于这个最大值,只能出现一次;

  • #EXT-X-KEY:表示加解密的作用,后面会详细聊一聊它。

  • #EXT-X-PLAYLIST-TYPE:可选项,如果是 VOD 则服务器不能改变整个 ts 文件列表;如果是 EVENT 则服务器不能改变或是删除 ts 文件列表中的任何部分,但是可以向该文件新增内容;

  • #EXT-X-ENDLIST: 表示到末尾了,它可以在中任意位置出现,但是只能出现一个;

  • #EXT-X-MEDIA: 常用在多语言版本,比如多个不同的音频语言;

  • #EXT-X-STREAM-INF: 用于嵌套 M3U8, 例如提供多个不同码率的 M3U8 播放链接;

  • #EXT-X-MEDIA-SEQUENCE: 唯一的序号,相邻之间往下+1, 如果没有则默认为0;这个后续也会讲到作用;

所以播放 M3U8 格式的视频,首先需要获取到索引文件,然后解析对应的 tag ,然后得到一个 PlayList 去播放,而在维护 GSYVideoPlayer 的这段时间里,接收到关于 M3U8 最多的问题之一其实是:加密。

前面我们介绍过,通过 #EXT-X-KEY 这个 tag ,可以实现 M3U8 的加密播放,一般格式是

#EXT-X-KEY:METHOD=<method> [,URI=<uri>][,IV=<iv>]

在 M3U8 里,加密方式一般是 AES-128 加密,具体到 ExoPlayer 里就是 AES-CBC-PKCS7Padding

为了大家更好地了解这部分加密的了逻辑,就有必要介绍下 AES / CBC / PKCS7Padding 这三个的组成,简单来说,一般加密过程就是:

  • 加密:Padding -> CBC加密 -> Base64编码
  • 解密:Base64解码 -> CBC解密 -> Unpadding

PKCS7Padding

首先是 PKCS7Padding,因为在使用 AES 做加密时,有时候数据可能会不满足一个块,也就是不对齐,而 PKCS7Padding 故名思义就是填充一些字节进入,从而到达每个块都是一样大小的结果。

对于 PKCS7Padding, 如果数据的大小不是指定块大小的整数倍,则添加字节直到它成为整数倍,每个添加进去字节的值是添加的字节总数。

是不是有点绕口,举个例子,如果消息比 16 的整数倍少了 3 个字节,则就会填充三个 0x03 字节添加到块的末尾。

对于 PKCS7Padding,如果数据已经是块大小的整数倍,则将整个 0x15 块(16 个 0x15 字符)添加到消息末尾。这是为了得到校验和,表示在解密期间已到达消息的末尾,并且没有更多数据跟随,我们也可以默认读取最后一位,来得到要剥离的字节数。

除了常见的 PKCS7Padding ,还有一个叫 PKCS5Padding,它的不同之处在于它仅针对使用 64 位(8 字节)块大小的块密码进行了定义,也就是块大小为固定的 8 。

CBC

CBC 就是是先将明文切分成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算后,再与密钥进行加密,也就是下一段内容和上一段有关系,在 M3U8 里也有类似交 IV 的参数。

IV 一般是 0,如果没有IV(Initialization Vector,则使用序列号作为IV进行编解码,将序列号的高位赋到16个字节的buffer中,左边补0。

第一个块使用初始化向量 (IV) 或 16 字节随机值加密,下一个块使用它来启动加密过程。每个后续块都使用来自前一个块的密文以一种称为密码块链接 (CBC) 的方法进行加密。

AES

AES 作为对称加密,它加密和解密用相同的密钥。

另外 AES为分组密码,把明文数据分成一组一组的,每组长度相等,每次加密一组数据,直到加密完整个明文。

AES标准规范中,分组长度只能是128位,也就是说,每个分组为16个字节(每个字节8位),也就是我们上面说的 AES-128,对于密钥的长度可以使用128位、192位或256位,这里面涉及到了矩阵运算,具体就不展开了,我也没太深入研究,记住这个位数 16 个字节。

介绍完加密,我们看 M3U8 格式在 ExoPlayer 里的,如果是遇到了存在加密的,首先需要通过 url 取获取密钥

在这里其实就遇到过一个有趣的问题,因为通常情况会对密钥进行一个 base64 的处理,而之前就遇有用户遇到过,一个应该为 16 个字节的 base64 密钥,在解析后出现:

“java.io.IOException: javax.crypto.IllegalBlockSizeException: error:xxxx Cipher functions:OPENSSL_internal:WRONG_FINAL_BLOCK_LENGTH”

这是因为在 Android 上于默认设置下,Android 的 Base64 包含行终止符,要获得与 Apache 编码相同的结果,就需要使用 Base64.NO_WRAP,因为默认的是 Base64.DEFAULT

publich String encode(byte[] bytes) {
    return Base64.encodeToString(bytes, Base64.NO_WRAP);
}
复制代码

这里为什么特意解释这么多关于 M3U8 的加解密逻辑呢?因为对于 M3U8 而已加密是非常常见,又非常容易出错的地方,介绍这个也是为了大家以后在使用 M3U8 格式播放时,可以更熟练地去寻找问题的解决办法。

RTSP

第二个想额外介绍的就是 RTSP,它也是我收到最多的问题之一, RTSP(Real Time Streaming Protocol)是一种实时流传输协议,当然有时候它也可以用于点播,最多是被用在监控和网络电视上。

如果你去看 RTSP 的概念,就会看到类似这一堆的概念,这里我们不展开解释它的详细内容,主要介绍它们概念是什么:

  • RTSP :它是应用层的一些网络协议,一种实时流传输协议,一般情况下 RTSP 使用 SDP 来传送媒体参数,使用 RTP(RTCP)协议来传输媒体流;另外它是一种双向实时数据传输协议,它允许客户端向服务器端发送请求,如回放、快进、倒退等操作,也就是它除了直播,还可以带上控制的逻辑。

  • SDP : 它是会话描述的格式,它其实并不包含传输协议,只是一种约定格式,里面包含了传输协议(RTP/UDP/IP,H.320等) 和 媒体格式(H.261视频,MPEG视频等),也即会话级别的描述(session level)和媒体级别的描述(media level)。

所以可以看到 RTSP 里会使用到 SDP ,但是它们基本属于应用层面。

  • RTP,(Real-time Transport Protocol),它是一种实时传输协议 ,定义它在传输层其实并不是特别准确,因为它其实依赖于我们常见的 TCP 或者 UDP 协议,默认情况下一般是 UDP ,因为一般情况下,RTP 并不保证传送或防止无序传送,也不确定底层网络的可靠性,例如 RTP 中的序列号允许接收方重组发送方的包序列,同时序列号也能用于决定适当的包位置,例如:在视频解码中,就不需要顺序解码。

  • RTCP (Real-time Transport Control Protocol)实时传输控制协议是实时传输协议(RTP)的一个姐妹协议,RTCP为RTP媒体流提供信道外(out-of-band)控制,RTCP本身并不传输数据,但和RTP一起协作将多媒体数据打包和发送。

一般情况下 RTP 提供实时传输时间信息和流同步,而RTCP保证服务质量。

所以 RTSP 内包含了应用层的 SDP 和传输层的 RTP ,在使用 RTP 时可以使用 UDP 或者 TCP,所以搞清了这些概念,就可以帮助你更好理解 RTSP 的整体概念。

例如在 ijkPlayer 里,如果你接入 rtsp 的话,就可以通过 rtsp_transportrtsp_flags 的配置来配置传输的协议。

另外还有一个叫 RTC 的概念,RTC(Real-time Communications)是实时通信协议,常见的 WebRTC 就是 RTC 的一部分,它和 RTSP 没有关系,一般用于做视频会议,实时通信和直播交互等。RTC 通常是指点对点 ( P2P ) 通信,覆盖了从了采集、编码、前后处理、传输、解码、缓冲、渲染等等的环节。

还有人问 WebSocket 能不能做直播,当然可以,通过 WebSocket 流式传输和共享音频和视频是可以做到的,但是 API 不像 WebRTC 中的对应功能那样强大,简单来说,比如采集,编码,打包,接收,同步这些都需要自己处理。

GSYVideoPlayer

介绍完这么多基础概念,接下来介绍 GSYVideoPlayer ,以 GSYVideoPlayer 给大家介绍下 Android 里音视频开发的一些常识。

其实 GSYVideoPlayer 并不是什么有技术含量的项目,代码量也不大,它更多是对现有资源的一个整合。如图所示,可以看到主要是分:

  • 播放内核层

主要是用于实现真正的播放器内核的,比如常见的 IJKPlayer,MediaPlayer 和 ExoPlayer ,用于集成它们的播放能力,至于它们的区别,我们后续会讲到。

  • Manager 层

主要用于管理和桥接内核层和 UI 层的中间过程,一般情况下不会直接操作内核,而是通过统一的 Manager 层来操作内核,比如释放、快进、暂停等等。

  • UI 层

就是 GSYVideoPlayer 里用户直接使用的 View ,用户在 GSYVideoPlayer 里使用的基本都是 UI 层对象的子类,比如 StandardGSYVideoPlayer ,UI 层主要是连通了 Manager 层和渲染层。

  • 渲染层

渲染层主要用于提供不同的渲染媒介,也就是 TextureViewSurfaceViewGLSurfaceView 等等,用于提供不同的渲染 View, 因为不同的场景或者机器环境,需要不同的渲染效果。

例如如果你需要在画面渲染时,对画面数据做一些定制操作时,你可能就需要 GLSurfaceView ;在某些硬件设备上 SurfaceView 渲染可能会比 TextureView 更清晰;比如涉及透明度操作时选用 TextureView 等等。

  • 缓存层

就是用于视频播放时,针对需要实现视频边播边下载的逻辑。

就是上面所示,GSYVideoPlayer 简单地根据需求对播放器做了一些分层,各个分层通过固定接口拼装在一起,其实这里为什么要实现多种播放内核和多种渲染 surface 的支持呢?

因为一般情况下一种内核很难满足各种各样的播放和渲染常见,千奇百怪的音视频来源下,最好的就是通过多种播放内核来支持,从而达到比较稳定的播放效果。

IJKPlayer、MediaPlayer 和 ExoPlayer

这里面主要需要讲的就是默认内置的三种内核:IJKPlayer、MediaPlayer 和 ExoPlayer 的区别。

IJKPlayer

首先 IJKPlayer 的底层使用的是 FFmpeg ,这说明 IJKPlayer 可以很灵活地配置需要支持的播放格式,因为 FFmpeg 的解码大部分时候都是软解码能力,也就是通过 CPU 来实现解码播放,所以可以很灵活地支持和配置自己需要支持的视频播放格式,当然这个支持是需要自己修改配置和打包。

另外使用了 FFmpeg ,所以 IJKPlayer 默认情况下使用的是软解码,软解码简单的理解就是纯 CPU 的解码能力,所以在面临高码率的视频时,就可能会出现解码能力出现瓶颈的问题,比如花屏,画面和视频不同步等等。

当然 IJKPlayer 也可以开启硬解码能力,硬解码就是配合硬件和GPU增加协助视频的解码能力,但是目前看来 IJKPlayer 的硬解码效果其实并不好。

MediaPlayer & ExoPlayer

MediaPlayer 和 ExoPlayer 的底层其实都是通过 MediaCodec 来实现解码的能力,他们之间的底层区别并不大,一般情况下可以理解为他们使用的都是硬解码的能力。

为什么说一般情况下呢?因为 Android 底层其实是会有一个优先级顺序排列的编解码器列表,如果设备具有用于该格式的解码器,则一般通常会选择硬解码器。

这个优先级顺序是不对上层公开的,Android系统中默认是通过解码器的命名区分,软解码器通常是以 OMX.google 开头,硬解码器通常是以 OMX.[hardware_vendor] 开头,当然有一些厂家可能会不遵守这个命名规范的,不以. OMX.. 开头的,那也会被认为是软解码器。

比如如下代码就可以简单看作为判断是否支持 H265 的硬件解码

   public static boolean isH265HardwareDecoderSupport() {
        MediaCodecList codecList = new MediaCodecList();
        MediaCodecInfo[] codecInfos = codecList.getCodecInfos();
        for (int i = 0; i < codecInfos.length; i++) {
            MediaCodecInfo codecInfo = codecInfos[i];
            if (!codecInfo.isEncoder() && (codecInfo.getName().contains("hevc")
                    && !isSoftwareCodec(codecInfo.getName()))) {
                return true;
            }
        }
        return false;
    }
    public boolean isSoftwareCodec(String codecName) {
        if (codecName.startsWith("OMX.google.")) {
            return true;
        }

        if (codecName.startsWith("OMX.")) {
            return false;
        }

        return true;
    }
复制代码

但是使用了 MediaCodec 也就代表了你没办法干预它的解码支持,所以一般情况下 MediaCodec 的编码能力支持拓展就很弱,特别是针对一些奇奇怪怪的视频编码的情况下。

而这里对于 MediaPlayer 和 ExoPlayer 的最大的区别就是: MediaPlayer 的自定义空间很小,基本上 MediaPlayer 如果出现无法播放,那基本上就是没救了,因为它可以提供的操作空间很少,就比如 https 的证书不校验。

相对的 ExoPlayer 是一个开源项目,它提供可以更灵活的配置和调试,在 ExoPlayer 里最重要的就是 DataSourceMediaSource

官方提供了丰富的 MediaSource 用于处理各种不同协议的数据,比如

  • SsMediaSource
  • RtspMediaSource
  • DashMediaSource
  • HlsMediaSource
  • ProgressiveMediaSource

一般情况下 SsMediaSourceDashMediaSource 可能大家用得比较少,简单介绍下它们对应分别是微软的 Smooth Streaming 和国际标准 MPEG-DASH(Dynamic Adaptive Streaming over HTTP)。

它们其实都类似于苹果的 HLS 方案,都是支持基于 HTTP 的自适应流视频方案。

一般情况下在现在新版的 ExoPlayer 里, ProgressiveMediaSource 会是最常用的,它内部默认的 DefaultExtractorsFactory 会为这个视频寻找到对应的解码格式。

另外除了 RTMP 和 RTSP 之外的协议,一般都需要给他们设置一个 HTTP 的 DataSource DataSource 主要负责根据网络协议下载视频,另外 ExoPlayer 官方的视频缓存 CacheDataSource 也是在这一层实现。

举个例子:

  • 如果你是 Rtmp 的视频,那就是 ProgressiveMediaSource + RtmpDataSource
  • 如果是你 http 的 MP4,那就是 ProgressiveMediaSource + DefaultDataSource
  • 如果你希望在 http 播放时缓存视频,那就是 ProgressiveMediaSource + (CacheDataSource 嵌套 DefaultDataSource
  • 如果你希望介入网络下载过程,比如忽略 SSL 证书,那么你可以定义自己的 DataSource 来实现;
  • 如果你需要预加载的列表播放,可以使用 ConcatenatingMediaSource
  • 如果你想自定义 M3U8 的解密和ts列表解析过程,可以自定义 HlsMediaSource 并实现里面的 HlsPlaylistParserAes128DataSource

以前循环播放还有一个叫 LoopingMediaSource ,但是现在已经弃用,直接改为 setRepeatMode 方法。

可以看到 ExoPlayer 更加灵活和可定制化,通过 MediaSource 的嵌套和定义,组合 DataSource 的嵌套,可以得到更可控和可调试的代码结构,当然对比 MediaPlayer 使用上相对会复杂一些。

另外还有一个消息,就是未来的 ExoPlayer 可能会迁移到 androidx 下的 Media 项目。

常见问题

首先看一张图,这是以前做音视频开发时,遇到某些细节条件时的局部截图,对应分支需要取处理的情况:

事实上音视频开发如果只是到 “能播就行” 的状态并不难,但是如果细节到每个问题上时,就有很多需要处理的情况,所以如果老板对你说“做一个类似爱奇艺的播放页面”时,不妨把这些细节的问题和他讨论一遍,让他感受下做一个播放器是有多不容易。

开始一个音视频相关的项目需要注意什么

因为很多时候开发者可能有个误解,以为 “不就是接个播放器 SDK 放个 Url 的功夫吗?” ,从而给后面的开发留下了太多的坑需要填。

  • 1、首先在做音视频开发时,要确定好自己需要支持的封装协议、视频编码、音频编码格式,因为编码格式千万种,一般情况下你不可能全都支持,所以首先要在需求内确定好需要支持的格式范围。

  • 2、如果存在用户自主上传视频的场景,最好还要在服务端提供转格式与转码率等功能。因为在服务端判断视频格式并转码可以规范编码统一,这能够减少客户端端因为编解码失败无法播放的问题;另外提供同一视频不同码率的链接,可以在不同手机型号和系统上能够拥有更好的播放体验,减少前面说过的因为码率太高出现音视频不同步或者卡顿的问题。

类似功能在阿里云和腾讯云等等流媒体平台都有支持。

通过解决数据源来适配,会比你被动在客户端做各种视频成本来得更低和更可靠。

“为什么我的视频缓冲了,在 seek 之后还需要重新请求?”

这就需要解释缓存和缓冲的区别:

  • 缓冲:就像在倒垃圾的时候,不可能一有垃圾马上跑去垃圾堆倒,而是先把垃圾倒到垃圾桶,垃圾桶满了再一起倒到垃堆。因为缓冲是在内存中,不可在内存中把整个视频都缓冲进去,所以一般情况下你看到的缓冲都是一段一段的临时数据,一个缓冲块是处于不断地加载又不断清除的过程。

  • 缓存: 缓存的解释就简单多了,就是把视频在播放的时候一边下载到本地,这样在缓存区域内的数据就不需要发生二次请求。

另外前面也介绍过 Seek 导致跳动的问题,其实就是关键帧的问题,你 Seek 到一个位置上因为如果没有关键帧,那其实是无法获取到一个完整画面的,所以当视频的关键帧被压缩得太少时,就会导致 Seek 的时候帧跳动。

“为什么我的视频会出现大小和方向不对?”

一般情况下视频信息里是带有旋转角度的,比如 Android 手机上录制的视频就可能带有旋转角度,所以在布局和绘制时需要把旋转角度如: 270,90 这样的角度考虑上。

另外视频在获取大小还会有 Width Height Ratio 的信息,也就是宽高比,例如这个信息在 ijkplayer 上是以 videoSarNum / videoSarDen 得到的,只有把宽高比和视频的宽高一起计算,才能获取到真正的展示宽高。

倍速

前面介绍过,视频在播放时音频编码和视频编码会被单独解码出来,这时候他们在播放时就需要组合起来,就会有一个同步时钟的概念,比如以音频为同步时钟,视频画面跟着同步播放。

倍速播放其中一个关键就是同步时钟,这时候比如同步时钟是音频,但是视频其实是没有音频的无声视频,那设置倍速播放会有无效。

另外如果音频和视频帧交错不合理,比如前面一大半数据都是音频帧,后面一大段才是视频帧,这就会导致预读取的数据会很多,需要反复横跳,导致播放过程中频繁 seek 和卡顿。

在 A 页面播放视频 a,跳到 B 页面播放 b ,回到 A 页面视频 a 不会继续播放

默认情况下在 GSYVideoPlayer 只会有一个活跃内核,这个问题也是被问得到最多的问题之一。

这是因为活跃的播放内核默认只有一个。

当 B 页面的 b 视频开始播放时,A 页面对应的内核就已经被释放了,这是因为如果同时存在多个内核会不方便管理,并且占用过多的内存,所以当 B 页面视频播放时,A 页面的内核就被释放了,如果这时候需要 A 页面回复播放,就是重新设置并调用播放接口,才能恢复播放。

当然如果 A 页面和 B 页面时同一个视频,比如时列表调整到详情页面的场景,就可以在不释放内核的情况下,通过切换渲染层的 surface 来达到无缝切换的效果。

当然针对一定要多个内核的场景,GSYVideoPlayer 里也做了对应的实例,比如:

  • 开启单独的内核用于广告播放,在播放广告时也同步加载视频内容;
  • 多个监控画面的同时查看,支持开启多个内核同时播放;

整体来说 GSYVideoPlayer 并不是一个完美的项目,因为他依旧有很多问题,依旧也有很多需要自己兼容和处理的场景,但是它免费啊,它开源啊~。

おすすめ

転載: juejin.im/post/7057132141875822622
おすすめ