美文网首页IOS个人开发音视频视频音频播放
iOS通过AVPlayer打造自己的视频播放器

iOS通过AVPlayer打造自己的视频播放器

作者: MNode | 来源:发表于2016-11-28 11:34 被阅读11534次

AVPlayer

AVPlayer是用于管理媒体资产的播放和定时控制器对象它提供了控制播放器的有运输行为的接口,如它可以在媒体的时限内播放,暂停,和改变播放的速度,并有定位各个动态点的能力。可以使用AVPlayer来播放本地和远程的视频媒体文件,如QuickTime影片和MP3音频文件,以及视听媒体使用HTTP流媒体直播服务。

一个普通播放器的组成

111.png

概述

注意

AVPlayer旨在用于在一段内时间播放单个媒体资产。播放器实例可以重复使用其播放额外的媒体资产[replaceCurrentItem(with:)]的方法,但它管理仅单个媒体资产的播放一次。该框架还提供了的一个子类AVPlayer,叫做AVQueuePlayer
你可以用它来创建和媒体资产的队列管理进行顺序播放。
使用AVPlayer需导入AVFoundation框架。

#import <AVFoundation/AVFoundation.h>

创建AVPlayer需是全局对象,否则在运行时无法显示视频图像。

@property (nonatomic,strong) AVPlayer *player;

AVPlayer播放器的创建

首先创建资产AVURLAsset
self.asset=[[AVURLAsset alloc]initWithURL:_url options:nil];
使用AVURLAsset然后将asset对象导入到AVPlayerItem中
self.item=[AVPlayerItem playerItemWithAsset:self.assert];
再将item对象添加到AVPlayer中
self.player=[[AVPlayer alloc]initWithPlayerItem:self.item];
比直接使用AVPlayer初始化方法播放URL如
self.player=[[AVPlayer alloc]initWithURL:url];
的好处是,self.asset可以记录缓存大小,而直接使用AVPlayer初始化URL不利于多个控制器更好的衔接缓存大小。
当我们在使用今日头条或者UC头条的时候,会发现点击cell上的视频播放一段时间后,再点击cell上的评论会跳到另外一个控制器,但是视频播放的位置和缓存的进度跟第一级控制器cell上位置一模一样,看起来就像是2个控制器共用一个视频播放器,这种无缝切换的效果用户体验很好,做法其实只需公用一个AVURLAsset就可以做到。

下面我将介绍下我最近封装AVPlayer的视频播放器SBPlayer组成及思路

SBPlayer github源码--->github
SBPlayer是基于AVPlayer封装的轻量级播放器,可以播放本地网络视频,易于定制,适合初学者学习打造属于自己的视频播放器

播放器集成于UIView 的SBPlayer.h文件开发的接口

#import "SBView.h"
#import "SBPlayerLoading.h"
#import <AVFoundation/AVFoundation.h>
#import "SBPlayerControl.h"
#import "SBPlayerPlayPausedView.h"
/**
 设置视频播放填充模式
 */
typedef NS_ENUM(NSInteger,SBPlayerContentMode) {
    SBPlayerContentModeResizeFit,//尺寸适合
    SBPlayerContentModeResizeFitFill,//填充视图
    SBPlayerContentModeResize,//默认
};
typedef NS_ENUM(NSInteger,SBPlayerState) {
    SBPlayerStateFailed,        // 播放失败
    SBPlayerStateBuffering,     // 缓冲中
    SBPlayerStatePlaying,       // 播放中
    SBPlayerStateStopped,        //停止播放
};

@interface SBPlayer : SBView

//当视频没有播放为0,播放后是1
@property (nonatomic,assign) NSInteger isNormal;
//加载的image;
@property (nonatomic,strong) UIImageView *imageViewLogin;
//视频填充模式
@property (nonatomic,assign) SBPlayerContentMode contentMode;
//播放状态
@property (nonatomic,assign) SBPlayerState state;
//加载视图
@property (nonatomic,strong) SBPlayerLoading *loadingView;
//是否正在播放
@property (nonatomic,assign,readonly) BOOL isPlaying;
//暂停时的插图
@property (nonatomic,strong) SBPlayerPlayPausedView *playPausedView;
//urlAsset
@property (nonatomic,strong) AVURLAsset *assert;
//当前时间
@property (nonatomic,assign) CMTime currentTime;
//播放器控制视图
@property (nonatomic,strong) SBPlayerControl *playerControl;
//初始化
- (instancetype)initWithUrl:(NSURL *)url;
- (instancetype)initWithURLAsset:(AVURLAsset *)asset;
//设置标题
-(void)setTitle:(NSString *)title;
//跳到某个播放时间段
-(void)seekToTime:(CMTime)time;
//播放
-(void)play;
//暂停
-(void)pause;
//停止
-(void)stop;
//移除监听,notification,dealloc
-(void)remove;
//显示或者隐藏暂停按键
-(void)hideOrShowPauseView;

SBPlayer.m文件中的扩展方法

@interface SBPlayer ()<SBPlayerControlSliderDelegate,SBPlayerPlayPausedViewDelegate>
{
    NSURL *_url;
    NSTimer *_timer;
}
@property (nonatomic,strong) AVPlayerLayer *playerLayer;
@property (nonatomic,strong) AVPlayer *player;
@property (nonatomic,strong) AVPlayerItem *item;
//总时长
@property (nonatomic,assign) CGFloat totalDuration;
//转换后的时间
@property (nonatomic,copy) NSString *totalTime;
//当前播放位置
@property (nonatomic,assign) CMTime currenTime;
//监听播放值
@property (nonatomic,strong) id playbackTimerObserver;
//全屏控制器
@property (nonatomic,strong) UIViewController *fullVC;
//全屏播放器
@property (nonatomic,strong) SBPlayer *fullScreenPlayer;
@end

播放器的初始化
//配置播放器
-(void)configPlayer{
    self.backgroundColor=[UIColor blackColor];
    self.item=[AVPlayerItem playerItemWithAsset:self.assert];
    self.player=[[AVPlayer alloc]init];
    [self.player replaceCurrentItemWithPlayerItem:self.item];
    self.player.usesExternalPlaybackWhileExternalScreenIsActive=YES;
    self.playerLayer=[[AVPlayerLayer alloc]init];
    self.playerLayer.backgroundColor=[UIColor blackColor].CGColor;
    self.playerLayer.player=self.player;
    self.playerLayer.frame=self.bounds;
    [self.playerLayer displayIfNeeded];
    [self.layer insertSublayer:self.playerLayer atIndex:0];
    self.playerLayer.videoGravity=AVLayerVideoGravityResizeAspect;
}

由于是使用AutoLayout布局,而不是直接设置Frame,所以需要在layoutSubviews中初始化AVPlayerLayer大小,否则在AVPlayerLayer播放区域显示不了自定义的控件,比如播放、暂停、进度条等等。

-(void)layoutSubviews{
    [super layoutSubviews];
    self.playerLayer.frame=self.bounds;
}
视频播放需大量使用KVO和NSNotificationCenter
-(void)addKVO{
    //监听状态属性
    [self.item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    //监听网络加载情况属性
    [self.item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    //监听播放的区域缓存是否为空
    [self.item addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
    //缓存可以播放的时候调用
    [self.item addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
    //监听暂停或者播放中
    [self.player addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionNew context:nil];
    [self.player addObserver:self forKeyPath:@"timeControlStatus" options:NSKeyValueObservingOptionNew context:nil];
    [self.playerControl addObserver:self forKeyPath:@"scalling" options:NSKeyValueObservingOptionNew context:nil];
    [self.playPausedView addObserver:self forKeyPath:@"backBtnTouched" options:NSKeyValueObservingOptionNew context:nil];
}
-(void)addNotification{
    //监听当视频播放结束时
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(SBPlayerItemDidPlayToEndTimeNotification:) name:AVPlayerItemDidPlayToEndTimeNotification object:[self.player currentItem]];
    //监听当视频开始或快进或者慢进或者跳过某段播放
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(SBPlayerItemTimeJumpedNotification:) name:AVPlayerItemTimeJumpedNotification object:[self.player currentItem]];
    //监听播放失败时
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(SBPlayerItemFailedToPlayToEndTimeNotification:) name:AVPlayerItemFailedToPlayToEndTimeNotification object:[self.player currentItem]];
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(SBPlayerItemPlaybackStalledNotification:) name:AVPlayerItemPlaybackStalledNotification object:[self.player currentItem]];
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(SBPlayerItemNewAccessLogEntryNotification:) name:AVPlayerItemNewAccessLogEntryNotification object:[self.player currentItem]];
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(SBPlayerItemNewErrorLogEntryNotification:) name:AVPlayerItemNewErrorLogEntryNotification object:[self.player currentItem]];
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(SBPlayerItemFailedToPlayToEndTimeErrorKey:) name:AVPlayerItemFailedToPlayToEndTimeErrorKey object:[self.player currentItem]];
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(deviceOrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil];
}
进度条的实现
Paste_Image.png

添加自定义控制器:有播放、暂停、进度条、全屏功能


12.png
#pragma mark - addPlayerControl
//添加播放控制器
-(void)addPlayerControl{
    self.playerControl=[[SBPlayerControl alloc]init];
    self.playerControl.minValue=0.0f;
    self.playerControl.delegate=self;
    //设置播放控制器的背景颜色
    self.playerControl.backgroundColor=[UIColor colorWithRed:0.20 green:0.20 blue:0.20 alpha:0.5];
    NSLog(@"self.totalDuration:%f",self.totalDuration);
    [self addSubview:self.playerControl];
    [self.playerControl mas_makeConstraints:^(MASConstraintMaker *make) {
        make.bottom.mas_equalTo(self.mas_bottom).priorityHigh();
        make.left.mas_equalTo(self.mas_left);
        make.right.mas_equalTo(self.mas_right);
        make.height.mas_equalTo(@(controlHeight));
    }];
    [self setNeedsLayout];
    [self layoutIfNeeded];
    self.playerControl.hidden=YES;
}

在自定义进度条的时候,如果使用UISlider定制进度条,虽然能实时显示视频播放的进度,却无法显示视频缓冲的进度。当自己高高兴兴,辛辛苦苦用UISlider写好了进度条后突然想到还要加缓冲进度条,这就坑爹了。还好,天无绝人之路,只需在UISlider视图底层再加一层UISlider, 设置UISider setMaximumTrackTintColor为透明色,改变底部UISlider的值便可以成功加入缓冲进度条,底部UISlider的系统默认的白色小圆圈可以通过设置[UIImage new]去除。大家有兴趣看下源码,帮忙加star。

加载动画的实现

SBPlayer加载动画是通过旋转图片实现,使用简单

#import "SBPlayerLoading.h"
#import <Masonry.h>
//间隔时间
#define duration 1.0f
//加载图片名
#define kLoadingImageName @"Source.bundle/collection_loading"
@interface SBPlayerLoading (){
   NSTimer *_timer;//定时器
}
@property (nonatomic,strong) UIImageView *loadingImage;//加载时的图片
@end
@implementation SBPlayerLoading
//初始化
- (instancetype)init
{
   self = [super init];
   if (self) {
       self.loadingImage=[[UIImageView alloc]initWithImage:[UIImage imageNamed:kLoadingImageName]];
       self.loadingImage.contentMode=UIViewContentModeScaleAspectFill;
       [self addSubview:self.loadingImage];
       [self addConstraintWithView:self.loadingImage];
   }
   return self;
}
//添加约束
-(void)addConstraintWithView:(UIImageView *)imageView{
   [imageView mas_makeConstraints:^(MASConstraintMaker *make) {
       make.centerX.centerY.mas_equalTo(self);
       make.size.mas_equalTo(CGSizeMake(30, 30));
   }];
   [self setNeedsLayout];
   [self layoutIfNeeded];
}
//显示
-(void)show{
   if ([_timer isValid]) {
       [_timer invalidate];
       _timer=nil;
   }
   self.hidden=NO;
   _timer=[NSTimer timerWithTimeInterval:duration/2 target:self selector:@selector(rotationImage) userInfo:nil repeats:YES];
   [[NSRunLoop currentRunLoop]addTimer:_timer forMode:NSDefaultRunLoopMode];
}
//旋转图片
-(void)rotationImage{
   
   [UIView animateWithDuration:duration animations:^{
       self.loadingImage.transform=CGAffineTransformRotate(self.loadingImage.transform, M_PI);
   }];
}
//隐藏
-(void)hide{
   if ([_timer isValid]) {
       [_timer invalidate];
       _timer=nil;
   }
   self.hidden=YES;
}

@end
全屏的实现

SBPlayer的全屏是通过将播放器添加到keyWindow上,然后约束好,很简单就实现此功能。

//获取当前屏幕显示的viewcontroller
- (UIViewController *)getCurrentVC
{
    UIViewController *result = nil;
    UIWindow * window = [[UIApplication sharedApplication] keyWindow];
    if (window.windowLevel != UIWindowLevelNormal)
    {
        NSArray *windows = [[UIApplication sharedApplication] windows];
        for(UIWindow * tmpWin in windows)
        {
            if (tmpWin.windowLevel == UIWindowLevelNormal)
            {
                window = tmpWin;
                break;
            }
        }
    }
    UIView *frontView = [[window subviews] objectAtIndex:0];
    id nextResponder = [frontView nextResponder];
    if ([nextResponder isKindOfClass:[UIViewController class]])
        result = nextResponder;
    else
        result = window.rootViewController;
    return result;
}

点击全屏按钮或者旋转手机的时候,自动判断旋转方向,当横屏时present新的控制器,竖屏时销毁

-(void)deviceOrientationDidChange:(NSNotification *)notification{
    UIInterfaceOrientation _interfaceOrientation=[[UIApplication sharedApplication]statusBarOrientation];
    switch (_interfaceOrientation) {
        case UIInterfaceOrientationLandscapeLeft:
        case UIInterfaceOrientationLandscapeRight:
        {
            self.oldConstriants = [self getCurrentVC].view.constraints;
            [UIView animateWithDuration:0.3 delay:0 usingSpringWithDamping:0.5 initialSpringVelocity:0. options:UIViewAnimationOptionTransitionCurlUp animations:^{
                [[UIApplication sharedApplication].keyWindow addSubview:self];
                [self mas_makeConstraints:^(MASConstraintMaker *make) {
                    make.edges.mas_equalTo([UIApplication sharedApplication].keyWindow);
                }];
                [self layoutIfNeeded];
            } completion:nil];
        }
            break;
        case UIInterfaceOrientationPortraitUpsideDown:
        case UIInterfaceOrientationPortrait:
        {
            [[UIApplication sharedApplication].keyWindow removeFromSuperview];
            [[self getCurrentVC].view addSubview:self];
            [UIView animateKeyframesWithDuration:0.5 delay:0 options:UIViewKeyframeAnimationOptionCalculationModeLinear animations:^{
                if (self.oldConstriants) {
                    [[self getCurrentVC].view addConstraints:self.oldConstriants];
                }
                [self layoutIfNeeded];
            } completion:nil];
        }
            break;
        case UIInterfaceOrientationUnknown:
            NSLog(@"UIInterfaceOrientationUnknown");
            break;
    }
    [[self getCurrentVC].view layoutIfNeeded];

}

23.gif
视频前添加标题和弹幕

前面重要的步骤处理完后,在视频上添加标题弹幕只需在AVPlayerLayer所在UIView上添加相关控件,就可以实现这些简单功能。

结语

表述能力有限,如果大家喜欢的话,希望进入github网址star一下

_SBPlayer _ github源码--->github

相关文章

网友评论

  • IT界的古天乐:您好,请问,AVplayer可以设置进度条禁止拖拽吗
  • A_rcher34:您好,请问,AVplayer可以设置更改清晰度吗?
  • 你好牛:大大 为什么缓存了一部分还是不会播放 要缓存很多才能播放 我们就15秒段视频 基本上快缓存完了 才能播放
  • 勇敢的呆喵:只支持竖屏 的APP 无法全屏播放:sob: 大神有解决办法么 旋转的话在iOS11上好使 在iOS11以下不好使
  • FR_Zhang:支持动态改变播放区域的大小吗
  • 硅谷小虾米:播放器在全屏播放之后再次点击返回小屏之后发现并没有恢复成原来的大小,而是竖屏整屏播放,出现这种情况的原因是因为在SBPlayer中有个getCurrentVC方法,这个方法是用来获取当前显示的控制器的,如果你的控制器是嵌套在一个UITabBarController中的话,那么通过这个方法获取的当前控制器是根视图控制器(UITabBarController),所以不能采用这种方法获取当前控制器,替换方法连接: https://blog.csdn.net/u011363981/article/details/53186676 只用把getCurrentVC方法实现修改就可以
  • Arthur澪:全屏观看后,无法正常缩屏还原了。原因是无法正确获取当前控制器。改正方法:当前控制器用不着每次都执行[self getCurrentVC]去获取。可以使用懒加载。一举两得!
  • 我是要成为大神的男人:支持下载缓存么
  • edb493d9a752:非常的赞 看了这么多播放器 楼主的简单易懂 是我这种菜鸟 最容易学懂的
  • 860ec265cd70:楼主,我的没法播放,一播放就出现 AVPlayerItemStatusFailed
  • 卢叁:我共用一个AVURLAsset 发现并没有实现无缝播放(今日头条的效果) 这是怎么回事
    _asset =[AVURLAsset assetWithURL:[NSURL URLWithString:VideoURL]];

    self.playerView =[[LYAVPlayerView alloc]init];
    self.playerView.frame =CGRectMake(0, 64, ScreenWidth,200);
    self.playerView.delegate =self;
    [self.view addSubview:self.playerView];
    [self.playerView setAsset:_asset];
    [self.playerView play];

    跳转到下一页面
    SecondViewController *viewCtrl =[[SecondViewController alloc]init];
    viewCtrl.asset =_asset;
    [self.navigationController pushViewController:viewCtrl animated:NO];

    这是怎么回事呢


    MNode:@卢叁 共享asset,同时seektime
    卢叁:@走哪都有风 直接记录时间 然后seekTotime?那这样就不用共享同一个AVURLAsset 效果也不好吧?麻烦贴出一个大概的代码 谢谢了
    MNode:@卢叁 共享了同一资产,把时间定为下就可以啊。另外一个控制器就不用再重新加载
  • 九剑仙:mkv播放不出来,怎么修改呀大大
  • 什么的黑夜:大小屏切换不正常,打印的vc.view.constraints.count = 0,这是为啥
  • Mister志伟:AVPlayer 的
    -(void)seekToTime
    方法当time时间大小小于一秒的时候,AVPlayer就不会作同步的处理(如:快进,后退等),会依然保留在原来的位置。
    AVPlayerDemo里面,通过Slider去拖动时,也是当经过时间大与1的时候才会进行一次同步。
    想请问一下怎么让AVPlayer 快进/后退 1秒以下的时间?
  • 054ce4d15908:请问一下楼主,你这个在TableView里,有多个视频,点击当前的,下一个会暂停不,类似于新浪微博的
  • 大浪捉鱼:SBControlView里的addSubView操作为什么要放在drawRect里面做?
  • 总想写点东西的陌小默:在git上面下载的代码竟然不能直接运行,大神为什么?
  • MonarchNie:楼主,我使用后放大后会全屏,但是点击那个放大按钮后,想返回小屏,但是视频并没有返回成之前那个那个小屏,而是感觉是放大之后的视频充满了屏幕,这个放大缩小还有bug
    香蕉你个菠萝:你好 这个问题解决了吗
    香蕉你个菠萝:退出全屏 我调用stop方法 视图不会移除……不知道原因,希望楼主能指导下
    离离离离:您好 我也遇到了 想问您解决了吗
  • MrJ的杂货铺:播放失败后重新播放,怎么不行
  • a3738549c736:你好!为什么模拟器测试下载视频很流畅,真机测试下载不了的感觉呢
  • 别欺骗小女生:楼主 就是默认开始是暂停 怎么改啊
  • TigerNong:你有没有尝试在全屏的情况下,进入后台,然后再回到前台时,全屏有什么变化。我的旋转方式与你的一样,然后在全屏情况下进入后台,再回到前台时,都变成了竖屏(播放屏幕还是全屏)!
  • 菠萝吹雪xs:人过留星
  • alelaile:大神 我这边demo屏幕旋转三次后就crash了
    alelaile:谢谢 帮助
    MNode:@alelaile 你好,代码已经重构好,不会再出现crash,代码量少,使用流畅
    MNode:了解,这个星期在重构这个播放器:smile:
  • 午马丶:你好,我用了你的demo 由于可能是我这里网太慢,显示播放失败,然后就crash了,原因是删除了没有注册的通知,是否有判断某个通知是否已经添加的方法,望回复谢谢,
    MNode:@A丶 我发现有个KVO没有移除,已经移除了。有空试下看行不行
    MNode:@A丶 谢谢你反馈,我找下原因
  • 碎小镜:按照步骤,然后真机测试发现avplayer放不出声音。同样的url路径在audioplayer里可以放。大概是什么问题
  • 酸三角:适合新手学习啊,感谢作者分享
  • kkkore:楼主,大大,你好~~
    我的Slider和ProgressView,位置总是偏差一点,你有什么好的办法吗?我看到demo里,貌似也有细微偏差。slider里的滑动条有缩进几个px,但是好像没有属性去获取到滑动条的frame,runtime貌似也不行=。= :cold_sweat:
    kkkore:@走哪都有风 楼主大大,我现在遇到一个问题,我有一个 MP4链接,模拟器播放,无声音。真机播放,AVPlayerItemStatusFailed。但是如果使用的是 ijkPlayer,则正常。这之间差别在哪儿。=。=你有遇到过吗。
    kkkore:@走哪都有风 楼主大大,这个slider可以重写一个它的方法,可以再去调整它的滑动条的缩进。
    MNode:@kkkore 4.0寸以下设置高度0.5, 4寸以上高度1,这样应该看不到什么偏差.不过最好还是用UIView自定义一个进度条. :smile:
  • hu9134:请问一下,有没有倒序播放的方案,正常情况下是正序播放,放置一个按钮,点击之后倒序播放,谢谢
    hu9134:@走哪都有风 大神你好,请教一个问题,我想实现N倍慢速,我以为直接设置0.0-1.0之间的数值就可以了,可是现在我设置为0.5和0.1是一样的速度(是比正常速度慢了),这个是什么情况啊?我想实现2,4,6倍慢速播放,谢谢
    hu9134:@走哪都有风 非常感谢,以前不知道有这个,非常感谢大神! :+1:
    MNode:@hu9134 设置AVplayer的rate属性等于-1的时候可以倒序播放.值为0.0暂停视频,而值为1.0的时候是正常速率播放。官网介绍:https://developer.apple.com/reference/avfoundation/avplayer/1388846-rate
  • 一只霸天犬v:请问一下楼主 要是想在视频播放之前加广告视频 大概是个什么原理?
    一只霸天犬v:@走哪都有风 太感谢了:grin::grin::grin:
    MNode:@小仙儿v 如果是AVPlayer的话,它有个replaceCurrentItem这个方法,可以等广告视频播放完替换下一个播放。或者用AVQueuePlayer,有队列播放功能,播放上一个会跳到一下个,使用方法和AVPlayer差不多。如果使用已经封装好的jikplayer也可以先嵌入一个播放对象,等上个播放完立马播放下一个。 :smile: 仅供参考
  • Shumin_Wu:支持。 avplayer 有个问题只能播放某几种格式的视频 比如 mp4 WMV 如果是流视频则不支持。
    进化中的程序猿:怎么不支持,m3u8流正常播放啊

本文标题:iOS通过AVPlayer打造自己的视频播放器

本文链接:https://www.haomeiwen.com/subject/vqfopttx.html