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的世界中游刃有余。
|
13天前
|
安全 数据处理 Swift
深入探索iOS开发中的Swift语言特性
本文旨在为开发者提供对Swift语言在iOS平台开发的深度理解,涵盖从基础语法到高级特性的全面分析。通过具体案例和代码示例,揭示Swift如何简化编程过程、提高代码效率,并促进iOS应用的创新。文章不仅适合初学者作为入门指南,也适合有经验的开发者深化对Swift语言的认识。
35 9
|
10天前
|
设计模式 Swift iOS开发
探索iOS开发:从基础到高级,打造你的第一款App
【10月更文挑战第40天】在这个数字时代,掌握移动应用开发已成为许多技术爱好者的梦想。本文将带你走进iOS开发的世界,从最基础的概念出发,逐步深入到高级功能实现,最终指导你完成自己的第一款App。无论你是编程新手还是有志于扩展技能的开发者,这篇文章都将为你提供一条清晰的学习路径。让我们一起开始这段旅程吧!
|
13天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
11天前
|
iOS开发 开发者
探索iOS开发中的SwiftUI框架
【10月更文挑战第39天】在苹果的生态系统中,SwiftUI框架以其声明式语法和易用性成为开发者的新宠。本文将深入SwiftUI的核心概念,通过实际案例展示如何利用这一框架快速构建用户界面,并探讨其对iOS应用开发流程的影响。
|
14天前
|
JSON 前端开发 API
探索iOS开发之旅:打造你的第一个天气应用
【10月更文挑战第36天】在这篇文章中,我们将踏上一段激动人心的旅程,一起构建属于我们自己的iOS天气应用。通过这个实战项目,你将学习到如何从零开始搭建一个iOS应用,掌握基本的用户界面设计、网络请求处理以及数据解析等核心技能。无论你是编程新手还是希望扩展你的iOS开发技能,这个项目都将为你提供宝贵的实践经验。准备好了吗?让我们开始吧!
|
19天前
|
设计模式 前端开发 Swift
探索iOS开发:从初级到高级的旅程
【10月更文挑战第31天】在这篇文章中,我们将一起踏上iOS开发的旅程。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧。我们将从基础开始,逐步深入到更高级的技术和概念。让我们一起探索iOS开发的世界吧!
|
22天前
|
设计模式 前端开发 Swift
探索iOS开发:从初级到高级的旅程
【10月更文挑战第28天】在这篇技术性文章中,我们将一起踏上一段探索iOS开发的旅程。无论你是刚入门的新手,还是希望提升技能的开发者,这篇文章都将为你提供宝贵的指导和灵感。我们将从基础概念开始,逐步深入到高级主题,如设计模式、性能优化等。通过阅读这篇文章,你将获得一个清晰的学习路径,帮助你在iOS开发领域不断成长。
57 2
|
17天前
|
存储 数据可视化 Swift
探索iOS开发之旅:从新手到专家
【10月更文挑战第33天】在这篇文章中,我们将一起踏上一场激动人心的iOS开发之旅。无论你是刚刚入门的新手,还是已经有一定经验的开发者,这篇文章都将为你提供宝贵的知识和技能。我们将从基础的iOS开发概念开始,逐步深入到更复杂的主题,如用户界面设计、数据存储和网络编程等。通过阅读这篇文章,你将获得成为一名优秀iOS开发者所需的全面技能和知识。让我们一起开始吧!
|
18天前
|
移动开发 Java Android开发
探索Android与iOS开发的差异性与互联性
【10月更文挑战第32天】在移动开发的大潮中,Android和iOS两大平台各领风骚。本文将深入浅出地探讨这两个平台的开发差异,并通过实际代码示例,展示如何在各自平台上实现相似的功能。我们将从开发环境、编程语言、用户界面设计、性能优化等多个角度进行对比分析,旨在为开发者提供跨平台开发的实用指南。
38 0