#import "AGPlayerView.h" #import "NSString+time.h" #import "RotationScreen.h" #import "Masonry.h" #define RGBColor(r, g, b) [UIColor colorWithRed:r / 255.0 green:g / 255.0 blue:b / 255.0 alpha:1.0] static const NSTimeInterval kVideoPlayerAnimationTimeinterval = 0.3f; @interface AGPlayerView () { BOOL _isIntoBackground; // 是否在后台 BOOL _isShowToolbar; // 是否显示工具条 BOOL _isSliding; // 是否正在滑动 AVPlayerItem *_playerItem; AVPlayerLayer *_playerLayer; NSTimer *_timer; id _playTimeObserver; // 观察者 CGRect _originFrame; } @property (weak, nonatomic) IBOutlet UIView *mainView; @property (weak, nonatomic) IBOutlet UIView *playerView; @property (weak, nonatomic) IBOutlet UIView *topView; @property (weak, nonatomic) IBOutlet UIButton *moreButton; @property (weak, nonatomic) IBOutlet UIView *downView; @property (weak, nonatomic) IBOutlet UIButton *playButton; @property (weak, nonatomic) IBOutlet UILabel *beginLabel; @property (weak, nonatomic) IBOutlet UILabel *endLabel; @property (weak, nonatomic) IBOutlet UISlider *playProgress; @property (weak, nonatomic) IBOutlet UIProgressView *loadedProgress; // 缓冲进度条 @property (weak, nonatomic) IBOutlet UIButton *rotationButton; @property (weak, nonatomic) IBOutlet UIButton *playerButton; @property (weak, nonatomic) IBOutlet UIButton *playerFullScreenButton; @property (weak, nonatomic) IBOutlet UIView *inspectorView; // 继续播放/暂停播放 @property (weak, nonatomic) IBOutlet UILabel *inspectorLabel; // // 约束动画 @property (weak, nonatomic) IBOutlet NSLayoutConstraint *topViewTop; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *downViewBottom; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *inspectorViewHeight; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *playProgressWidth; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *playerViewLeading; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *playerViewTrailing; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *playerViewAspect; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *backLeading; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *playLeading; @end @implementation AGPlayerView - (void)removeObserveAndNOtification { [_player replaceCurrentItemWithPlayerItem:nil]; [_playerItem removeObserver:self forKeyPath:@"status"]; [_playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"]; [_player removeTimeObserver:_playTimeObserver]; _playTimeObserver = nil; [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (instancetype)initWithFrame:(CGRect)frame{ self = [super initWithFrame:frame]; if (self) { //o 这里AGPlayerView。xib 实际是self。mainView。而不是self。改的别人代码有点乱(以后找时间重写吧,xib上面好多多余的元素,而且横屏机制不一样) // set self.mainView = [[[NSBundle mainBundle] loadNibNamed:@"AGPlayerView" owner:self options:nil] lastObject]; self.mainView.frame = self.bounds; [self addSubview:self.mainView]; self.playProgressWidth.constant = kSize.width - 200; // self.topViewTop.constant = _isLandscape ? 0 : (kStatusHeight-20); // setAVPlayer self.player = [[AVPlayer alloc] init]; _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; [self.playerView.layer addSublayer:_playerLayer]; // bringFront [self.playerView bringSubviewToFront:_topView]; [self.playerView bringSubviewToFront:_downView]; [self.playerView bringSubviewToFront:_playerButton]; [self.playerView bringSubviewToFront:_playProgress]; // [self.playerView sendSubviewToBack:_inspectorView]; // setPortraintLayout [self setPortarintLayout]; // slider self.playProgress.value = 0.0; [self.playProgress setThumbImage:[UIImage imageNamed:@"icmpv_thumb_light"] forState:(UIControlStateNormal)]; // 设置progress self.loadedProgress.progress = 0.0; // inspectorBackgroundColor self.inspectorView.backgroundColor = [RGBColor(203, 201, 204) colorWithAlphaComponent:0.5]; // 不影响子视图的透明度 } return self; } #pragma mark- #pragma mark 横竖屏约束 - (void)setPortarintLayout { _isLandscape = NO; // 不隐藏工具条 [self portraitShow]; // hideInspector self.inspectorViewHeight.constant = 0.0f; [self layoutIfNeeded]; } // 显示工具条 - (void)portraitShow { _isShowToolbar = YES; // 显示工具条置为 yes // 约束动画 // self.topViewTop.constant = _isLandscape ? 0 : (kStatusHeight-20); self.downViewBottom.constant = 0; [UIView animateWithDuration:0.1 animations:^{ [self layoutIfNeeded]; self.topView.alpha = self.downView.alpha = 1; self.playerButton.alpha = self.playerFullScreenButton.alpha = 1; } completion:^(BOOL finished) { }]; // 显示状态条 (暂时找不到替代方法,然后运行在ios11上又没有问题。所以先这样吧。。) [[UIApplication sharedApplication] setStatusBarHidden:NO animated:YES]; [[UIApplication sharedApplication] setStatusBarStyle:(UIStatusBarStyleLightContent)]; } - (void)portraitHide { _isShowToolbar = NO; // 显示工具条置为 no // 约束动画 // self.topViewTop.constant = -(_isLandscape ? 0 : (kStatusHeight-20)+self.topView.frame.size.height); self.downViewBottom.constant = -(self.downView.frame.size.height); [UIView animateWithDuration:0.1 animations:^{ [self layoutIfNeeded]; self.topView.alpha = self.downView.alpha = 0; self.playerButton.alpha = self.playerFullScreenButton.alpha = 0; } completion:^(BOOL finished) { }]; // 显示状态条 [[UIApplication sharedApplication] setStatusBarHidden:YES animated:YES]; [[UIApplication sharedApplication] setStatusBarStyle:(UIStatusBarStyleLightContent)]; } #pragma mark- #pragma mark inspectorView 动画 - (void)inspectorViewShow { // [self.inspectorView.layer removeAllAnimations]; // 更改文字 if (_isPlaying) { self.inspectorLabel.text = @"继续播放"; } else { self.inspectorLabel.text = @"暂停播放"; } // 约束动画 self.inspectorViewHeight.constant = 20.0f; [UIView animateWithDuration:0.3 animations:^{ [self layoutIfNeeded]; } completion:^(BOOL finished) { [self performSelector:@selector(inspectorViewHide) withObject:nil afterDelay:1]; // 0.2秒后隐藏 }]; } - (void)inspectorViewHide { self.inspectorViewHeight.constant = 0.0f; [UIView animateWithDuration:0.3 animations:^{ [self layoutIfNeeded]; } completion:^(BOOL finished) { }]; } - (void)layoutSubviews { [super layoutSubviews]; _playerLayer.frame = self.bounds; } #pragma mark- sizeClass 横竖屏约束 // sizeClass 横竖屏切换时,执行 - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { [super traitCollectionDidChange:previousTraitCollection]; // 横竖屏切换时重新添加约束 CGRect bounds = self.bounds; [_mainView mas_remakeConstraints:^(MASConstraintMaker *make) { make.top.left.equalTo(@(0)); make.width.equalTo(@(bounds.size.width)); make.height.equalTo(@(bounds.size.height)); }]; // 横竖屏判断 if (self.traitCollection.verticalSizeClass != UIUserInterfaceSizeClassCompact) { // 竖屏 self.downView.backgroundColor = self.topView.backgroundColor = [UIColor clearColor]; [self.rotationButton setImage:[UIImage imageNamed:@"player_fullScreen_iphone"] forState:(UIControlStateNormal)]; } else { // 横屏 self.downView.backgroundColor = self.topView.backgroundColor = RGBColor(89, 87, 90); [self.rotationButton setImage:[UIImage imageNamed:@"player_window_iphone"] forState:(UIControlStateNormal)]; } // iPhone 6s 6 6sP 6p // 竖屏情况下 compact * regular compact * regular // 横屏情况下 compact * compact regular * compact // 以 verticalClass 来判断横竖屏 // NSLog(@"horizontal %ld", (long)self.traitCollection.horizontalSizeClass); // NSLog(@"vertical %ld", (long)self.traitCollection.verticalSizeClass); // } #pragma mark- #pragma mark 横竖屏切换 - (IBAction)rotationAction:(id)sender { if (_isLandscape) { // 如果是横屏, [RotationScreen forceOrientation:(UIInterfaceOrientationPortrait)]; // 切换为竖屏 } else { [RotationScreen forceOrientation:(UIInterfaceOrientationLandscapeRight)]; // 否则,切换为横屏 } //1点击-调屏幕转变调私有接口setOrientation,实际上因为没有配置general landscapeLeft/right 所以我们看见的屏幕不会变化。 //2但是监听屏幕转变的方法又检测到屏幕方向变化。所以最后是在orientationHandler里面改变的我们看见的屏幕方向 //3orientationHandler -> setIsLandscape //以上2,3当手机手动横置的时候也会触发 不过我没有细分左/右,只让他home在左了 } /** * 屏幕旋转处理 */ - (void)orientationHandler { if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) { self.isLandscape = YES; }else if(UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation)) { self.isLandscape = NO; }else{ //NSLog(@"我就看看有木有第三种"); } } - (void)setIsLandscape:(BOOL)isLandscape { if (_isLandscape == isLandscape) { return; } _isLandscape = isLandscape; if (isLandscape) { _originFrame = self.frame; CGFloat height = kSize.width; CGFloat width = kSize.height; CGRect frame = CGRectMake((height - width) / 2, (width - height) / 2, width, height); if (is_iPhoneX) { self.playerViewLeading.constant = kStatusHeight; self.playerViewAspect.constant = kSize.width-(kSize.height-kStatusHeight-kSafeAreaBottomHeight)*14/25; }else{ self.playerViewLeading.constant = 0; self.playLeading.constant = 40; self.backLeading.constant = 40; self.playerViewAspect.constant = kSize.width-(kSize.height-kSafeAreaBottomHeight)*14/25; } self.playerViewTrailing.constant = kSafeAreaBottomHeight; //以下2种方式无效,更新self.playerView约束 // mas_remakeConstraints/mas_updateConstraints wuxiao self.playProgressWidth.constant = kSize.height - 200 - kStatusHeight-kSafeAreaBottomHeight;; [UIView animateWithDuration:kVideoPlayerAnimationTimeinterval animations:^{ self.frame = frame; self.transform = CGAffineTransformMakeRotation(M_PI_2); [self.mainView mas_remakeConstraints:^(MASConstraintMaker *make) { make.top.left.equalTo(@(0)); make.width.equalTo(@(kSize.height)); make.height.equalTo(@(kSize.width)); }]; [self layoutIfNeeded]; self.downView.backgroundColor = self.topView.backgroundColor = RGBColor(89, 87, 90); [self.rotationButton setImage:[UIImage imageNamed:@"player_window_iphone"] forState:(UIControlStateNormal)]; } completion:^(BOOL finished) {}]; }else { self.playLeading.constant = 12; self.backLeading.constant = 12; self.playerViewLeading.constant = 0; self.playerViewTrailing.constant = 0; self.playerViewAspect.constant = 0; self.playProgressWidth.constant = kSize.width - 200;; [UIView animateWithDuration:kVideoPlayerAnimationTimeinterval animations:^{ self.transform = CGAffineTransformIdentity; self.frame = _originFrame; [self.mainView mas_remakeConstraints:^(MASConstraintMaker *make) { make.top.left.equalTo(@(0)); make.width.equalTo(@(kSize.width)); make.height.equalTo(@(self.bounds.size.height)); }]; [self layoutIfNeeded]; self.downView.backgroundColor = self.topView.backgroundColor = [UIColor clearColor]; [self.rotationButton setImage:[UIImage imageNamed:@"player_fullScreen_iphone"] forState:(UIControlStateNormal)]; } completion:^(BOOL finished) {}]; } } #pragma mark - 初始化playerItem - (void)updatePlayerWithURL:(NSURL *)url { NSLog(@"updatePlayerWithURL: %@",url); // 创建要播放的资源 _playerItem = [[AVPlayerItem alloc]initWithURL:url]; // 播放当前资源 [self.player replaceCurrentItemWithPlayerItem:_playerItem]; [self addObserverAndNotification]; // 添加观察者,发布通知 } /** * 添加观察者 、通知 、监听播放进度 */ - (void)addObserverAndNotification { [_playerItem addObserver:self forKeyPath:@"status" options:(NSKeyValueObservingOptionNew) context:nil]; // 观察status属性, 一共有三种属性 [_playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil]; // 观察缓冲进度 [self monitoringPlayback:_playerItem]; // 监听播放 [self addNotification]; // 添加通知 } // 观察播放进度 - (void)monitoringPlayback:(AVPlayerItem *)item { __weak typeof(self)WeakSelf = self; // 播放进度, 每秒执行30次, CMTime 为30分之一秒 _playTimeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 30.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { if (_touchMode != TouchPlayerViewModeHorizontal) { // 当前播放秒 float currentPlayTime = (double)item.currentTime.value/ item.currentTime.timescale; // 更新slider, 如果正在滑动则不更新 if (_isSliding == NO) { [WeakSelf updateVideoSlider:currentPlayTime]; } } else { return; } }]; } // 更新滑动条 - (void)updateVideoSlider:(float)currentTime { self.playProgress.value = currentTime; self.beginLabel.text = [NSString convertTime:currentTime]; } #pragma mark- #pragma mark 添加通知 - (void)addNotification { //监听屏幕方向(这里没有在general里面配置landscape left/right 所以选择了这种方式) [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationHandler) name:UIDeviceOrientationDidChangeNotification object:nil]; // 播放完成通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; //程序到前台 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil]; //程序到后台 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil]; //程序挂起 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive) name:UIApplicationWillResignActiveNotification object:nil]; //程序复原 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; } - (void)playbackFinished:(NSNotification *)notification { NSLog(@"视频播放完成通知"); [self pause]; _playerItem = [notification object]; // 是否无限循环 [_playerItem seekToTime:kCMTimeZero]; // 跳转到初始 // [_player play]; // 是否无限循环 } /** * 程序到前台 */ - (void)applicationWillEnterForeground { NSLog(@"applicationWillEnterForeground-play"); [self play]; } /** * 程序进入后台 */ - (void)applicationDidEnterBackground { NSLog(@"applicationDidEnterBackground-pause"); [self pause]; } /** * 程序挂起 */ - (void)applicationWillResignActive { NSLog(@"applicationWillResignActive-pause"); [self pause]; } /** *程序复原 */ - (void)applicationDidBecomeActive { NSLog(@"applicationDidBecomeActive-play"); [self play]; } #pragma mark- #pragma mark KVO - status - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { AVPlayerItem *item = (AVPlayerItem *)object; if ([keyPath isEqualToString:@"status"]) { if (_isIntoBackground) { return; } else { // 判断status 的 状态 AVPlayerStatus status = [[change objectForKey:@"new"] intValue]; // 获取更改后的状态 if (status == AVPlayerStatusReadyToPlay) { NSLog(@"准备播放"); // CMTime 本身是一个结构体 CMTime duration = item.duration; // 获取视频长度 NSLog(@"%.2f", CMTimeGetSeconds(duration)); // 设置视频时间 [self setMaxDuration:CMTimeGetSeconds(duration)]; // 播放 [self play]; self.playButton.enabled = YES; self.playerButton.enabled = YES; self.playProgress.enabled = YES; } else if (status == AVPlayerStatusFailed) { NSLog(@"AVPlayerStatusFailed: %@",self.player.error.description); ShowMsg(@"播放失败,请退出重试"); } else { NSLog(@"AVPlayerStatusUnknown"); ShowMsg(@"未知错误"); } } } else if ([keyPath isEqualToString:@"loadedTimeRanges"]) { NSTimeInterval timeInterval = [self availableDurationRanges]; // 缓冲时间 CGFloat totalDuration = CMTimeGetSeconds(_playerItem.duration); // 总时间 [self.loadedProgress setProgress:timeInterval / totalDuration animated:YES]; } } // 设置最大时间 - (void)setMaxDuration:(CGFloat)duration { self.playProgress.maximumValue = duration; // maxValue = CMGetSecond(item.duration) self.endLabel.text = [NSString convertTime:duration]; } // 已缓冲进度 - (NSTimeInterval)availableDurationRanges { NSArray *loadedTimeRanges = [_playerItem loadedTimeRanges]; // 获取item的缓冲数组 // discussion Returns an NSArray of NSValues containing CMTimeRanges // CMTimeRange 结构体 start duration 表示起始位置 和 持续时间 CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue]; // 获取缓冲区域 float startSeconds = CMTimeGetSeconds(timeRange.start); float durationSeconds = CMTimeGetSeconds(timeRange.duration); NSTimeInterval result = startSeconds + durationSeconds; // 计算总缓冲时间 = start + duration return result; } #pragma mark - 返回 - (IBAction)goBackAction:(id)sender { if (self.goBackBlock) { self.goBackBlock(); } } #pragma mark- #pragma mark 播放 暂停 - (IBAction)playOrStopAction:(id)sender { if (_isPlaying) { [self pause]; } else { [self play]; } // inspectorAnimation [self inspectorViewShow]; } - (void)play { NSLog(@"lee-play"); _isPlaying = YES; [_player play]; // 调用avplayer 的play方法 [self.playButton setImage:[UIImage imageNamed:@"Stop"] forState:(UIControlStateNormal)]; [self.playerButton setImage:[UIImage imageNamed:@"player_pause_iphone_window"] forState:(UIControlStateNormal)]; [self.playerFullScreenButton setImage:[UIImage imageNamed:@"player_pause_iphone_fullscreen"] forState:(UIControlStateNormal)]; } - (void)pause { NSLog(@"lee-pause"); _isPlaying = NO; [_player pause]; [self.playButton setImage:[UIImage imageNamed:@"Play"] forState:(UIControlStateNormal)]; [self.playerButton setImage:[UIImage imageNamed:@"player_start_iphone_window"] forState:(UIControlStateNormal)]; [self.playerFullScreenButton setImage:[UIImage imageNamed:@"player_start_iphone_fullscreen"] forState:(UIControlStateNormal)]; } #pragma mark- #pragma mark 处理点击事件 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { _touchMode = TouchPlayerViewModeNone; } // - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (_touchMode == TouchPlayerViewModeNone) { if (_isLandscape) { // 如果当前是横屏 if (_isShowToolbar) { [self portraitHide]; } else { [self portraitShow]; } } else { // 如果是竖屏 if (_isShowToolbar) { [self portraitHide]; } else { [self portraitShow]; } } } } // - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { } #pragma mark- #pragma mark 滑块事件 - (IBAction)playerSliderTouchDown:(id)sender { [self pause]; } - (IBAction)playerSliderTouchUpInside:(id)sender { _isSliding = NO; // 滑动结束 [self play]; } // 不要拖拽的时候改变, 手指抬起来后缓冲完成再改变 - (IBAction)playerSliderValueChanged:(id)sender { _isSliding = YES; [self pause]; CMTime changedTime = CMTimeMakeWithSeconds(self.playProgress.value, 1.0); // NSLog(@"%.2f", self.playProgress.value); [_playerItem seekToTime:changedTime completionHandler:^(BOOL finished) { // 跳转完成后做某事 }]; } @end