IJKPlayer是一款基于ffmpeg/ffplay的开源播放器,可支持rtmp/rtsp/hls等多种媒体协议,支持Android/IOS等移动平台。
一、帧
在进行IJKPlayer的学习之前,我们先来了解下帧的概念。视频压缩中,每帧代表一幅静止的图像。而在实际压缩时,会采取各种算法减少数据的容量,其中IPB就是最常见的。
I帧是关键帧,属于帧内压缩。就是和AVI的压缩是一样的。 P是向前搜索的意思。B是双向搜索。他们都是基于I帧来压缩数据。
I帧表示关键帧,你可以理解为这一帧画面的完整保留;解码时只需要本帧数据就可以完成(因为包含完整画面,记得以前的播放软件暂停的时候的图片是模糊的,这就是非关键帧,但现在的播放软件暂停的时候显示的是关键帧,是一张清晰的图片)。
P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。(也就是差别帧,P帧没有完整画面数据,只有与前一帧的画面差别的数据)
B帧是双向差别帧,也就是B帧记录的是本帧与前后帧的差别(具体比较复杂,有4种情况),换言之,要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码时CPU会比较累~。
从上面的解释看,我们知道I和P的解码算法比较简单,资源占用也比较少,I只要自己完成就行了,P呢,也只需要解码器把前一个画面缓存一下,遇到P时就使用之前缓存的画面就好了,如果视频流只有I和P,解码器可以不管后面的数据,边读边解码,线性前进,观看视频也会很舒服。
二、IJKplayer属性参数设置IJKFFOptions
IJKFFOptions *options = [IJKFFOptions optionsByDefault];
IJKFFOptions的默认值
===== options =====
player-opts : start-on-prepared = 1
player-opts : overlay-format = fcc-i420
player-opts : max-fps = 60
player-opts : framedrop = 0
player-opts : videotoolbox-max-frame-width = 960
player-opts : videotoolbox = 1
player-opts : video-pictq-size = 3
format-opts : ijkinject-opaque = 140449007406288
format-opts : user-agent = ijkplayer
format-opts : auto_convert = 0
format-opts : timeout = 30000000
format-opts : reconnect = 1
format-opts : safe = 0
codec-opts : skip_frame = 0
codec-opts : skip_loop_filter = 0
===================
//1为开启硬件解码,用特定方法把数字编码还原成它所代表的内容或将电脉冲信号转换成它所代表的信息、数据等;
//硬件解码其实就是用GPU的专门模块编码来解(建议使用硬解码)。
//0为软件解码,更稳定,是cpu进行解码(如果使用软解码可能导致cpu占用率高)
[options setPlayerOptionIntValue:1 forKey:@"videotoolbox"];
// 设置音量大小,256为标准音量。(要设置成两倍音量时则输入512,依此类推)
[options setPlayerOptionIntValue:512 forKey:@"vol"];
// 最大fps
[options setPlayerOptionIntValue:30 forKey:@"max-fps"];
// 跳帧开关,如果cpu解码能力不足,可以设置成5,否则
// 会引起音视频不同步,也可以通过设置它来跳帧达到倍速播放
[options setPlayerOptionIntValue:0 forKey:@"framedrop"];
// 指定最大宽度
[options setPlayerOptionIntValue:960 forKey:@"videotoolbox-max-frame-width"];
// 自动转屏开关
[options setFormatOptionIntValue:0 forKey:@"auto_convert"];
// 重连次数
[options setFormatOptionIntValue:1 forKey:@"reconnect"];
// 超时时间,timeout参数只对http设置有效,若果你用rtmp设置timeout,ijkplayer内部会忽略timeout参数。rtmp的timeout参数含义和http的不一样。
[options setFormatOptionIntValue:30 * 1000 * 1000 forKey:@"timeout"];
// 帧速率(fps) (可以改,确认非标准桢率会导致音画不同步,所以只能设定为15或者29.97)帧速率越大,画质越好,但太大了,有些机器版本不支持,反而有些卡。
[options setPlayerOptionIntValue:29.97 forKey:@"r"];
//如果是rtsp协议,可以优先用tcp(默认是用udp)
[options setFormatOptionValue:@"tcp" forKey:@"rtsp_transport"];
//播放前的探测Size,默认是1M, 改小一点会出画面更快
[options setFormatOptionIntValue:1024 * 16 forKey:@"probesize"];
//播放前的探测时间
[options setFormatOptionIntValue:50000 forKey:@"analyzeduration"];
[options setCodecOptionIntValue:IJK_AVDISCARD_DEFAULT forKey:@"skip_loop_filter"];
[options setCodecOptionIntValue:IJK_AVDISCARD_DEFAULT forKey:@"skip_frame"];
if (_isLive) {
// 直播参数
[options setPlayerOptionIntValue:3000 forKey:@"max_cached_duration"]; // 最大缓存大小是3秒,可以依据自己的需求修改
//设置无极限的播放器buffer,这个选项常见于实时流媒体播放场景
[options setPlayerOptionIntValue:1 forKey:@"infbuf"]; // 无限读
//播放器缓冲可以避免因为丢帧引入花屏的,因为丢帧都是丢到I帧之前的P/B帧为止。我之前也写过一个类似的,思路都是一样,但这个代码更精简。
//A:如果你想要实时性,可以去掉缓冲区,一句代码:
//B: 如果你这样试过,发现你的项目中播放频繁卡顿,
//你想留1-2秒缓冲区,让数据更平缓一些,
//那你可以选择保留缓冲区,不设置上面那个就行。
[options setPlayerOptionIntValue:0 forKey:@"packet-buffering"]; // 关闭播放器缓冲
} else {
// 如果不判断是否是关键帧会导致视频画面花屏。但是这样会导致全部清空的可能也会出现花屏
// 所以这里推流端设置好 GOP(画面组,一个GOP就是一组连续的画面。MPEG编码将画面(即帧)分为I、P、B三种) 的大小,如果 max_cached_duration > 2 * GOP,可以尽可能规避全部清空
// 也可以在调用control_queue_duration之前判断新进来的视频pkt是否是关键帧,这样即使全部清空了也不会花屏。
[options setPlayerOptionIntValue:0 forKey:@"max_cached_duration"];
[options setPlayerOptionIntValue:0 forKey:@"infbuf"];
//播放时开启,推流时关闭
[options setPlayerOptionIntValue:1 forKey:@"packet-buffering"];
}
2.1 skip_loop_filter
// 'skip_loop_filter' 的 'skip_frame'编码
typedef enum IJKAVDiscard {
/* We leave some space between them for extensions (drop some
* keyframes for intra-only or drop just some bidir frames). */
IJK_AVDISCARD_NONE =-16, ///< 什么也不丢弃
IJK_AVDISCARD_DEFAULT = 0, ///< 丢弃无用的数据包,像0大小的数据包在avi
IJK_AVDISCARD_NONREF = 8, ///< 抛弃非参考帧(P帧)
IJK_AVDISCARD_BIDIR = 16, ///< 抛弃所有的双向帧
IJK_AVDISCARD_NONKEY = 32, ///< 抛弃除关键帧以外的帧,比如B,P帧
IJK_AVDISCARD_ALL = 48, ///< 抛弃所有的帧
} IJKAVDiscard;
skip_loop_filter和skip_frame的对象要过滤哪些帧类型。
skip_loop_filter这个是解码的一个参数,叫环路滤波,环路滤波主要用于滤除方块效应(块内模糊, 图像信号的低频区是用来反应一个图像的细节程度的,要是去掉低频部分,只用高频来描述图像的轮廓肯定很明显的出现方块效应 。)decode_slice()在解码完一行宏块之后,会调用loop_filter()函数完成环路滤波功能。
设置成48和0,图像清晰度对比,0比48清楚,理解起来就是,
0是开启了环路滤波,过滤的是大部分,
而48基本没启用环路滤波,所以清晰度更低,但是解码性能开销小
skip_loop_filter(环路滤波)简言之:
a:环路滤波器可以保证不同水平的图像质量。
b:环路滤波器更能增加视频流的主客观质量,同时降低解码器的复杂度。
三、IJKFFMoviePlayerController
了解了IJKFFOptions,接下来我们创建IJKFFMoviePlayerController,专门用来直播,传入拉流地址,设置IJKFFOptions。
//创建IJKFFMoviePlayerController
[[IJKFFMoviePlayerController alloc] initWithContentURL:url withOptions:options];
视频内容的拉伸样式
/*
*IJKMPMovieScalingModeNone 不拉伸
*IJKMPMovieScalingModeAspectFit 均匀拉伸直到一个尺寸合适
*IJKMPMovieScalingModeAspectFill 均匀拉伸,直到电影填充可见边界。一个维度可能有被裁剪的内容
*IJKMPMovieScalingModeFill 不均匀伸。渲染维度将完全匹配可见边界
*/
[self.player setScalingMode:IJKMPMovieScalingModeAspectFit];
//设置缓存大小,太大了没啥用,太小了视频就处于边播边加载的状态,目前是10M,后期可以调整
[self.player setPlayerOptionIntValue:10* 1024 *1024 forKey:@"max-buffer-size"];
//如果是直播,最好不让他自动播放,如果YES,那么就会自动播放电影,不需要通过[self.player play];就可以播放了,
//但是如果NO,我们需要注册通知,然后到响应比较合适的地方去检测通知,然后必须通过[self.player play];手动播放
self.player.shouldAutoplay = NO;
创建好了IJKFFMoviePlayerController之后,真正有用的是他的view,我们获取到他的view,并将它添加到我们自定义的viewController的view之上,创建一个自定义的PlayerController,或者是PlayerView。
四、IJKFFMoviePlayerController通知
视频的播放的消息传递是能过通知来完成的
// MPMediaPlayback.h
// 当准备状态的对象改变符合MPMediaPlayback协议时发布。
//他支持 MPMoviePlayerContentPreloadDidFinishNotification.
IJKMPMediaPlaybackIsPreparedToPlayDidChangeNotification;
该通知说明视屏已经准备好马上就要开始播放,这时候我们可以取到当前视屏的总时长,当前播放时间行信息,所以在该通知产生时我们可以对界面视屏数据进行初始化。
// MPMoviePlayerController.h
// Movie Player Notifications
//当Player缩放模式发生变化时发布。
IJKMPMoviePlayerScalingModeDidChangeNotification;
//当视频播放结束或用户退出播放时发布。
JKMPMoviePlayerPlaybackDidFinishNotification;
// 在播放状态发生变化时发布,无论是编程改变的还是用户事件触发的
IJKMPMoviePlayerPlaybackStateDidChangeNotification;
我们可以通过当前player的playbackState获取当前播放状态:
IJKMPMoviePlaybackStateStopped, //停止状态
IJKMPMoviePlaybackStatePlaying,//播放状态
IJKMPMoviePlaybackStatePaused,//暂停状态
IJKMPMoviePlaybackStateInterrupted,//中断状态
IJKMPMoviePlaybackStateSeekingForward,//向前拖动状态
IJKMPMoviePlaybackStateSeekingBackward//向后拖动状态
//在网络加载状态更改时发布。
IJKMPMoviePlayerLoadStateDidChangeNotification;
// 当播放器开始或结束正在通过AirPlay播放的视频时发送
IJKMPMoviePlayerIsAirPlayVideoActiveDidChangeNotification;
// -----------------------------------------------------------------------------
// Movie Property Notifications
// 在视频播放器上调用- preparetoplay将开始异步确定视频属性。相关的视频属性可用时,这些通知就会被发布。
IJKMPMovieNaturalSizeAvailableNotification;
// -----------------------------------------------------------------------------
// Extend Notifications
IJKMPMoviePlayerVideoDecoderOpenNotification;
IJKMPMoviePlayerFirstVideoFrameRenderedNotification;
IJKMPMoviePlayerFirstAudioFrameRenderedNotification;
//视频加载完成时发送
IJKMPMoviePlayerDidSeekCompleteNotification;