iOS开发——AVPlayer自定义播放器(持续更新,学习中)

一、 前言

边学边记录AVPlayer封装一个功能十分 全的自定义播放器,目前在学习阶段,demo和文章会根据学习进度与总结情况去更新,欢迎各位批评指正。
2020年8月1日更新3.2

二、相关知识点

  • AVPlayer本身并不显示视频!需要一个AVPlayerLayer播放层来显示视频,然后添加到父视图的layer中。
  • AVPlayer只负责视频管理和调控!而视频资源是由AVPlayerItem提供的,每个AVPlayerItem对应一个视频地址。
  • 工程内需要引入AVFoundation库
  • 持续补充……

2.1 AVplayerItem

(instancetype)playerItemWithURL:(NSURL *)URL; 初始化视频资源方法
duration 视频总时长
status 视频资源的状态 (需要监听的属性)
loadedTimeRanges 在线视频的缓冲进度 (需要监听的属性)
playbackBufferEmpty 进行跳转,无缓冲 (可选监听)
playbackLikelyToKeepUp 进行跳转,有缓冲 (可选监听)

2.2 AVplayer

(void) play;
(void) pause; 播放暂停
(void) seekToTime:(CMTime)time;跳转到指定时间
(CMTime) currentTime;获取当前播放进度
(void)removeTimeObserver:(id)observer;移除监听 (销毁播放器的时候调用)
(void)replaceCurrentItemWithPlayerItem:(nullable AVPlayerItem *)item;切换视频资源

2.3 AVPlayerLayer

videoGravity 视频的填充方式

  1. AVQueuePlayer:这个是用于处理播放列表的操作,待学习…

三、代码部分

3.1 单例

  1. 声明相关属性
// AVAsset对象
@property (nonatomic, strong) AVAsset *videoAsset;
// 播放器对象
@property (nonatomic, strong) AVPlayer *player;
// 播放属性 
@property (nonatomic, strong) AVPlayerItem *playerItem; 
// 播放容器,用于放playerlayer
@property (nonatomic, strong) UIView *videoView;  
// 用于播放的layer
@property (nonatomic, strong) AVPlayerLayer *playLayer; 
  1. 实现播放器
- (UIView *)createPlayViewFrame:(CGRect)frame withStringURL:(NSString *)stringURL isLocalFile:(BOOL)isTrue{
    if (isTrue == YES){
        self.videoAsset = [[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:stringURL] options:nil];
    } else  {
        self.videoAsset = [[AVURLAsset alloc] initWithURL:[NSURL URLWithString:stringURL] options:nil];
    }
    
    self.playerItem = [AVPlayerItem playerItemWithAsset:self.videoAsset];
    // 添加观察者
    [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    
    // 获取当前视频的尺寸
    [self getVideoSize:self.videoAsset];
    
    self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
    // 监听是否 播放完毕
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playBackFinished:withBlock:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem];
    
    
    self.playLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
    self.playLayer.frame = frame;
    // 填充模式
    self.playLayer.videoGravity = AVLayerVideoGravityResizeAspect;
    
    UIView *videoView = [[UIView alloc] initWithFrame:frame];
    [videoView.layer addSublayer:self.playLayer];
    
    return videoView;
}
  1. 调用
- (void)viewDidLoad {
    [super viewDidLoad];
    UIView *videoView = [[UIView alloc] initWithFrame:self.view.frame];
    videoView = [[SMZVideoManager sharedInstance] createPlayViewFrame:CGRectMake(0, 0, creenWidth, creenHeight) withStringURL:videoUrl isLocalFile:NO];
    [self.view addSubview:videoView];
    // 也可以将视频播放完毕的监听加在这里,需要将manager中的移除掉
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playBackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:[[SMZVideoManager sharedInstance] getCurrentPlayerItem]];
    
    [[SMZVideoManager sharedInstance] startVideo];
}
  1. 观察者监听相关
#pragma mark - 观察者相关
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"status"]){
        AVPlayerItem *playItem = (AVPlayerItem *)object;
        if (playItem.status ==  AVPlayerStatusReadyToPlay){
            // 可以播放
        } else if (playItem.status == AVPlayerStatusFailed){
            // 失败
        } else {
            // 未知错误AVPlayerStatusUnknown
        }
    }
}

- (void)playBackFinished:(NSNotification *)notification withBlock:(void (^)(void))code{
    // 播放完毕的操作,这里 提供重新播放的能力
    self.playerItem = [notification object];
    [self.playerItem seekToTime:kCMTimeZero completionHandler:nil];
    code();
}

3.2 将播放器封装成view

单例形式适合多个视频由一个播放器控制,但是上述写法明显存在问题 ,控制不了每个视图,所以现在想让每个视图有自己的播放器

// 相关方法
- (instancetype)initWithFrame:(CGRect)frame;

- (void)setupPlayerWith:(NSURL *)videoURL;

- (void)play;

- (void)pause;

- (void)replay;

- (void)destory;
// 相关实现
-  (instancetype)initWithFrame:(CGRect)frame{
    self = [super initWithFrame:frame];
    if (self){
        self.backgroundColor = [UIColor whiteColor];
    }
    return self;
}

// 准备播放器
- (void)setupPlayerWith:(NSURL *)videoURL{
    AVURLAsset *sset = [[AVURLAsset alloc] initWithURL:videoURL options:nil];
    self.videoScale = [self getVideoSize:sset];
    [self creatPlayer:videoURL];
}


// 获取播放item
- (AVPlayerItem *)getPlayerItem:(NSURL *)videoURL {
    AVPlayerItem *item = [AVPlayerItem playerItemWithURL:videoURL];
    
    return item;
}


//  创建播放器
- (void)creatPlayer:(NSURL *)videoURL {
    if (!_player) {
        
        self.currentItem = [self getPlayerItem:videoURL];
        
        _player = [AVPlayer playerWithPlayerItem:self.currentItem];
        
        [self creatPlayerLayer];
        
        [self addPlayerObserver];
        
        [self addObserverWithPlayItem:self.currentItem];
        
        [self addNotificatonForPlayer];
    }
}

// 创建视图
- (void)creatPlayerLayer {
    CGFloat origin_x = 15;
    CGFloat main_width = kScreenWidth - (origin_x * 2);

    self.videoView = [[UIView alloc] initWithFrame:CGRectMake(origin_x, 85, main_width, main_width * self.videoScale)];
    
    AVPlayerLayer *layer = [AVPlayerLayer playerLayerWithPlayer:self.player];

    layer.frame = CGRectMake(0, 0, self.videoView.frame.size.width, self.videoView.frame.size.height);
    
    layer.videoGravity = AVLayerVideoGravityResizeAspect;

    [self.videoView.layer addSublayer:layer];

    [self addSubview:self.videoView];
    
}

// 获取视频的比例
- (CGFloat)getVideoSize:(AVAsset *)videoAsset{
    NSArray *array = videoAsset.tracks;
    CGSize videoSize = CGSizeZero;
    for(AVAssetTrack  *track in array)
    {
        if([track.mediaType isEqualToString:AVMediaTypeVideo])
        {
              videoSize = track.naturalSize;
        }
    }
    CGFloat videoH = videoSize.height;
    CGFloat videoW = videoSize.width;
    return videoH / videoW;
}

#pragma mark - 播放器功能
// 播放
- (void)play {
    if (self.player.rate == 0) {
        [self addNotificatonForPlayer];
        [self addPlayerObserver];
    }
    [self.player play];
}

// 暂停
- (void)pause {
    if (self.player.rate == 1.0) {
        [self.player pause];
        [self.player seekToTime:kCMTimeZero];
    }
}

// 重新开始
- (void)replay {
    if (self.player.rate == 1.0) {
        [self.player pause];
        [self.player seekToTime:kCMTimeZero];
        [self removeNotification];
    } else if (self.player.rate == 0){
        [self addNotificatonForPlayer];
        [self play];
    }
}

- (void)destory {
    [self pause];
    [self removeNotification];
    [self removePlayerObserver];
}

#pragma mark - 添加 监控
// 给player 添加 time observer
- (void)addPlayerObserver {
    __weak typeof(self)weakSelf = self;
    _timeObser = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
        AVPlayerItem *playerItem = weakSelf.player.currentItem;
        
        float current = CMTimeGetSeconds(time);
        
        float total = CMTimeGetSeconds([playerItem duration]);

        NSLog(@"当前播放进度 %.2f/%.2f.",current,total);
        
    }];
}
// 移除 time observer
- (void)removePlayerObserver {
    [_player removeTimeObserver:_timeObser];
}

- (void)addObserverWithPlayItem:(AVPlayerItem *)item {
    [item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    [item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    [item addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
    [item addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
}
//  移除 item 的 observer
- (void)removeObserverWithPlayItem:(AVPlayerItem *)item {
    [item removeObserver:self forKeyPath:@"status"];
    [item removeObserver:self forKeyPath:@"loadedTimeRanges"];
    [item removeObserver:self forKeyPath:@"playbackBufferEmpty"];
    [item removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    AVPlayerItem *item = object;
    if ([keyPath isEqualToString:@"status"]) {// 播放状态
        
        [self handleStatusWithPlayerItem:item];
        
    }
}

- (void)handleStatusWithPlayerItem:(AVPlayerItem *)item {
    AVPlayerItemStatus status = item.status;
    switch (status) {
        case AVPlayerItemStatusReadyToPlay:   // 准备好播放
            NSLog(@"AVPlayerItemStatusReadyToPlay");
            
            break;
        case AVPlayerItemStatusFailed:        // 播放出错
            
            NSLog(@"AVPlayerItemStatusFailed");
            
            break;
        case AVPlayerItemStatusUnknown:       // 状态未知
            
            NSLog(@"AVPlayerItemStatusUnknown");
            
            break;
            
        default:
            break;
    }
    
}

- (void)addNotificatonForPlayer {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    
    [center addObserver:self selector:@selector(videoPlayEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
    [center addObserver:self selector:@selector(videoPlayError:) name:AVPlayerItemPlaybackStalledNotification object:nil];
    [center addObserver:self selector:@selector(videoPlayEnterBack:) name:UIApplicationDidEnterBackgroundNotification object:nil];
    [center addObserver:self selector:@selector(videoPlayBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
}
// 移除 通知
- (void)removeNotification {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    
    [center removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
//    [center removeObserver:self name:AVPlayerItemTimeJumpedNotification object:nil];
    [center removeObserver:self name:AVPlayerItemPlaybackStalledNotification object:nil];
    [center removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
    [center removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
    [center removeObserver:self];
}

// 视频播放结束
- (void)videoPlayEnd:(NSNotification *)notification {
    NSLog(@"视频播放结束");
//    self.currentItem = [notic object];
//    [self.currentItem seekToTime:kCMTimeZero completionHandler:nil];
//    [self.player play];
    [self.player seekToTime:kCMTimeZero];
//    [self.player play];
}

// 视频异常中断
- (void)videoPlayError:(NSNotification *)notification {
    NSLog(@"视频异常中断");
}
// 进入后台
- (void)videoPlayEnterBack:(NSNotification *)notification {
    NSLog(@"进入后台");
}
// 返回前台
- (void)videoPlayBecomeActive:(NSNotification *)notification {
    NSLog(@"返回前台");
}

#pragma mark - 销毁 release
- (void)dealloc {
    NSLog(@"--- %@ --- 销毁了",[self class]);
    
    [self removeNotification];
    [self removePlayerObserver];
    [self removeObserverWithPlayItem:self.player.currentItem];
    
}

四、demo

文章只是记录一下相关部分代码,会在慢慢进行改善。
demo会持续更新,完善功能 ,一次比一次好。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
iOS AVPlayer 是苹果官方提供的一个强大的音视频播放器框架。AVPlayer 提供了丰富的功能和接口,可以用来播放各种不同格式的音视频文件。 iOS AVPlayer 播放器的 UI 取决于个人的设计需求和实现方式。一般情况下,可以通过 AVPlayerLayer 来显示视频画面,并在其上添加一些自定义的控件来实现播放器的 UI。 可以使用 AVPlayerLayer 来创建一个视图层用于显示视频,然后将其添加到你的视图层级。你可以设置 AVPlayerLayer 的 frame 属性来确定视频画面的位置和大小。 除了视频画面的显示,你还可以添加一些控件来实现播放器的控制功能。比如可以添加一个播放/暂停按钮、一个进度条显示当前播放的进度、一个音量调节控件等等。这些控件可以通过与 AVPlayer 进行交互来实现不同功能。 在实现过程,你可以利用 AVPlayer 的一些相关属性和方法来控制播放器的状态。比如可以使用 play() 方法来播放视频,pause() 方法来暂停视频的播放,并且可以通过 KVO(键值观察)机制来监听播放器的状态变化,以及通过观察 AVPlayerItem 的属性来获取视频的总时长等信息。 另外,你还可以根据需要进行一些自定义的 UI 设计来美化播放器的外观,比如更改按钮的样式、调整进度条的颜色等等,以增强用户体验。 综上所述,iOS AVPlayer 播放器的 UI 取决于你个人的设计需求和实现方式,可以通过 AVPlayerLayer 实现视频画面的显示,通过添加自定义控件来实现播放器的控制功能,并根据需要进行一些自定义的 UI 设计。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值