KSPlayer 是一款基于 AVPlayer、FFmpeg 的媒体资源播放器框架。支持RTMP、RTSP 等直播流;同时支持 iOS、macOS、tvOS 三个平台。扩张性强,支持自定义播放器内核和UI界面 。本文将采用图解+说明的方式把关键模块的实现原理介绍给大家。
上图展示了 KSPlayer 的主要组件,一共分为四层。每一个层都是解耦的,都可以单独拿出来使用。下面简单介绍图中各组件的分工
PlayerView 是播放器UI控件。目前有VideoPlayerView,AudioPlayerView两个子类。
VideoPlayerView根据平台特色又有IOSVideoPlayerView,MacVideoPlayerView这两个子类
KSPlayerLayer 是播放内核的封装,主要工作是根据配置参数切换播放器内核,管理播放状态,
MediaPlayerProtocol是播放器内核接口。只要遵守MediaPlayerProtocol协议的播放器内核就可以在KSPlayer使用。默认提供了两种播放器内核:KSAVPlayer、KSMEPlayer
1、KSAVPlayer 是基于 AVPlayer 封装而成,支持H.264、H.265、MPEG-4格式。
2、KSMEPlayer是自研播放器内核,支持所有的主流视频格式。支持硬解和软解。
了解了各组件的功能,重新梳理一下完整的播放过程
- PlayerView 收到播放请求。
- 由 KSPlayerLayer 根据配置参数分发给 KSAVPlayer 或 KSMEPlayer 进行播放。
- 如果使用 KSAVPlayer 播放,将视频画面输出给 KSAVPlayerView 中的 AVPlayerLayer 。
- 如果使用 KSMEPlayer 播放,将视频画面输出给 MetalPlayView,音频输出至 AudioEnginePlayer。
通过抽象的 MediaPlayerProtocol 将真正负责播放的 KSAVPlayer 、 KSMEPlayer 屏蔽起来,这样可以保证无论资源是何种类型,对外仅暴露一套统一的接口和回调,将播放内核间的差异内部消化,尽可能降低使用成本。如果需要接入别的的播放器内核的话,如ijkplayer。那只要为ijkplayer实现MediaPlayerProtocol接口即可。
上图展示了 KSMEPlayer 的主要组件,下面简单介绍图中各组件的分工
视频控制的上下文,负责创建视频流,读取数据包。
解码处理,负责把数据包解码成数据帧。支持硬解,软解。使用VideoToolbox进行视频硬解,FFmpeg进行视频软硬解、音频软解、字幕软解
AudioEnginePlayer 负责声音的播放和音频事件的处理。内部使用 AVAudioEngine 做了一层混音,通过混音可以设置声音的输出音量大小和播放倍数
MetalPlayView是视频画面绘制实现类,里面会更加参数参数来决定是使用AVSampleBufferDisplayLayer还是Metal来进行绘制。Metal支持全景视频,AVSampleBufferDisplayLayer支持HDR。
上图展示了 KSMEPlayer 的协作流程图,下面简单介绍图中各组件
KSMEPlayer 中共有5个线程。与图中5个蓝色圆圈对应。
- 数据读取 - Read Packet Loop
- 视频解码 - Video Decode Loop
- 音频解码 - Audio Decode Loop
- 视频绘制 - Video Display Loop
- 音频播放 - Audio Playback Loop
这五个线程采用生产者-消费者模式。通过ObjectQueue中的数据个数来作为线程的控制条件
解码处理,目前一共有两个实现类
类名 | 同步/异步 | 备注 |
---|---|---|
SyncPlayerItemTrack | 同步 | 主要用于字幕,如果是纯音频的话,也是可以用同步 |
AsyncPlayerItemTrack | 异步 |
1、视频、音频采用异步的解码方式。是因为视频、音频解码的时间比较久,除了这个还有更重要的原因:视频、音频解码后的数据帧比数据包大了好几倍,为了节约内存,要控制数据帧的大小。
2、异步解码过程:解码器收到数据包存入数据包队列,当独立的解码线程取出数据包并完成解码后,再存入数据帧队列
3、同步解码过程:解码器收到数据包后立即解码,并存入数据帧队列。
4、优先使用视频硬解,当视频无法软解或硬解失败的话,就自动切换到软解
解码器接口,目前一共有三个实现类
类名 | 解码类型 | 同步/异步 | 备注 |
---|---|---|---|
FFmpegDecode | 音视频 | 同步 | 软硬解 |
VideoToolboxDecode | 视频 | 异步 | 硬解 |
SubtitleDecode | 字幕 | 同步 | 软解 |
CircularBuffer是环形队列
数据队列提供push
、pop(wait, where)
search(where)
三个方法
操作 | 行为 |
---|---|
put |
队列满了,有两个处理方式:1.没有现在队列长度,那会双倍扩展队列长度,如果限制了,那会阻塞当前线程,直到队列只剩下1/2的数据,线程才会通过NSCondition 被唤醒。避免频繁的进行锁操作 |
pop(wait, where) |
如果参数wait为true,那当队列中没有数据时,会阻塞当前线程,直到向队列中添加新元素时,线程才会通过NSCondition 被唤醒,如果wait为false,那就会直接返回空 |
search(where) |
只是访问队列里面的数据,不会对队列的游标产生影响 ,一般是用于文字字幕 |
CircularBuffer还支持排序,因为视频有可能不是按顺序解码。所以一定要排序下,不然画面会来回抖动
常用的同步当时有3种
- 音频时钟
- 视频时钟
- 自制时钟
在 KSMEPlayer 中,优先使用音频时钟,当视频中没有音轨时,或是音轨数据都播放完了,会使用视频时钟进行同步。音视频同步的接口是OutputRenderSourceDelegate, 具体实现类是MEPlayerItem。
了解了各组件的功能,重新梳理一下整个流程
- 数据读取线程读取到数据包,根据数据包类型分发给音频解码器、视频解码器、字幕解码器。
- 如果是字幕包,字幕解码器收到字幕包的同时进行解码,并将解码后的字幕帧存入字幕帧队列。
- 音视频解码器收到音视频包存入音视频包队列,当独立的解码线程取出音视频包并完成解码后,再存入音视频帧队列
- 音频播放线程循环从音频帧队列中取出音频帧并播放。
- 视频展示线程循环从视频帧队列中取出视频帧并绘制。
- 字幕控件根据时间戳到字幕帧队列查找对应的字幕帧并展示,不会把字幕帧从字幕队列删除。
关于 KSPlayer 的原理就阐述到这里,由于本文以理论为主,所以并没有贴代码。感兴趣的同学可以在 GitHub 上找到全部的代码实现。希望对大家能有所帮助。