详解iOS视频播放方式
多媒体这整个系列的文章自己也准备好开始整理了,先从视频音频最简单也是最常用的播放出发慢慢的往下深究,探索到底层的编码解码等等,这篇文章就从视频的播放这个最简单的说起。
iOS的视频播放方式有几种?其实要是只是简单的想播放一段视频并且对UI没什么要求的话的确比较简单,很容易搞定,但我相信这种情况除了你的Demo一般是不会出现的,对播放UI的定义以及可能有各种不同的需求对应着你是不能随便写个播放器就没事了的。
最原始的播放
要不是刚接触iOS开发的同学应该是知道MediaPlayer这个框架的,要是想简单的使用它播放视频,可能几行代码就能搞定了,它里面有一个MPMoviePlayerViewController,利用起来简单的不要不要的。
不过遗憾的是自从iOS 9.0开始,它是被Apple遗弃了的,9.0之后的项目建议用的我们下面再说,你要是有维护9.0之前的项目,可能它你也有必要了解一下,我们也介绍一个它的基本的使用,以及它里面的整个播放的代码逻辑。
工程师以前写过一个三方,KRVideoPlayer
这个播放器就是基于MediaPlayer框架写的,里面就两个文件,代码也是相当的简单,你直接把它源码下载下来之后我们当一个了解MediaPlayer的Demo简单的说一下。下满是它git上面展示的gif Demo图片:
你在看看它源码里面的文件:只有 KRVideoPlayerControlView 和 KRVideoPlayerController 两个,简单分析它们:
1、KRVideoPlayerControlView 继承自 UIView
说白了这个文件写的就是播放器的UI,包括一些播放按钮,进度条,以及全屏切换等等
2、KRVideoPlayerController 集成自 MPMoviePlayerController
继承之后直接使用MPMoviePlayerController来播放视频,是在它初始化的时候在self.view 上添加 KRVideoPlayerControlView 这个自定义的UI,你可以看到下面的代码:
// 初始化KRVideoPlayerController - (instancetype)initWithFrame:(CGRect)frame { self = [super init]; if (self) { self.view.frame = frame; self.view.backgroundColor = [UIColor blackColor]; self.controlStyle = MPMovieControlStyleNone; [self.view addSubview:self.videoControl]; self.videoControl.frame = self.view.bounds; [self configObserver]; [self configControlAction]; } return self; } // 懒加载KRVideoPlayerControlView - (KRVideoPlayerControlView *)videoControl { if (!_videoControl) { _videoControl = [[KRVideoPlayerControlView alloc] init]; } return _videoControl; }
关于MediaPlayer还有下面的需要你留意一下:
1、关于播放或者暂停等的方法都是在MPMediaPlayback协议里面的
2、MPMoviePlayerController就是遵守了上面说的MPMediaPlayback协议,下面的MPMoviePlayerController源码:
3、在给MPMoviePlayerController写的类别MPMovieProperties、MPMoviePlayerThumbnailGeneration、MPMoviePlayerTimedMetadataAdditions包含了这个播放器几乎所有的功能,淡然这部分的方法代代码都是在MPMoviePlayerController.h中,有兴趣或者需要的可以command进去了解。
4、上面介绍的三方提供给大家的不仅仅是一份代码,希望我们都能理解一个思路,就是自定义的播放器我们该怎么去理解去动手做。这点后面我还会再提。
关于MediaPlayer的暂时就提这么多,有问题欢迎交流。
该升级一下了
嗯,该升级一下了,说到这里就的说我们前面说到的9.0系统之后的播放器,这说这个之前顺便提一个自己的见解,以前我们开发应用的时候我记得最开始适配的最低版本是7.0以上的,到前两年发展到8.0以上,按照我自己的理解,在11系统发布后我们要是做新应用或者旧的项目项目维护的时候应该要慢慢的舍弃7.0以及8.0的了,也就是最低版本按照9.0开始,因为不管是7.0还是8.0,用户所占的比例真的是很小很下了,并且一些新鲜的功能在我们的低版本是不支持的, 维护的成本也会慢慢的变得越来越大,当然这些也都不是空穴来风,可以上网去搜一下8.0之前版本系统占得比例,以及8.0、7.0给整个维护带来的成本,我在最近逛一些论坛的时候也有同行在说这个问题了。好了回到正题!
说我们的正题:9.0之后Apple建议用的: AVKit框架,首先AVKit框架是8.0之后出现的,它是建立在我们熟悉的AVFoundation框架之上的.
利用AVKit进行视频播放时我们整理一下我们需要的大致都在这几个类或者协议当中:
1、AVPlayerItem (视频要播放的元素)
2、AVPlayerLayer (播放显示视频的图层界面)
3、AVPlayer (用于播放音视频)
4、AVPlayerViewController (控制器)
5、AVPlayerViewControllerDelegate(协议)
要是想要彻底的了解AVFoundation这个框架是不容易的,这个框架的确很庞大,有一本书叫做 《AV Foundation 开发秘籍》有兴趣的可以去购买看看,自己也在学习当中,后续的文章全都会整理在这个系列当中。
这篇文章就等于是给这个系列开一个头,这个框架的学习之路应该是漫长的,也希望自己能坚持完吧这个系列文章全都总结出来。下面把上面说的各个类分别说一下:
1、AVPlayerItem
在我们使用AVPlayer播放视频的时候,提供视频信息的就是AVPlayerItem,一个AVPlayerItem对应着你提供的一个视频Url资源,这个理解它的时候可以把它比作一个Model, 你初始化了AVPlayerItem之后,并不是马上就可以使用它了,因为凡是和Url网络扯上关系的,都需要时间,等AVPlayerItem加载好之后就可以使用它了,那这一步我们怎么处理呢?
1> : 答案是利用KVO观察statues属性为 AVPlayerStatusReadyToPlay,看看这个属性的定义:
@property (nonatomic, readonly) AVPlayerStatus status 它是一个只读属性,这点也需要注意,其实也就理解利用KVO的原因。
2>: 顺便总结要是你要显示当前视屏的缓存进度,你需要监测它的loadedTimeRanges属性。
2、AVPlayerLayer
它主要负责的就是视频的显示,继承自CALayer,其实你可以把它理解成我们的View。我们自定义的那些播放时候的控件就是添加在它上面的,比如我们能看到的播放按钮,停止按钮,或者播放进度条等等。
3、 AVPlayer
它主要负责的是管理视频播放,暂停等等,相当于一个视频管理器,要是类比的话他就是一个ViewController(当然不是真正的ViewController),这三者就基本含括了一个基本的视频播,基于着三者我们总结一下播放一个视频的基本的过程:
首先,得到视频的URL 根据URL创建AVPlayerItem 把AVPlayerItem 提供给 AVPlayer AVPlayerLayer 显示视频。 AVPlayer 控制视频, 播放, 暂停, 跳转 等等。播放过程中获取缓冲进度,获取播放进度。视频播放完成后做些什么,是暂停还是循环播放,还是获取最后一帧图像。
4、AVPlayerViewController
它是Apple 帮我们封装好的可以一个视频播放控制器,它就有一个 @property (nonatomic, strong, nullable) AVPlayer *player 的属性,前面的AVPlayer也就像相应的需要赋值给它,它里面还有一些我们需要理解一下的属性,我们也把它写出来,具体代码我们下面再看:
player: 设置播放器 showsPlaybackControls: 设置是否显示媒体播放组件,默认YES videoGravity: 设置视频拉伸模式 allowsPictureInPicturePlayback: 设置是否允许画中画回放,默认YES delegate: 设置代理
5、AVPlayerViewControllerDelegate
这个代理就是前面说的AVPlayerViewController的协议,它主要的是为画中画的设置的代理,前面介绍 AVPlayerViewController 的时候有看到过一个是否允许画中画的属性,具体什么是画中画相信大家都了解,看过直接的朋友应该都看到了这个技术点的具体应用。我们看看它里面的饭法规主要都干了些什么?
// 1、即将开始画中画 - (void)playerViewControllerWillStartPictureInPicture:(AVPlayerViewController *)playerViewController; // 2、开始画中画 - (void)playerViewControllerDidStartPictureInPicture:(AVPlayerViewController *)playerViewController; // 3、画中画失败 - (void)playerViewController:(AVPlayerViewController *)playerViewController failedToStartPictureInPictureWithError:(NSError *)error; // 4、即将结束画中画 - (void)playerViewControllerWillStopPictureInPicture:(AVPlayerViewController *)playerViewController; // 5、结束画中画 - (void)playerViewControllerDidStopPictureInPicture:(AVPlayerViewController *)playerViewController;
我们看一个简单的Demo
我们先不说关于AVFoundation复杂的东西,因为自己也是在学习这个 AVFoundation当中,我们先看一些很简单的Demo,就简单的利用一下AVFoundation 播放一下视频:
我们在简单的看一下我们写的这部分的代码,简单的先使用了一下我们说的上面的一些知识点:
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. self.view.backgroundColor = [UIColor whiteColor]; self.avPlayerItem = [AVPlayerItem playerItemWithURL:[NSURL URLWithString:MovieURL]]; self.avPlayer = [[AVPlayer alloc]initWithPlayerItem:self.avPlayerItem]; self.avPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:self.avPlayer]; self.avPlayerLayer.frame = CGRectMake(10, 100, 355, 200); [self.view.layer addSublayer:self.avPlayerLayer]; // 添加观察者 [self addObserverWithAVPlayerItem]; } #pragma mark -- #pragma mark -- KVO -(void)addObserverWithAVPlayerItem{ //状态添加观察者 [self.avPlayerItem addObserver:self forKeyPath:@"status" options:(NSKeyValueObservingOptionNew) context:nil]; // 缓存进度添加观察者 [self.avPlayerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil]; } -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ AVPlayerItem * avplayeritem = (AVPlayerItem *)object; if ([keyPath isEqualToString:@"status"]) { AVPlayerStatus status = [[change objectForKey:@"new"] intValue]; if (status == AVPlayerStatusReadyToPlay) { NSLog(@"准备好播放"); CMTime duration = avplayeritem.duration; NSLog(@"视频总时长:%.2f",CMTimeGetSeconds(duration)); // 播放 [self.avPlayer play]; }else if (status == AVPlayerStatusFailed){ NSLog(@"视频准备发生错误"); }else{ NSLog(@"位置错误"); } }else if ([keyPath isEqualToString:@"loadedTimeRanges"]){ // 可以自定义缓存进度 NSTimeInterval timeInterval = [self alreadyCacheVideoProgress]; NSLog(@"视频已经缓存的时长:%.2f",timeInterval); } } #pragma mark -- #pragma mark -- alreadyCacheVideoProgress -(NSTimeInterval)alreadyCacheVideoProgress{ // 先获取到它的缓存的进度 NSArray * cacheVideoTime = [self.avPlayerItem loadedTimeRanges]; // CMTimeRange 结构体 start duration 表示起始位置 和 持续时间 // 获取缓冲区域 CMTimeRange timeRange = [cacheVideoTime.firstObject CMTimeRangeValue]; float startSeconds = CMTimeGetSeconds(timeRange.start); float durationSeconds = CMTimeGetSeconds(timeRange.duration); // 计算总缓冲时间 = start + duration NSTimeInterval result = startSeconds + durationSeconds; return result; }
这些点我们有必要注意一下
1、CMTime 一个专门用于标识视频时间的结构体
/*! @typedef CMTime @abstract Rational time value represented as int64/int32. */ typedef struct { CMTimeValue value; /*! @field value The value of the CMTime. value/timescale = seconds. 帧数 */ CMTimeScale timescale; /*! @field timescale The timescale of the CMTime. value/timescale = seconds.帧率(影片每秒有几帧)*/ CMTimeFlags flags; /*! @field flags The flags, eg. kCMTimeFlags_Valid, kCMTimeFlags_PositiveInfinity, etc. */ CMTimeEpoch epoch; /*! @field epoch Differentiates between equal timestamps that are actually different because of looping, multi-item sequencing, etc. Will be used during comparison: greater epochs happen after lesser ones. Additions/subtraction is only possible within a single epoch, however, since epoch length may be unknown/variable. */} CMTime;
前面的代码中我们看到有一个获取视频总长度的方法:
CMTime duration = avplayeritem.duration; NSLog(@"视频总时长:%.2f",CMTimeGetSeconds(duration));
可以看到CMTimeGetSeconds这个函数把一个CMTime类型转化成一个浮点型,如果一个影片为60帧/每秒, 当前想要跳转到120帧的位置,也就是两秒的位置,那么就可以创建一个 CMTime 类型数据。它通常可以用下面两个函数来创建.
1>: CMTimeMake(int64_t value, int32_t scale) Eg: CMTime time1 = CMTimeMake(120, 60);
2>:CMTimeMakeWithSeconds(Flout64 seconds, int32_t scale) Eg: CMTime time2 = CMTimeWithSeconds(120, 60);
CMTimeMakeWithSeconds 和 CMTimeMake 区别在于,第一个函数的第一个参数可以是float,其他一样。
- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;
比如说:我们把时间间隔设置为, 1/ 10 秒,然后 block 里面更新 UI。就是一秒钟更新10次UI,我们验证一下:
[self.avPlayer addPeriodicTimeObserverForInterval:CMTimeMake(1, 10) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { // CMTime的timescale的定义帮助理解下面代码 // @field timescale The timescale of the CMTime. value/timescale = seconds. float currentPlayTime = (double)self.avPlayerItem.currentTime.value/ self.avPlayerItem.currentTime.timescale; NSLog(@"当前播放进度:%f",currentPlayTime); }];
我们随便截取出一段打印的日志,看一下结果就可以验证:
2、AVPlayerItem 视频播放结束通知
/* Note that NSNotifications posted by AVPlayerItem may be posted on a different thread from the one on which the observer was registered. */ // notifications description AVF_EXPORT NSString *const AVPlayerItemTimeJumpedNotification NS_AVAILABLE(10_7, 5_0); // the item's current time has changed discontinuously AVF_EXPORT NSString *const AVPlayerItemDidPlayToEndTimeNotification NS_AVAILABLE(10_7, 4_0); // item has played to its end time AVF_EXPORT NSString *const AVPlayerItemFailedToPlayToEndTimeNotification NS_AVAILABLE(10_7, 4_3); // item has failed to play to its end time AVF_EXPORT NSString *const AVPlayerItemPlaybackStalledNotification NS_AVAILABLE(10_9, 6_0); // media did not arrive in time to continue playback AVF_EXPORT NSString *const AVPlayerItemNewAccessLogEntryNotification NS_AVAILABLE(10_9, 6_0); // a new access log entry has been added AVF_EXPORT NSString *const AVPlayerItemNewErrorLogEntryNotification NS_AVAILABLE(10_9, 6_0); // a new error log entry has been added // notification userInfo key type AVF_EXPORT NSString *const AVPlayerItemFailedToPlayToEndTimeErrorKey NS_AVAILABLE(10_7, 4_3); // NSError
3、这些个三方框架
(1): VKVideoPlayer
(2): ALMoviePlayerController
(3): PBJVideoPlayer
(4): 还有这个比较厉害的MobileVLCKit
关于上面上的这些三方都给出了连接,最后一个给的是一篇帮助我们集成的文章,这些三方在后面这个系列文章的总结中会一点点慢慢的全都说一下,在这里只提一下有这些框架在,有兴趣可以先了解,后面我在总结。