在我们之前的项目,视频的播放都是使用MPMoviePlayerViewController(包含了播放的UI,并封装好了播放的一些函数),但是苹果的官网表示从IOS9版本开始,MPMoviePlayerViewController被弃用了,推荐使用AVPictureInPictureController或者AVPlayerViewController。
但是一直使用苹果封装的ViewController可控性依旧比较差,如:
(1)播放状态无法获得;
(2)置于后台和唤醒时,在不同设备上视频播放展现状态不同;
(3)视频播放进度与画面不同步;
(4)播放的声音和画面不同步;
(5)无法直接加其他UI,如:分享按钮等。
基于以上几点考虑,我决定直接用AVPlayer直接实现一个播放器,完成一些简单的播放功能。
基本功能:
(1)进度条、视频播放画面、播放时间同步;
(2)可以播放和暂停,且两种状态有不同UI切换;
(3)进度条可以拖动,且视频播放画面同步;
(4)点击关闭按钮可以销毁播放器;
(5)增加了AutoLayout布局约束
(6)视频的横竖屏切换播放与设备的横竖屏切换同步。
(7)一些小细节,如:进度条拖拽、点击、抬起时,播放/暂停按钮UI的变化;置于后台和唤醒,重新设置视频播放状态等。
代码如下:
头文件:ViewController.h
#import
#import @interface ViewController : UIViewController @property (nonatomic,strong)AVPlayer *avPlayer; @property (nonatomic,strong)AVPlayerItem *avPlayerItem; @property (nonatomic,strong)AVPlayerLayer *avPlayerLayer; //装载视频layer、进度条、时间、时长等UI的容器 @property (nonatomic,strong)UIView *videoContainer; //进度条UI @property (nonatomic,strong)UISlider *progress; //当前播放时间UI @property (nonatomic,strong) UILabel *startLabel; //视频总时长UI @property (nonatomic,strong) UILabel *endLabel; //关闭按钮 @property (nonatomic,strong) UIButton *closeButton; //控制播放和暂停的按钮 @property (nonatomic,strong) UIButton *playButton; //当前播放时间 @property (nonatomic,assign) float playheadTime; //是否处于播放状态 @property (nonatomic,assign) BOOL isPlaying; //播放帧的监视器 @property (nonatomic,strong) id playTimeObserver; @end
实现:ViewController.m#import "ViewController.h" @interface ViewController () @end static NSBundle *playerBundle; NSString * const XAdVideoStatusKey = @"status"; @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; NSString *urlString = @"要播放的视频地址 http://...."; [self configPlayerControllerWithURL:urlString]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } #pragma mark - public - (void) configPlayerControllerWithURL:(NSString *)urlString{ [self initBundle]; [self setProgressRefProperties]; [self showUI]; NSURL *url = [NSURL URLWithString:urlString]; [self setupPlayerWithURL:url]; } /* * 重头播放 */ - (void)play{ if (self.avPlayer) { self.isPlaying = YES; [self.avPlayer play]; } } /* * 暂停 */ - (void)pause { if (self.avPlayer) { [self.avPlayer pause]; self.isPlaying = NO; } } /* * 销毁对象 */ - (void)cancel { if (self.avPlayer) { [self.avPlayer pause]; } [self removeUIView]; [self removePlayerItemObserver]; [self removePlayerObserver]; [self destroyAllProperties]; } /* * 从某一帧播放 */ - (void)playFromTime:(double)time { NSLog(@"playFromTime = %f",time); if (self.avPlayer) { [self seekToTime:time]; [self.avPlayer play]; self.isPlaying = YES; } } /* * 在某一帧暂停 */ - (void)pauseOnTime:(double)time { NSLog(@"pauseOnTime = %f",time); if (self.avPlayer) { [self seekToTime:time]; [self.avPlayer pause]; self.isPlaying = NO; } } #pragma mark - action event /* * 点击播放或暂停按钮,控制播放状态 */ - (void) onClickPlayOrPause{ if (self.isPlaying) { [_playButton setImage:[self getImageFromBundle:playerBundle imageName:@"player_play"] forState:UIControlStateNormal]; [self pauseOnTime:self.playheadTime]; }else{ [_playButton setImage:[self getImageFromBundle:playerBundle imageName:@"player_pause"] forState:UIControlStateNormal]; if ([self playerReachedEnd]) { [self playFromTime:0]; }else{ [self playFromTime:self.playheadTime]; } } } /* * 关闭按钮触发,销毁所有player和UI相关对象 */ - (void) onClickClose{ [self cancel]; } #pragma mark - add observer /* * 播放帧的观察者 */ - (void)addPlayerObserver { // add 应用置于后台、唤醒、屏幕旋转的监听器 (如果不需要此功能可以不添加) [self addAppObserver]; CMTime interval = CMTimeMakeWithSeconds(0.1, NSEC_PER_USEC); __weak ViewController *selfWeak = self; self.playTimeObserver = [self.avPlayer addPeriodicTimeObserverForInterval:interval queue:NULL usingBlock:^(CMTime time) { // 当前的播放时间 float currentTime = (float)CMTimeGetSeconds(time); // 刷新进度条以及时间Label的显示 [selfWeak refreshSlide:currentTime]; }]; } - (void)addPlayerItemObserver{ // 监测播放的状态(已就绪 / 失败 ) [self.avPlayerItem addObserver:self forKeyPath:XAdVideoStatusKey options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.avPlayerItem]; } - (void)addAppObserver{ [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; // 应用从后台唤醒 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; // 应用置于后台 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidEnterBackground:) name:UIApplicationWillResignActiveNotification object:nil]; // 屏幕旋转 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil]; } #pragma mark - remove observer - (void)removePlayerObserver{ [self removeAppObserver]; if (self.avPlayer && self.playTimeObserver) { [self.avPlayer removeTimeObserver:self.playTimeObserver]; } } - (void)removePlayerItemObserver{ [self.avPlayerItem removeObserver:self forKeyPath:XAdVideoStatusKey context:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:self.avPlayerItem]; } - (void)removeAppObserver{ [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; } #pragma mark - observer & notification - (void)appDidBecomeActive:(NSNotification *)notification { //后台唤醒继续播放 [self playFromTime:self.playheadTime]; } - (void)appDidEnterBackground:(NSNotification *)notification { //置于后台停止播放 [self pauseOnTime:self.playheadTime]; } - (void)orientationChanged:(NSNotification *)notification{ //屏幕旋转重新布局UI [self reloadVideoContainerView]; } /* * 监测player的状态 */ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == self.avPlayerItem && [keyPath isEqualToString:XAdVideoStatusKey]) { if (self.avPlayerItem.status == AVPlayerItemStatusFailed) { NSLog(@"AVPlayerItemStatusFailed."); } else if (self.avPlayerItem.status == AVPlayerItemStatusReadyToPlay) { if (self.videoContainer) { self.avPlayerLayer.frame = [self.videoContainer bounds]; NSLog(@"add playerLayer to video container."); [self.videoContainer.layer insertSublayer:self.avPlayerLayer atIndex:0]; [self play]; } } } } - (void) playerItemDidReachEnd:(NSNotification *)notification{ // 播放结束,改变播放暂停按钮的状态 [self changePlayOrPauseUI]; } /* * 刷新进度条以及相关UI */ - (void)refreshSlide:(float) time{ CMTime duration = self.avPlayer.currentItem.asset.duration; self.playheadTime = time; int currentSeconds = (int)(self.playheadTime); int fullDuration = (int)(duration.value/duration.timescale); self.progress.minimumValue = 0; self.progress.maximumValue = fullDuration; // 刷新进度条的进度 self.progress.value = currentSeconds; // 刷新时间的显示 NSMutableAttributedString *startLabelText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%02d:%02d",currentSeconds/60,currentSeconds%60]]; NSMutableAttributedString *endLabelText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%02d:%02d",fullDuration/60,fullDuration%60]]; [startLabelText addAttribute:NSForegroundColorAttributeName value:[UIColor whiteColor] range:NSMakeRange(0,startLabelText.length)]; [endLabelText addAttribute:NSForegroundColorAttributeName value:[UIColor whiteColor] range:NSMakeRange(0,endLabelText.length)]; self.startLabel.attributedText = startLabelText; self.endLabel.attributedText = endLabelText; } /* * 手动滑动进度条继续播放或者暂停 */ - (void) progressChanged{ if ([self isPlaying]) { // 如果之前是播放状态,那么继续播放 [self playFromTime:self.progress.value]; }else{ // 如果之前是是暂停状态,那么保持暂停 [self pauseOnTime:self.progress.value]; } } /* * 进度条被拖拽,改变播放暂停按钮的状态 */ - (void) progressDragInside{ NSLog(@"progressDragInside value:%f",self.progress.value); [_playButton setImage:[self getImageFromBundle:playerBundle imageName:@"player_play"] forState:UIControlStateNormal]; } /* * 进度条被按住,改变播放暂停按钮的状态 */ - (void) progressTouchDown{ NSLog(@"progressTouchDown value:%f",self.progress.value); [_playButton setImage:[self getImageFromBundle:playerBundle imageName:@"player_play"] forState:UIControlStateNormal]; } /* * 进度条按住后松开,改变播放暂停按钮的状态 */ - (void)progressTouchUpInside{ NSLog(@"progressTouchUpInside value:%f",self.progress.value); if ([self isPlaying]) { [_playButton setImage:[self getImageFromBundle:playerBundle imageName:@"player_pause"] forState:UIControlStateNormal]; }else{ [_playButton setImage:[self getImageFromBundle:playerBundle imageName:@"player_play"] forState:UIControlStateNormal]; } } #pragma mark - private /* * 屏幕旋转后刷新frame的大小 */ - (void)reloadVideoContainerView{ CGPoint currentOrigin = [UIScreen mainScreen].applicationFrame.origin; CGSize currentSize = [self getScreenSize]; CGRect currentScreenFrame = CGRectMake(currentOrigin.x, currentOrigin.y, currentSize.width, currentSize.height); self.videoContainer.frame = currentScreenFrame; self.avPlayerLayer.frame = self.videoContainer.bounds; } /* * 进度条相关UI的布局(如果无此需求可忽略,但是在初始化时的CGRect要设置好) * 布局顺序:在app的底部 ==> 播放/暂停按钮、播放时间、进度条、播放时长 * 在app的左上角 ==> 关闭按钮 */ - (void)addConstraint{ _closeButton.translatesAutoresizingMaskIntoConstraints = NO; _progress.translatesAutoresizingMaskIntoConstraints = NO; _playButton.translatesAutoresizingMaskIntoConstraints = NO; _startLabel.translatesAutoresizingMaskIntoConstraints = NO; _endLabel.translatesAutoresizingMaskIntoConstraints = NO; // Align play button left to video container NSLayoutConstraint* playbuttonLeftConstraint = [NSLayoutConstraint constraintWithItem:_playButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.videoContainer attribute:NSLayoutAttributeLeading multiplier:1.0f constant:10.0f]; // Align play button buttom to video container NSLayoutConstraint* playbuttonButtomConstraint = [NSLayoutConstraint constraintWithItem:_playButton attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.videoContainer attribute:NSLayoutAttributeBottom multiplier:1.0f constant: -5.0f]; // Align start label left to play button NSLayoutConstraint* startLabelLeftConstraint = [NSLayoutConstraint constraintWithItem:_startLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.playButton attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:10.0f]; // Align start label centerY to video container NSLayoutConstraint* startLabelCenterYConstraint = [NSLayoutConstraint constraintWithItem:_startLabel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.progress attribute:NSLayoutAttributeCenterY multiplier:1.0f constant:0.0f]; // Align progress slider left to start label NSLayoutConstraint* progressLeftConstraint = [NSLayoutConstraint constraintWithItem:_progress attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.startLabel attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:10.0f]; // Align progress slider right to end label NSLayoutConstraint* progressRightConstraint = [NSLayoutConstraint constraintWithItem:_progress attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.endLabel attribute:NSLayoutAttributeLeading multiplier:1.0f constant:-10.0f]; // Align progress centerY to play button NSLayoutConstraint* progressCenterYConstraint = [NSLayoutConstraint constraintWithItem:_progress attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.playButton attribute:NSLayoutAttributeCenterY multiplier:1.0f constant:0.0f]; // Align end label right to video contrainer NSLayoutConstraint* endLabelRightConstraint = [NSLayoutConstraint constraintWithItem:_endLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.videoContainer attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:-10.0f]; // Align end label centerY to video container NSLayoutConstraint* endLabelCenterYConstraint = [NSLayoutConstraint constraintWithItem:_endLabel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.progress attribute:NSLayoutAttributeCenterY multiplier:1.0f constant:0.0f]; // Align close button left to video container NSLayoutConstraint* closebuttonLeftConstraint = [NSLayoutConstraint constraintWithItem:_closeButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.videoContainer attribute:NSLayoutAttributeLeading multiplier:1.0f constant:3.0f]; // Align close button top to video container NSLayoutConstraint* closebuttonTopConstraint = [NSLayoutConstraint constraintWithItem:_closeButton attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.videoContainer attribute:NSLayoutAttributeTop multiplier:1.0f constant:3.0f]; [self.videoContainer addConstraints:@[playbuttonLeftConstraint, startLabelLeftConstraint, progressLeftConstraint, endLabelRightConstraint,progressRightConstraint,playbuttonButtomConstraint,closebuttonLeftConstraint,closebuttonTopConstraint,playbuttonButtomConstraint,startLabelCenterYConstraint,endLabelCenterYConstraint,progressCenterYConstraint]]; } /* * 获取屏幕大小,IOS8以后屏幕宽高是真实宽高,IOS8之前需要翻转 */ - (CGSize) getScreenSize{ CGSize size; CGSize screenSize = [UIScreen mainScreen].applicationFrame.size; if ([self isLaterThanIOS8]) { size = screenSize; NSLog(@"ios8 or ios9, width = %f,height = %f",size.width,size.height); }else{ if ( UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) { // Landscape Orientation, reverse size values size.width = screenSize.height; size.height = screenSize.width; } else { // portrait orientation, use normal size values size.width = screenSize.width; size.height = screenSize.height; } NSLog(@" not ios8 or ios9, width = %f,height = %f",size.width,size.height); } return size; } /* * 初始化avplayerItem、avplayer、avplayerLayer */ - (void)setupPlayerWithURL:(NSURL *)URL { // dispatch_async(dispatch_get_global_queue(0, 0), ^{ self.avPlayerItem = [AVPlayerItem playerItemWithURL:URL]; [self addPlayerItemObserver]; self.avPlayer = [AVPlayer playerWithPlayerItem:self.avPlayerItem]; [self addPlayerObserver]; self.avPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:self.avPlayer]; [self.avPlayerLayer setVideoGravity:AVLayerVideoGravityResizeAspect]; self.avPlayerLayer.needsDisplayOnBoundsChange = YES; // }); } - (void) showUI{ [self addConstraint]; [self.view addSubview:self.videoContainer]; [self.videoContainer addSubview:self.progress]; [self.videoContainer addSubview:self.startLabel]; [self.videoContainer addSubview:self.endLabel]; [self.videoContainer addSubview:self.playButton]; [self.videoContainer addSubview:self.closeButton]; } - (void)setProgressRefProperties{ // 获取不包括状态栏的屏幕frame CGRect appViewFrame = [ UIScreen mainScreen ].applicationFrame; _videoContainer = [[UIView alloc]initWithFrame:appViewFrame]; self.videoContainer.backgroundColor = [UIColor blackColor]; // 初始化进度条(尺寸没有生效,因为后面用了布局约束) _progress = [[UISlider alloc] initWithFrame:CGRectMake(0, 0, 100, 5)]; // 设置进度条播放按钮的图片 [_progress setThumbImage:[self getImageFromBundle:playerBundle imageName:@"player_dot"] forState:UIControlStateNormal]; [_progress setThumbImage:[self getImageFromBundle:playerBundle imageName:@"player_dot"] forState:UIControlStateHighlighted]; // 设置进度条已播放和未播放部分的颜色 [_progress setMinimumTrackTintColor:[UIColor whiteColor]]; [_progress setMaximumTrackTintColor:[UIColor darkGrayColor]]; // 设置进度条的事件函数 [self.progress addTarget:self action:@selector(progressChanged) forControlEvents:(UIControlEventValueChanged)]; [self.progress addTarget:self action:@selector(progressDragInside) forControlEvents:(UIControlEventTouchDragInside)]; [self.progress addTarget:self action:@selector(progressTouchDown) forControlEvents:(UIControlEventTouchDown)]; [self.progress addTarget:self action:@selector(progressTouchUpInside) forControlEvents:(UIControlEventTouchUpInside)]; // 初始化进度条(尺寸没有生效,因为后面用了布局约束) _startLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 50, 15)]; _endLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 50, 15)]; // 设置时间Lable字体样式和颜色 NSMutableAttributedString *startLabelText = [[NSMutableAttributedString alloc] initWithString:@"00:00"]; NSMutableAttributedString *endLabelText = [[NSMutableAttributedString alloc] initWithString:@"00:00"]; [startLabelText addAttribute:NSForegroundColorAttributeName value:[UIColor whiteColor] range:NSMakeRange(0,startLabelText.length)]; [endLabelText addAttribute:NSForegroundColorAttributeName value:[UIColor whiteColor] range:NSMakeRange(0,endLabelText.length)]; self.startLabel.attributedText = startLabelText; self.endLabel.attributedText = endLabelText; //初始化播放暂停按钮,设置图片 _playButton = [[UIButton alloc] initWithFrame:CGRectMake(10, 50, 20, 20)]; [_playButton setImage:[self getImageFromBundle:playerBundle imageName:@"player_pause"] forState:UIControlStateNormal]; [_playButton addTarget:self action:@selector(onClickPlayOrPause) forControlEvents:UIControlEventTouchUpInside]; //初始化关闭按钮,设置相应图片 _closeButton = [[UIButton alloc]initWithFrame:CGRectMake(50, 50, 20, 20)]; [_closeButton setImage:[self getImageFromBundle:playerBundle imageName:@"player_close"] forState:UIControlStateNormal]; [_closeButton addTarget:self action:@selector(onClickClose) forControlEvents:UIControlEventTouchUpInside]; } /* * 改变播放或暂停按钮的状态 */ - (void) changePlayOrPauseUI{ if (self.isPlaying) { [_playButton setImage:[self getImageFromBundle:playerBundle imageName:@"player_play"] forState:UIControlStateNormal]; self.isPlaying = NO; }else{ [_playButton setImage:[self getImageFromBundle:playerBundle imageName:@"player_pause"] forState:UIControlStateNormal]; self.isPlaying = YES; } } /* * 判断播放是否结束 */ - (BOOL)playerReachedEnd { CMTime duration = self.avPlayer.currentItem.asset.duration; CMTime currentTime = self.avPlayer.currentItem.currentTime; // CMTime包含value和timescale两个属性,比值用来获取真正的播放时间 int currentSeconds = (int)(currentTime.value/currentTime.timescale); int fullDuration = (int)(duration.value/duration.timescale); return (currentSeconds == fullDuration) ? YES : NO; } /* * 跳到某一播放帧 */ - (void)seekToTime:(double)time { if (time >= 0) { CMTime timeStruct = CMTimeMake(time * 1000, 1000); [self.avPlayer seekToTime:timeStruct toleranceBefore:kCMTimeZero toleranceAfter:kCMTimePositiveInfinity]; } } /* * 获取资源包 */ - (void) initBundle{ NSString * bundlePath = [[ NSBundle mainBundle] pathForResource: @"playicon" ofType :@"bundle"]; playerBundle = [NSBundle bundleWithPath:bundlePath]; } /* * 获取资源包中的图片资源 */ - (UIImage *)getImageFromBundle:(NSBundle *) bundle imageName:(NSString *)imageName{ NSString *imagePath = [playerBundle pathForResource:imageName ofType:@"png"]; UIImage *image = [[UIImage alloc]init]; if (imagePath) { image = [UIImage imageWithContentsOfFile:imagePath]; } return image; } /* * 是否系统版本高于IOS8 */ - (BOOL) isLaterThanIOS8{ return ([[[UIDevice currentDevice] systemVersion] hasPrefix:@"8."] || [[[UIDevice currentDevice] systemVersion] hasPrefix:@"9."]); } - (void)destroyAllProperties{ _avPlayer = nil; _avPlayerItem = nil; _avPlayerLayer = nil; _progress = nil; _startLabel = nil; _endLabel = nil; _closeButton = nil; _playButton = nil; _playTimeObserver = nil; } - (void) removeUIView{ if (self.avPlayerLayer) { [self.avPlayerLayer removeFromSuperlayer]; } if (self.videoContainer) { [self.videoContainer removeFromSuperview]; } if (self.progress) { [self.progress removeFromSuperview]; } if (self.closeButton) { [self.closeButton removeFromSuperview]; } if (self.startLabel) { [self.startLabel removeFromSuperview]; } if (self.endLabel) { [self.endLabel removeFromSuperview]; } if (self.playButton) { [self.playButton removeFromSuperview]; } } - (void)dealloc { [self cancel]; } @end
源码地址:
https://github.com/MsSmart/VideoPlayerDemo.git
未解决的问题:
(1)视频播放进度与画面不同步
(2)播放的声音和画面不同步
这两个问题是偶尔出现的,猜测是苹果AVPlayer底层的原因导致,有人提出解决方案是监控音频进度,图像线程跟音频保持一致,不同步的时候等待。但是如何实现还在研究中,如果有哪位牛人解决过类似问题,还多多请指教。