在iOS视频开发中,传统的方案可以直接使用系统的 MPMoviePlayerController 既可以直接将系统的播放页面掉出来,更贴心的为我们添加了控制条,全屏放大及暂停按钮。但是实际中我们可以需要针对播放器做更多的自定义设置,继而更多的是会采用 AVPlayer,因为 AVPlayer 提供了更为强大的功能,虽然在使用的过程会比较麻烦,但是确实能为我们的 app 提供更好的视频播放体验提供前提。
初步认识 AVPlayer
在这之前我们先介绍几个相关类:
- AVPlayerItem:一个视频资源对应一个 AVPlayerItem对象,当你需要循环播放多个视频资源时也需创建多个AVPlayerItem对象,并将其添加在一个数组中。我们可以通过静态方法 playerItemWithURL 进行创建,同样也可以通过 AVAsset,而根据两种创建方法的不同,我们即知道加载本地视频资源还是网络视频资源。
- CMTime:关于 CMTime 我们可以很方便的获取到视频资源的精准播放长度长度,及快速让播放器定位到指定时间进行播放。
- AVPlayer: AVPlayer 我们能把播放器做得那么溜全靠它了,视频资源就是交给它进行处理的。
- AVPlayerLayer: AVPlayerLayer 其实 AVPlayer 仅仅处理视频资源,但是视频的画面其实是加载到AVPlayerLayer上的。
根据以上的简单介绍,我们可以做一个简单的视频播放器,只要仅仅能看到播放画面及听到声音就可以了,这里需要特别注意 AVPlayer 必须设置为成员变量,在局部变量中因为方法执行完后即被释放了会导致视频播放失败:
1 2 3 4 5 6 7 8 9 10
| NSString *filePath = [[NSBundle mainBundle] pathForResource:@"snow" ofType:@"mp4"]; NSURL *sourceMovieURL = [NSURL fileURLWithPath:filePath]; AVAsset *movieAsset = [AVURLAsset URLAssetWithURL:sourceMovieURL options:nil]; AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:movieAsset]; self.player = [AVPlayer playerWithPlayerItem:playerItem]; self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player]; self.playerLayer.frame = self.view.bounds; self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspect; [self.view.layer insertSublayer:self.playerLayer atIndex:0]; [self.player play];
|
为我们的播放器添加控制条
这一步主要为在播放器上添加常规按钮,如返回、播放/暂停、全屏、播放时间进度条等一些为用户和播放器的交互试图。
首先我们自定义一个 BHPlayerControlView,这里面包含了所有的操作按钮及进度条、展示时间的 Label、当然还包括加载视频缓冲时的菊花 UIActivityIndicatorView,以下主要为设置控制条子试图约束的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
|
- (void)makeSubViewsConstraints { [self.topImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.leading.trailing.top.equalTo(self); make.height.mas_equalTo(80); }]; [self.backBtn mas_makeConstraints:^(MASConstraintMaker *make) { make.leading.equalTo(self.mas_leading).offset(15); make.top.equalTo(self.mas_top).offset(5); make.width.height.mas_equalTo(30); }]; [self.bottomImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.leading.trailing.bottom.equalTo(self); make.height.mas_equalTo(50); }]; [self.startBtn mas_makeConstraints:^(MASConstraintMaker *make) { make.leading.equalTo(self.bottomImageView.mas_leading).offset(5); make.bottom.equalTo(self.bottomImageView.mas_bottom).offset(-5); make.width.height.mas_equalTo(30); }]; [self.fullScreenBtn mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.mas_equalTo(30); make.trailing.equalTo(self.bottomImageView.mas_trailing).offset(-5); make.centerY.equalTo(self.startBtn.mas_centerY); }]; [self.currentTimeLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.leading.equalTo(self.startBtn.mas_trailing).offset(-3); make.centerY.equalTo(self.startBtn.mas_centerY); make.width.mas_equalTo(43); }]; [self.totalTimeLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.trailing.equalTo(self.fullScreenBtn.mas_leading).offset(3); make.centerY.equalTo(self.startBtn.mas_centerY); make.width.mas_equalTo(43); }]; [self.progressView mas_makeConstraints:^(MASConstraintMaker *make) { make.leading.equalTo(self.currentTimeLabel.mas_trailing).offset(4); make.trailing.equalTo(self.totalTimeLabel.mas_leading).offset(-4); make.centerY.equalTo(self.startBtn.mas_centerY); }]; [self.videoSlider mas_makeConstraints:^(MASConstraintMaker *make) { make.leading.equalTo(self.currentTimeLabel.mas_trailing).offset(4); make.trailing.equalTo(self.totalTimeLabel.mas_leading).offset(-4); make.centerY.equalTo(self.currentTimeLabel.mas_centerY).offset(-1); make.height.mas_equalTo(30); }]; [self.progressIndicatorLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.width.mas_equalTo(160); make.height.mas_equalTo(40); make.center.equalTo(self); }]; [self.activity mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self); }]; [self.repeatBtn mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self); }]; }
|
当然还需要对外暴漏我们的控制器试图的隐藏及显示,及一些子试图初始某些属性设置方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
- (void)showControlView { self.topImageView.alpha = 1; self.bottomImageView.alpha = 1; self.backBtn.alpha = 1; }
- (void)hideControlView { self.topImageView.alpha = 0; self.bottomImageView.alpha = 0; self.backBtn.alpha = 0; }
- (void)resetControlView { self.videoSlider.value = 0; self.progressView.progress = 0; self.currentTimeLabel.text = @"00:00"; self.totalTimeLabel.text = @"00:00"; self.progressIndicatorLabel.hidden = YES; self.repeatBtn.hidden = YES; self.backgroundColor = [UIColor clearColor]; }
|
封装 AVPlayer
以上已经自定义好了我们的控制器试图,下面我们开始封装 AVPlayer。
首先我们需要对 AVPlayerItem 设置监听,监听我们的视频资源的状态,这里通过 KVO 监听播放器的状态:
1 2 3 4 5 6 7 8 9 10
| [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayDidEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem];
[self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
[self.playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
[self.playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
[self.playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
|
然后在下面的回调方法中分别针对不同的状态进行不同的逻辑判断,主要为视频成功加载后需要为我们的播放器添加触摸手势,这里不建议直接重写 touches 的四个回调事件,因为这样会更新我们触摸逻辑的复杂度,其次视频如果加载失败,还需要额外的判断,不划算。所以这里建议直接使用手势,滑动事件监听手势的回调方法就可以了。
1
| - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
|
关于计算缓冲进度:
1 2 3 4 5 6 7 8 9
| - (NSTimeInterval)availableDuration { NSArray *loadedTimeRanges = [[_player currentItem] loadedTimeRanges]; CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue]; float startSeconds = CMTimeGetSeconds(timeRange.start); float durationSeconds = CMTimeGetSeconds(timeRange.duration); NSTimeInterval result = startSeconds + durationSeconds; return result; }
|
loadedTimeRanges 这个属性是一个数组,里面装的是本次缓冲的时间范围,这个用一个结构体 CMTimeRange 表示,start 表示本次缓冲时间的起点,duratin 表示本次缓冲持续的时间范围。
关于 CMTime,我们可以通过 CMTimeGetSeconds([_player currentTime]) 获取当前播放器的时间,但是通常我们可能需要换算为小时:分钟:秒这种格式。
关于触摸的回调事件,主要为控制左右滑动时视频的快进快退逻辑,及上下滑动时的音量控制逻辑,具体可参考代码:
1 2 3 4 5 6
|
- (void)panDirection:(UIPanGestureRecognizer *)pan
|
还有一个很重要的触摸事件,就是滑动我们的进度条 UISlider 的回调,这个回调里面主要为处理 NSTimer 及滑动过程中进度指示 Label 的文本内容设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
- (void)progressSliderTouchBegan:(UISlider *)slider;
- (void)progressSliderValueChanged:(UISlider *)slider
- (void)progressSliderTouchEnded:(UISlider *)slider;
|
最后就是关于视频播放完成后,我们需要对播放器及播放器控制器进行重置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
- (void)moviePlayDidEnd:(NSNotification *)notification { self.state = BHPlayerStateStopped; self.playDidEnd = YES; self.controlView.repeatBtn.hidden = NO; self.isMaskShowing = NO; [self animateShow]; }
|
在这里提醒一点很重要的地方,当我们的时候及处于静音的时候需要做以下处理:
1 2 3 4
| NSError *setCategoryError = nil; BOOL success = [[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayback error: &setCategoryError]; if (!success) { }
|
代码可以下载GITHUB中BlogDemo进行查看。