iOS开发 - 用AVPlayer封装一个播放器

简介: iOS开发 - 用AVPlayer封装一个播放器

因为GIF上传的时候一直失败,所以大家到Github查看效果图

https://github.com/codeliu6572/LHHVideoPlayer

由于亮度和音量只能在真机上显现效果,所以GIF未给出操作,其

中快进快退区域为当前播放器宽度的一半,高度为播放器高度,

音量和亮度上下滑动区域总和为剩下的一半区域,且二者大小一样。


播放器基本功能:


1、视频播放和缓存(不支持流播放);

2、双击屏幕全屏和竖屏;

3、快进快退;

4、滑动调节屏幕亮度和系统声音;

5、全屏按钮和返回按钮;

6、进度条拖动和点击定位播放;

7、完美适配横竖屏;

8、打断机制和监听机制;

9、dealloc;

10、总结;


1.视频播放和缓存(不支持流播放);


创建播放器:

  // 3.playerItem关联创建player
    self.player = [AVPlayer playerWithPlayerItem:self.playItem];
    self.playerView = [[LHHPlayerView alloc]initWithFrame:CGRectMake(0, 0, VIEWWIDTH, 240)];
    // 4.player关联创建playerView
    // 4.player关联创建playerView
    [self.playerView setPlayer:self.player];
    [self.playerView.layer setBackgroundColor:[UIColor blackColor].CGColor];
    //添加播放视图到self.view
    [self.view addSubview:self.playerView];

为了能够在播放器的表层加上控件(AVPlayer原生不能往上加控件无效且没有交互属性),需要对它进行一个小操作:

#import <UIKit/UIKit.h>
@class AVPlayer;
@interface LHHPlayerView : UIView
@property (nonatomic, strong) AVPlayer *player;
@end
#import "LHHPlayerView.h"
#import <AVFoundation/AVFoundation.h>
@interface LHHPlayerView ()
@end
@implementation LHHPlayerView
// 为了使PlayerView的layer为AVPlayerLayer类型
+ (Class)layerClass {
    return [AVPlayerLayer class];
}
- (AVPlayer *)player {
    return [(AVPlayerLayer *)[self layer] player];
}
- (void)setPlayer:(AVPlayer *)player {
    [(AVPlayerLayer *)[self layer] setPlayer:player];
}
- (void)dealloc
{
    self.player = nil;
}
@end

然后用这个处理过的View作为AVPlayerLayer,此时可以往上增加控件。关于缓存,系统的播放器对于http连接的视频是会自动缓存的,不需要我们做什么操作,不妨下载Demo断网试一试。


2.双击屏幕全屏和竖屏;


这里的横竖屏机制请看这篇博客: http://blog.csdn.net/codingfire/article/details/50387774 按照上面博客的配置好之后,需要设置如下代码:

#pragma mark - 屏幕方向
// 允许自动旋转,在支持的屏幕中设置了允许旋转的屏幕方向。
- (BOOL)shouldAutorotate
{
    return YES;
}
// 支持的屏幕方向,这个方法返回 UIInterfaceOrientationMaskAllButUpsideDown 类型的值。
- (NSUInteger)supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskAllButUpsideDown;
}
// 视图展示的时候优先展示为 竖屏,可更改
-(UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
    return UIInterfaceOrientationPortrait;
}

博主这里屏幕给了小屏,没有直接全屏,按自己需求更改播放器的播放界面frame,同时在横竖屏切换时要适时的刷新界面上UI:

根据重力感应手机方向来切换屏幕需要用到这个方法:
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation duration:(NSTimeInterval)duration {
    switch (interfaceOrientation) {
        case UIInterfaceOrientationPortrait:
            //home健在下
            self.playerView.frame = CGRectMake(0, 0, VIEWWIDTH, 220);
            _bigView = NO;
            break;
        case UIInterfaceOrientationPortraitUpsideDown:
            //home健在上
            break;
        case UIInterfaceOrientationLandscapeLeft:
            //home健在左
            self.playerView.frame = CGRectMake(0, 0, VIEWWIDTH, VIEWHEIGTH);
            _bigView = YES;
            break;
        case UIInterfaceOrientationLandscapeRight:
            //home健在右
            self.playerView.frame = CGRectMake(0, 0, VIEWWIDTH, VIEWHEIGTH);
            _bigView = YES;
            break;
        default:
            break;
    }
    [self layoutUI];
}

下面是通过双击屏幕和全屏按钮来控制横竖屏(也可根据需要适时退出界面):

#pragma mark - backAction 返回按钮功能
- (void)backBtnAction
{
    if (_bigView == YES) {
        [[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIDeviceOrientationPortrait]forKey:@"orientation"];
        self.playerView.frame = CGRectMake(0, 0, VIEWWIDTH, 220);
        _bigView = NO;
        [self layoutUI];
    }
    else
    {
        AppDelegate *appdelegate=(AppDelegate *)[UIApplication sharedApplication].delegate;
        appdelegate.allowRotation=NO;
        [self.player pause];
        [self.playerView removeFromSuperview];
        [self.navigationController popViewControllerAnimated:YES];
    }
}
#pragma mark - bigViewBtnAction
- (void)bigViewBtnAction  全屏按钮功能
{
    if (_bigView == NO) {
        self.playerView.frame = CGRectMake(0, 0, VIEWWIDTH, VIEWHEIGTH);
        [[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIDeviceOrientationLandscapeLeft]forKey:@"orientation"];
        _bigView = YES;
    }
    else
    {
        self.playerView.frame = CGRectMake(0, 0, VIEWWIDTH, 220);
        [[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIDeviceOrientationPortrait]forKey:@"orientation"];
        _bigView = NO;
    }
    [self layoutUI];
}

改变UI其实不需要改变什么,就是刷新下控件的位置,淫秽横竖屏宽高交换原则,相当于重新给一个frame,原值保持不变,其中的变量随着播放器frame改变而改变:

#pragma mark layoutUI
- (void)layoutUI
{
    _headerView.frame = CGRectMake(0 ,0, VIEWWIDTH, 40);
    _bottomView.frame = CGRectMake(0 ,self.playerView.bounds.size.height - 50, VIEWWIDTH, 50);
    _bigViewBtn.frame = CGRectMake(_headerView.bounds.size.width - 44 ,0, 44, 40);
    _progressView.frame = CGRectMake(45, 21, VIEWWIDTH - 60 - 80, 10);
    _currentTimeLabel.frame = CGRectMake(VIEWWIDTH - 90, 5, 45, 40);
    _remainTimeLabel.frame = CGRectMake(VIEWWIDTH - 45, 5, 45, 40);
    _voiceLight.frame = self.playerView.bounds;
}


3.快进快退;

#pragma mark 快退
- (void)fastBackward {
    [self cancelPerformSelector:@selector(hideHeaderViewAndBottomView)];
    [self progressAdd:-OFFSET];
    [self delayHideHeaderViewAndBottomView];
}
#pragma mark 快进
- (void)fastForward {
    [self cancelPerformSelector:@selector(hideHeaderViewAndBottomView)];
    [self progressAdd:OFFSET];
    [self delayHideHeaderViewAndBottomView];
}
- (void)progressAdd:(CGFloat)step {
    // 如果正在播放先暂停播放(但是不改变_playing的值为NO,因为快进或快退完成后要根据_playing来判断是否要继续播放),再进行播放定位
    if (_isPlaying) {
        [self.player pause];
    }
    Float64 currentSecond = CMTimeGetSeconds(self.player.currentTime); // 当前秒
    Float64 totalSeconds = CMTimeGetSeconds(self.duration); // 总时间
    CMTime dstTime; // 目标时间
    if (currentSecond + step >= totalSeconds) {
        dstTime = CMTimeSubtract(self.duration, CMTimeMakeWithSeconds(1, self.duration.timescale));
        self.progressView.value = dstTime.value / self.duration.value;
    } else if (currentSecond + step < 0.0) {
        dstTime = kCMTimeZero;
        self.progressView.value = 0.0;
    } else {
        dstTime = CMTimeMakeWithSeconds(currentSecond + step, self.player.currentTime.timescale);
        self.progressView.value += step / CMTimeGetSeconds(self.duration);
    }
    [self seekToCMTime:dstTime progress:self.progressView.value];
    if (_isPlaying) {
        [self.player play];
    }
}

利用Swipe滑动手势来设定播放节点,找到播放位置,手势和控制屏幕亮度封装在了一起,通过一个代理来调用。


4.滑动调节屏幕亮度和系统声音;


关于这个,请查看下面这篇博客: http://blog.csdn.net/codingfire/article/details/53810649 里面详细说明了怎么通过滑动来控制系统声音和屏幕亮度,并有Demo供参考。


5.全屏按钮和返回按钮;


这里的功能上面已经给出,需要注意的就是因为没有使用约束,所以每次横竖屏切换都要用方法自动或者手动来刷新界面控件的位置甚至图片,在代码中可以查看。


6.进度条拖动和点击定位播放;


进度条用slider来完成,自定义UI,通过拖动的位置value属性和视频总时间判断当前播放时间和进度条位置,slider增加点击手势,通过点击位置瞬间定位到所点击的位置播放:

 //progressView初始化
    _progressView = [[UISlider alloc]initWithFrame:CGRectMake(45, 21, VIEWWIDTH - 60 - 80, 10)];
    [_progressView addTarget:self action:@selector(slidingProgress:) forControlEvents:UIControlEventValueChanged];
    [_progressView addTarget:self action:@selector(slidingEnded:) forControlEvents:UIControlEventTouchUpInside];
    [_progressView addTarget:self action:@selector(slidingEnded:) forControlEvents:UIControlEventTouchUpOutside];
    [_bottomView addSubview:_progressView];
//进度条UI定制
  [self.progressView setMinimumTrackImage:[[UIImage imageNamed:@"video_num_front.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 5) resizingMode:UIImageResizingModeStretch] forState:UIControlStateNormal];
    [self.progressView setMaximumTrackImage:[[UIImage imageNamed:@"video_num_bg.png"]  resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 5) resizingMode:UIImageResizingModeStretch] forState:UIControlStateNormal];
    [self.progressView setThumbImage:[UIImage imageNamed:@"progressThumb.png"] forState:UIControlStateNormal];
#pragma mark - 拖动进度条改变播放点(playhead)
// valueChanged
- (void)slidingProgress:(UISlider *)slider {
    // 取消调用hideHeaderViewAndBottomView方法,不隐藏
    [self cancelPerformSelector:@selector(hideHeaderViewAndBottomView)];
    Float64 totalSeconds = CMTimeGetSeconds(self.duration);
    CMTime time = CMTimeMakeWithSeconds(totalSeconds * slider.value, self.duration.timescale);
    [self seekToCMTime:time progress:self.progressView.value];
}
// touchUpInside/touchUpOutside
- (void)slidingEnded:(UISlider *)sender {
    // 拖动手势取消后延迟调用hideHeaderViewAndBottomView
    [self delayHideHeaderViewAndBottomView];
}
//进度条点击定位
- (void)sliderTapGesture:(UITapGestureRecognizer *)sender {
    [self cancelPerformSelector:@selector(hideHeaderViewAndBottomView)];
    CGFloat tapX = [sender locationInView:sender.view].x;
    CGFloat sliderWidth = sender.view.bounds.size.width;
    Float64 totalSeconds = CMTimeGetSeconds(self.duration); // 总时间
    CMTime dstTime = CMTimeMakeWithSeconds(totalSeconds * (tapX / sliderWidth), self.duration.timescale);
    [self seekToCMTime:dstTime progress:self.progressView.value];
    [self delayHideHeaderViewAndBottomView];
}


7.完美适配横竖屏;


很多时候大家注意的是手机方向改变时横竖屏切换和整个项目内横竖屏的关系,上面给出了方法来解决,在Appdelegate中给属性来决定是否支持横屏,这里还有一个手动的通过代码来设置的方法:

[[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIDeviceOrientationLandscapeLeft]forKey:@"orientation"];

可以根据自己的需要设置屏幕方向,前提是Xcode中设置了支持该方向。


8.打断机制和监听机制;


   // 监控 app 活动状态,打电话/锁屏 时暂停播放
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBecomActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
- (void)appWillResignActive:(NSNotification *)aNotification {
    [self.player pause];
    self.isPlaying = NO;
}
- (void)appDidBecomActive:(NSNotification *)aNotification {
    //
    [self.player play];
    self.isPlaying = YES;
}

KVO监听

 // KVO观察self.isPlaying属性的变化以改变playButton的状态
    [self addObserver:self forKeyPath:@"isPlaying" options:NSKeyValueObservingOptionNew context:playerPlayingContext];
// 观察self.playItem.status属性变化,变为AVPlayerItemStatusReadyToPlay时就可以播放了
    [self addObserver:self forKeyPath:@"playItem.status" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:playerItemStatusContext];
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == playerItemStatusContext) {
        if (self.playItem.status == AVPlayerItemStatusReadyToPlay) {
            // 视频准备就绪
            dispatch_async(dispatch_get_main_queue(), ^{
                [self readyToPlay];
            });
        } else {
            // 如果一个不能播放的视频资源加载进来会进到这里
            NSLog(@"视频无法播放");
            // 延迟dismiss播放器视图控制器
            [self performSelector:@selector(delayDismissPlayerViewController) withObject:nil afterDelay:3.0f];
        }
    } else if (context == playerPlayingContext){
        dispatch_async(dispatch_get_main_queue(), ^{
            if ([[change objectForKey:@"new"] intValue] == 1) {
                // 如果playing变为YES就显示暂停按钮
                [self.playButton setImage:[[UIImage imageNamed:@"pause_nor@2x.png"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] forState:UIControlStateNormal];
            } else {
                // 如果playing变为NO就显示播放按钮
                [self.playButton setImage:[[UIImage imageNamed:@"play_nor@2x.png"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] forState:UIControlStateNormal];
            }
        });
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}


9.dealloc;


最后的最后,别忘了dealloc,虽然目前有自动回收机制,不过类似视频这种的,貌似是没用的,需要手动来释放内存,删除监听和通知。 ``` #pragma mark - dealloc

(void)dealloc

{

[self.player pause];

[self removeObserver:self forKeyPath:@“playItem.status” context:playerItemStatusContext];

[self removeObserver:self forKeyPath:@“isPlaying” context:playerPlayingContext];

[self.player removeTimeObserver:self.timeObserver];

self.timeObserver = nil;

[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];

[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];

[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];

self.player = nil;

self.playItem = nil;

self.mediaUrl = nil;

}

<p id="j">10.总结;</p>
以上便是博主封装AVPlayer的过程,不过其中也借鉴了很多东西,有一个叫JRVideoPlayer的开源Demo,博主的内容很多都是从这里来的,有些方法因为写的很完善索性直接拿过来用了,基本上是在原基础上做了优化和新增功能,前人植树,后人乘凉,踩着大神的脚步前进,这里也是因为博主项目中需要用到缓存才去做了封装,功能都是另外加的,UI因为我们的设计就是这样,所以列为不要嫌丑,可自行更换UI,刚好这个播放器可塑性很强,还有下载地址:[点击下载](https://github.com/codeliu6572/LHHVideoPlayer)
博主没有做本地视频的播放,只有网络视频的方法,后续会补上,这部分也不复杂,也可自行加上去。
有一点要着重说明,如果你需要播放器进去就是全屏,就在播放器的VC中改变frame为全屏,同时根据第7条的方法设置默认为横屏,如果你愿意竖屏也行,另外还需要把layoutUI中竖屏的frame改成全屏或者直接删除。如果你的详情页需要现在这样的小窗,下面展示信息和评论等,你需要在外部创建一个UIView,在VC中视频下面的区域放置这个UIView不需要改变frame,视频全屏时会遮住这个View,不用怕下面的界面混乱,以上是两种情况的思路,各位请自便。
目录
相关文章
|
6天前
|
安全 Android开发 iOS开发
探索安卓与iOS开发的差异:平台特性与用户体验的深度对比
在移动应用开发的广阔天地中,安卓和iOS两大平台各占半壁江山。本文旨在通过数据驱动的分析方法,深入探讨这两大操作系统在开发环境、用户界面设计及市场表现等方面的差异。引用最新的行业报告和科研数据,结合技术专家的观点,本文将提供对开发者和市场分析师均有价值的洞见。
|
8天前
|
Java 开发工具 Android开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
在移动应用开发的广阔天地中,Android和iOS两大平台各自占据着半壁江山。本文将深入探讨这两个平台在开发过程中的关键差异点,包括编程语言、开发工具、用户界面设计、性能优化以及市场覆盖等方面。通过对这些关键因素的比较分析,旨在为开发者提供一个清晰的指南,帮助他们根据项目需求和目标受众做出明智的平台选择。
|
9天前
|
编解码 Android开发 iOS开发
深入探索Android与iOS开发的差异与挑战
【6月更文挑战第24天】在移动应用开发的广阔舞台上,Android和iOS两大操作系统扮演着主角。它们各自拥有独特的开发环境、工具集、用户基础及市场策略。本文将深度剖析这两个平台的开发差异,并探讨开发者面临的挑战,旨在为即将踏入或已在移动开发领域奋斗的开发者提供一份实用指南。
32 13
|
5天前
|
iOS开发 开发者 UED
探索iOS开发中的SwiftUI框架
【6月更文挑战第28天】在移动应用开发的海洋中,SwiftUI作为iOS平台的新星,以其声明式语法和灵活性,正引领着界面设计的未来。本文将带你深入理解SwiftUI的核心概念、布局能力以及如何通过它提升开发效率,为开发者们提供一份实操指南,解锁SwiftUI的强大潜力。
13 1
|
12天前
|
iOS开发 开发者 容器
探索iOS开发中的SwiftUI框架
【6月更文挑战第21天】本文深入探讨了苹果在iOS开发中推出的SwiftUI框架,旨在为开发者提供一种声明式、更简洁的界面设计方法。文章首先概述了SwiftUI的核心概念和优势,接着通过一个天气预报应用实例,详细讲解了如何使用SwiftUI进行布局和用户界面的设计。此外,还讨论了SwiftUI与UIKit的差异,以及如何将SwiftUI集成到现有的项目中。最后,文章展望了SwiftUI的未来发展方向,包括潜在的改进和新特性。
|
8天前
|
监控 Android开发 iOS开发
探索Android与iOS开发的差异:平台、工具和用户体验的比较
【6月更文挑战第25天】在移动应用开发的广阔天地中,Android和iOS两大平台各领风骚,它们在开发环境、工具选择及用户体验设计上展现出独特的风貌。本文将深入探讨这两个操作系统在技术实现、市场定位和用户交互方面的关键差异,旨在为开发者提供一个全景式的视图,帮助他们在面对项目决策时能够更加明智地选择适合自己项目需求的平台。
|
12天前
|
Java 开发工具 Android开发
安卓与iOS开发差异解析
【6月更文挑战第21天】本文旨在深入探讨安卓和iOS两大移动操作系统在应用开发过程中的主要差异。通过对比分析,揭示各自的设计哲学、编程语言选择、用户界面构建、性能优化策略以及发布流程的异同。文章将提供开发者视角下的实用信息,帮助他们更好地理解各自平台的特点和挑战,从而做出更明智的开发决策。
|
1天前
|
API Swift iOS开发
探索iOS开发:SwiftUI框架的革新之旅
【7月更文挑战第2天】在这篇文章中,我们将深入探讨SwiftUI——苹果为简化iOS界面开发而设计的现代框架。不同于传统的摘要概述,我们将通过一个开发者的视角,逐步揭示SwiftUI如何改变iOS应用的构建过程,并分享一些实用的技巧和最佳实践,以助你快速上手这一前沿技术。
|
6天前
|
缓存 C语言 iOS开发
一篇文章讲明白iOS开发系列
一篇文章讲明白iOS开发系列
|
8天前
|
设计模式 IDE Swift
探索iOS开发:从新手到专家的旅程
【6月更文挑战第25天】在数字时代的浪潮中,iOS开发作为一门艺术和科学的结合体,吸引了众多开发者的目光。本文将带领读者踏上一场精彩的旅程,从基础的搭建环境开始,逐步深入到高级编程技巧,再到应用发布与市场策略,全方位解读iOS开发的魅力所在。通过实际案例分析,我们将揭示那些让应用脱颖而出的秘密,以及如何在竞争激烈的应用市场中保持竞争力。无论你是初学者还是有经验的开发者,这篇文章都将为你提供宝贵的见解和实用的技巧,让你的iOS开发之旅更加顺畅。