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

5,386 阅读7分钟

AVPlayer

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

一个普通播放器的组成


111.png

概述

注意

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

#import

创建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 
#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 ()
{
    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视图底层再加一层UIProgressView, 设置UISider setMaximumTrackTintColor为透明色,改变UIProgressView的值便可以成功加入缓冲进度条。最好是通过UIView自定义进度条。我在写进度条的时候正好是用UISilder做的,可实现缓冲进度哦,大家有兴趣看下源码,帮忙加star。

加载动画的实现

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

#import "SBPlayerLoading.h"
#import 
//间隔时间
#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


文/走哪都有风(简书作者)
原文链接:http://www.jianshu.com/p/ffe1bd598bf2
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
全屏的实现

SBPlayer的全屏是通过Present出一个新的UIViewController,通过NSNotificationCenter监听UIDeviceOrientationDidChangeNotification来处理横屏和竖屏下的播放器。
由于SBPlayer是继承UIView,UIView是不可以推出新的控制器的,所以通过keyWindow获取当前控制器。这样就不用管是在UITableViewController还是UIViewController

//获取当前屏幕显示的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.fullVC=[self pushToFullScreen];
            [self.player pause];
            [self.fullScreenPlayer seekToTime:self.item.currentTime];
            [[self getCurrentVC] presentViewController:self.fullVC animated:YES completion:nil];
        }
            break;
        case UIInterfaceOrientationPortraitUpsideDown:
        case UIInterfaceOrientationPortrait:
        {

            if (self.fullVC) {
                if (self.fullScreenPlayer.isPlaying) {
                    [self.player play];
                    [self.playPausedView hide];
                }else{
                    [self pause];
                    _isNormal=1;
                    [self hideOrShowPauseView];
                }
                if (self.fullScreenPlayer.item.currentTime.value/self.fullScreenPlayer.item.currentTime.timescale>0) {
                    [self.player seekToTime:self.fullScreenPlayer.currentTime];
                }
                [self.fullScreenPlayer remove];
                self.fullScreenPlayer=nil;
                [self.fullVC dismissViewControllerAnimated:YES completion:nil];
            }
        }
            break;
        case UIInterfaceOrientationUnknown:
            NSLog(@"UIInterfaceOrientationLandscapePortial");
            break;
    }
}
视频前添加标题和弹幕

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

结语

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

SBPlayer github源码--->github