本文主要是对视频播放器项目的介绍。本文主要介绍视频控制和多个视频的切换以及全屏播放。
1.播放控制
播放控制,主要的难点在于处理AVPlayerLayer的层级问题。为了保证AVPlayerLayer永远在控制层下面,我们可以把控制层(控制层是用来控制视频的暂停、播放、播放进度和缓冲进度等)加入AVPlayerLayer,作为AVPlayerLayer的子视图。但是AVPlayerLayer是CALayer,不能进行用户交互。所以我们要自定义UIView,修改他的layer,作为显示视图。
制作完的效果如下:
View一共有四层,controlView -> presentView -> containerView -> controller.view,。controlView在最上,上文介绍用途了;presentView是用来展示视频的,它的layer就是AVPlayerLayer;containerView作为controlView和presentView的父视图。
@interface FHPlayerPresentView : UIView
@property (nonatomic, strong) AVPlayer *player;
/// default is AVLayerVideoGravityResizeAspect.
@property (nonatomic, strong) AVLayerVideoGravity videoGravity;
@end
@implementation FHPlayerPresentView
/*
+ (Class)layerClass;
- (AVPlayerLayer *)avLayer;
- (void)setPlayer:(AVPlayer *)player;
这三个方法相当于下面的方法
[self.layer addSublayer:avPlayerLayer];
**/
// 重写+layerClass方法使得在创建的时候能返回一个不同的图层子类。UIView会在初始化的时候调用+layerClass方法,然后用它的返回类型来创建宿主图层
+ (Class)layerClass
{
return [AVPlayerLayer class];
}
- (AVPlayerLayer *)avLayer
{
return (AVPlayerLayer *)self.layer;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor blackColor];
}
return self;
}
- (void)setPlayer:(AVPlayer *)player
{
if (player == _player) return;
self.avLayer.player = player;
}
- (void)setVideoGravity:(AVLayerVideoGravity)videoGravity
{
if (videoGravity == self.videoGravity) return;
[self avLayer].videoGravity = videoGravity;
}
- (AVLayerVideoGravity)videoGravity
{
return [self avLayer].videoGravity;
}
@end
为什么是FHPlayerPresentView不是直接操作AVPlayerLayer呢?因为AVPlayerLayer的层级没办法控制(反正我是没找着方法),当切换视频的时候controlView就要在AVPlayerLayer下面了,controlView上面的控制按钮就不能用了。因为当切换视频的时候,首先是移除AVPlayerLayer,再添加新的,而 controlView一直没有变化。所已我把AVPlayerLayer放在FHPlayerPresentView中,操作FHPlayerPresentView的层级就很简单了。
另外,我们需要设置一个属性,来记录视频是否暂停,是否是播放了。
// 是否正在播放
@property (nonatomic, assign) BOOL playing;
// 控制视频播放
- (void)controlAction:(UIButton *)button
{
// 如果视频正在播放,暂停;否则,播放。
if (self.playing)
{
// 暂停
[self.player pause];
}
else
{
//播放
[self play];
}
// 记录播放的状态
self.playing = !self.playing;
// 修改button的图标
NSString *imageName = self.playing ? @"pause" : @"play";
[self.controlView.playBtn setImage:[UIImage imageNamed:imageName] forState:UIControlStateNormal];
}
下面的这些代码就相当于[self.containerView.layer addSublayer:self.avLayer];
。
FHPlayerPresentView *presentView = [[FHPlayerPresentView alloc] initWithFrame:self.containerView.bounds];
[self.containerView addSubview:presentView];
presentView.player = self.player;
self.presentView = presentView;
[self.containerView insertSubview:self.controlView aboveSubview:self.presentView];
2.切换视频
切换视频需要我们多次创建AVPlayer及其相关对象,创建之后一定要释放,否则就会发生内存泄漏,且不会有提示。
- (IBAction)playTheNext:(id)sender
{
_currentIndex++;
if (_currentIndex < 0 || self.dataSource.count == 0)
{
return;
}
if ( _currentIndex >= self.dataSource.count)
{
_currentIndex = 0;
}
// 停止播放视频
[self stop];
[self initPlayer];
}
我的思路是移除原来的视频播放器,在初始化一个新的。苹果还提供了一个切换视频的方法:
- (void)replaceCurrentItemWithPlayerItem:(nullable AVPlayerItem *)item;
但是这个方法在某些版本上会引发异常,所以我就没用。
// 停止播放视频
- (void)stop
{
// 暂停播放视频
[self.player pause];
// 记录视频的播放状态
self.playing = NO;
// 移除观察者
[self.player removeTimeObserver:_timeObserver];
// 一定要取消player的当前PlayerItem,负责会造成内存泄漏,且没有提示
// 多次切换PlayerItem就会崩溃
[self.player replaceCurrentItemWithPlayerItem:nil];
_timeObserver = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
_itmePlaybackEndObserver = nil;
// 移除KVO
// 必须先移除KVO,在释放playerItem,否则多初始化几次播放器,就会崩溃,而且没有错误日志。
[self removeObserver];
// 释放视频相关对象
self.player = nil;
self.playerItem = nil;
self.asset = nil;
[self.playerLayer removeFromSuperlayer];
}
视频播放器,一定要把观察者也一并移除,不然会一直存在,这样会造成大量的内存损耗,但是重复添加并不会引起崩溃。移除KVO的时候,我用了try - catch,因为重复移除是会引起崩溃的。
[self.player replaceCurrentItemWithPlayerItem:nil];
这行代码的意思释放AVPlayer持有的AVPlayerItem,一定要执行,否则会发生内存泄漏,切没哟提示。如果不添加,多次切换,大约10次以上后,就会发生崩溃。
- (void)removeObserver
{
// 防止删除不存在的观察者,崩溃
@try{
[self.playerItem removeObserver:self forKeyPath:@"status"];
[self.playerItem removeObserver:self forKeyPath:@"presentationSize"];
[self.playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
[self.playerItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
[self.playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty"];
} @catch(NSException *e){
NSLog(@"failed to remove observer");
}
}
3.全屏播放
全屏播放是每一个视屏播放器的标配。现在一般是用户点击按钮,进行竖屏和全屏的切换。
- (void)fullScreanAction:(UIButton *)button
{
[self changeInterfaceOrientation:self.isFullScreen ? UIInterfaceOrientationPortrait : UIInterfaceOrientationLandscapeRight];
}
// 旋转屏幕,interfaceOrientation要旋转的方向
- (void)changeInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
// 父视图
UIView *superView = nil;
// 旋转的角度,默认值是恢复原来的样式
CGAffineTransform transform = CGAffineTransformIdentity;
// 竖屏 -> 横屏
if (interfaceOrientation == UIInterfaceOrientationLandscapeLeft || interfaceOrientation == UIInterfaceOrientationLandscapeRight) {
// 父视图是keyWindow
superView = [[UIApplication sharedApplication] keyWindow];
// HOME键在左边,逆时针旋转90°
if (interfaceOrientation == UIInterfaceOrientationLandscapeLeft) {
transform = CGAffineTransformMakeRotation(-M_PI_2);
}else if(interfaceOrientation == UIInterfaceOrientationLandscapeRight){
// HOME键在右边,顺时针旋转90°
transform = CGAffineTransformMakeRotation(M_PI_2);
}
// 记录界面的状态
self.isFullScreen = YES;
}else{
// 横屏 -> 竖屏
superView = self.containerView;
transform = CGAffineTransformIdentity;
self.isFullScreen = NO;
}
[superView addSubview:self.presentView];
// 修改界面的方向
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
/*
* 设置- (BOOL)shouldAutorotate{return NO;}才有效
*/
[UIApplication sharedApplication].statusBarOrientation = interfaceOrientation;
#pragma clang diagnostic pop
// 标记界面的方向需要更改
[self setNeedsStatusBarAppearanceUpdate];
// 旋转动画
[UIView animateWithDuration:0.25 animations:^{
// 旋转
self.presentView.transform = transform;
[UIView animateWithDuration:0.25 animations:^{
// 修改尺寸
self.presentView.frame = superView.bounds;
}];
} completion:^(BOOL finished) {
// 修改控制视图的约束
[self updateControlViewConstraint];
}];
}
- (void)updateControlViewConstraint
{
// 当屏幕旋转后,屏幕的长宽也发生了变化,现在长的值变为了原来的宽的值
if (self.isFullScreen)
{
CGFloat width = self.presentView.bounds.size.width;
CGFloat height = self.presentView.bounds.size.height;
self.controlView.frame = CGRectMake(0, height - 40, width, 40);
}
else
{
CGFloat width = SCREEN_WIDTH;
CGFloat height = SCREEN_WIDTH / 7 * 4;
self.controlView.frame = CGRectMake(0, height - 40, width, 40);
}
// 如果不执行下面的两个方法, 上面的设置无效
// 标记更新约束
[self.controlView setNeedsUpdateConstraints];
// 更新约束
[self.controlView updateConstraintsIfNeeded];
}
修改手机的statusbar的方向的核心方法是:[UIApplication sharedApplication].statusBarOrientation = interfaceOrientation;
;但是发现有时候发现这样设置无效,那是因为还需要添加下面的代码。
- (BOOL)shouldAutorotate
{
return NO;
}
有时候这样设置了可能仍然无效。如果window.rootViewController是一个容器视图,例如UINavigationController,UITabBarController,默认走的是容器视图下面的方法,我们要设置成走对应视图的对应方法。以UINavigationController为例。
// 是否支持屏幕旋转
- (BOOL)shouldAutorotate {
return [self.topViewController shouldAutorotate];
}
// 支持的屏幕旋转方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return [self.topViewController supportedInterfaceOrientations];
}