美文网首页iOS专题iOS开发iOS学习笔记
【iOS】AVPlayer 播放音视频

【iOS】AVPlayer 播放音视频

作者: 焚雪残阳 | 来源:发表于2017-07-22 14:46 被阅读266次

    1、常见的音视频播放器

    iOS开发中不可避免地会遇到音视频播放方面的需求。

    常用的音频播放器有 AVAudioPlayer、AVPlayer 等。不同的是,AVAudioPlayer 只支持本地音频的播放,而 AVPlayer 既支持本地音频播放,也支持网络音频播放。

    常用的视频播放器有 MPMoviePlayerController、AVPlayer 等。不同的是,MPMoviePlayerController 内部做了高度封装,包含了播放控件,几乎不用写几行代码就能完成一个播放器,但是正是由于它的高度封装使得要自定义这个播放器变得很复杂,甚至是不可能完成。而 AVPlayer 更加接近于底层,所以灵活性也更强,更加方便自定义。

    今天我们要介绍的主角就是强大的 AVPlayer。

    2、AVPlayer

    AVPlayer 存在于 AVFoundation 框架中,所以要使用 AVPlayer,要先在工程中导入 AVFoundation 框架。

    AVPlayer 播放界面中不带播放控件,想要播放视频,必须要加入 AVPlayerLayer 中,并添加到其他能显示的 layer 当中。

    AVPlayer 中音视频的播放、暂停功能对应着两个方法 playpause 来实现。

    大多播放器都是通过通知来获取播放器的播放状态、加载状态等,而 AVPlayer 中对于获得播放状态和加载状态有用的通知只有一个:AVPlayerItemDidPlayToEndTimeNotification(播放完成通知) 。播放器的播放状态判断可以通过播放器的播放速度 rate 来获得,如果 rate 为0说明是停止状态,为1时则是正常播放状态。想要获取视频播放情况、缓冲情况等的实时变化,可以通过 KVO 监控 AVPlayerItem 的 statusloadedTimeRanges 等属性来获得。当 AVPlayerItem 的 status 属性为 AVPlayerStatusReadyToPlay 时说明可以开始播放,只有处于这个状态时才能获得视频时长等信息;当 loadedTimeRanges 改变时(每缓冲一部分数据就会更新此属性),可以获得本次缓冲加载的视频范围(包含起始时间、本次加载时长),这样一来就可以实时获得缓冲情况。

    AVPlayer 中播放进度的获取通常是通过:- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block 方法。这个方法会在设定的时间间隔内定时更新播放进度,通过 time 参数通知客户端。至于播放进度的跳转则是依靠 - (void)seekToTime:(CMTime)time 方法。

    AVPlayer 还提供了 - (void)replaceCurrentItemWithPlayerItem:(AVPlayerItem *)item 方法用于在不同视频之间的切换(事实上在AVFoundation内部还有一个AVQueuePlayer专门处理播放列表切换,有兴趣的朋友可以自行研究,这里不再赘述)。

    3、自定义AVPlayer

    下面是我自己在项目中封装的音视频播放器,贴上代码,大家可以参考一下。

    #import <UIKit/UIKit.h>
    #import <Foundation/Foundation.h>
    #import <AVFoundation/AVFoundation.h>
    
    /**
     播放器开始播放的通知
     当存在多个播放器,可使用该通知在其他播放器播放时暂停当前播放器
     */
    extern NSString * const YDPlayerDidStartPlayNotification;
    
    /**
     enum 播放器状态
    
     - YDPlayerStatusUnknown: 未知
     - YDPlayerStatusPlaying: 播放中
     - YDPlayerStatusLoading: 加载中
     - YDPlayerStatusPausing: 暂停中
     - YDPlayerStatusFailed: 播放失败
     - YDPlayerStatusFinished: 播放完成
     */
    typedef NS_ENUM(NSInteger, YDPlayerStatus) {
        YDPlayerStatusUnknown,
        YDPlayerStatusPlaying,
        YDPlayerStatusLoading,
        YDPlayerStatusPausing,
        YDPlayerStatusFailed,
        YDPlayerStatusFinished
    };
    
    
    @interface YDPlayerMananger : NSObject
    
    /**
     播放器
     */
    @property (nonatomic, strong) AVPlayer *player;
    
    /**
     播放器layer层
     */
    @property (nonatomic, strong) AVPlayerLayer *playerLayer;
    
    /**
     当前PlayerItem
     */
    @property (nonatomic, strong) AVPlayerItem *currentItem;
    
    /**
     播放器状态
     */
    @property (nonatomic, assign) YDPlayerStatus playStatus;
    
    /**
     Item总时长回调
     */
    @property (nonatomic, copy) void(^currentItemDurationCallBack)(AVPlayer *player, CGFloat duration);
    
    /**
     Item播放进度回调
     */
    @property (nonatomic, copy) void(^currentPlayTimeCallBack)(AVPlayer *player, CGFloat time);
    
    /**
     Item缓冲进度回调
     */
    @property (nonatomic, copy) void(^currentLoadedTimeCallBack)(AVPlayer *player, CGFloat time);
    
    /**
     Player状态改变回调
     */
    @property (nonatomic, copy) void(^playStatusChangeCallBack)(AVPlayer *player, YDPlayerStatus status);
    
    
    /**
     初始化方法
     
     @param url 播放链接
     @return YDPlayerMananger对象
     */
    - (instancetype)initWithURL:(NSURL *)url;
    
    
    /**
     创建单例对象
    
     @return YDPlayerMananger单例对象
     */
    + (instancetype)shareManager;
    
    
    /**
     将播放器展示在某个View
     
     @param view 展示播放器的View
     */
    - (void)showPlayerInView:(UIView *)view withFrame:(CGRect)frame;
    
    
    /**
     替换PlayerItem
    
     @param url 需要播放的链接
     */
    - (void)replaceCurrentItemWithURL:(NSURL *)url;
    
    
    /**
     播放某个链接
    
     @param urlStr 需要播放的链接
     */
    - (void)playWithUrl:(NSString *)urlStr;
    
    
    /**
     开始播放
     */
    - (void)play;
    
    
    /**
     暂停播放
     */
    - (void)pause;
    
    
    /**
     停止播放
     */
    - (void)stop;
    
    
    /**
     跳转到指定时间
    
     @param time 指定的时间
     */
    - (void)seekToTime:(CGFloat)time;
    
    @end
    
    #import "YDPlayerMananger.h"
    
    NSString * const YDPlayerDidStartPlayNotification = @"YDPlayerDidStartPlayNotification";
    
    @interface YDPlayerMananger ()
    @property (nonatomic, strong) id timeObserver; // 监控播放进度的观察者
    
    @end
    
    @implementation YDPlayerMananger
    
    #pragma mark - 生命周期
    
    - (instancetype)init
    {
        if (self = [super init]) {
            AVAudioSession *audioSession = [AVAudioSession sharedInstance];
            [audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
            [audioSession setActive:YES error:nil];
            self.player = [[AVPlayer alloc] init];
            [self addNotificationAndObserver];
        }
        return self;
    }
    
    - (instancetype)initWithURL:(NSURL *)url
    {
        if (self = [self init]) {
            [self replaceCurrentItemWithURL:url];
        }
        return self;
    }
    
    + (instancetype)shareManager
    {
        static YDPlayerMananger *manager = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            manager = [[self alloc] init];
        });
        return manager;
    }
    
    - (void)dealloc
    {
        [self removeNotificationAndObserver];
    }
    
    #pragma mark - 公开方法
    
    - (void)showPlayerInView:(UIView *)view withFrame:(CGRect)frame
    {
        self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
        _playerLayer.frame = frame;
        _playerLayer.backgroundColor = [UIColor blackColor].CGColor;
        _playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
        [view.layer addSublayer:_playerLayer];
    }
    
    - (void)replaceCurrentItemWithURL:(NSURL *)url
    {
        // 移除当前观察者
        if (_currentItem) {
            [_currentItem removeObserver:self forKeyPath:@"status"];
            [_currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
        }
        _currentItem = [[AVPlayerItem alloc] initWithURL:url];
        [self.player replaceCurrentItemWithPlayerItem:_currentItem];
        
        // 重新添加观察者
        [_currentItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
        [_currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    }
    
    - (void)playWithUrl:(NSString *)urlStr
    {
        [self replaceCurrentItemWithURL:[NSURL URLWithString:urlStr]];
        [self play];
    }
    
    - (void)play
    {
        [self.player play];
        self.playStatus = YDPlayerStatusPlaying;
        // 发起开始播放的通知
        [[NSNotificationCenter defaultCenter] postNotificationName:YDPlayerDidStartPlayNotification object:_player];
    }
    
    - (void)pause
    {
        [self.player pause];
        self.playStatus = YDPlayerStatusPausing;
    }
    
    - (void)stop
    {
        [self.player pause];
        [_currentItem cancelPendingSeeks];
        self.playStatus = YDPlayerStatusFinished;
    }
    
    - (void)seekToTime:(CGFloat)time
    {
        [_currentItem seekToTime:CMTimeMakeWithSeconds(time, 1.0) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
    }
    
    #pragma mark - 私有方法
    
    // 添加通知、观察者
    - (void)addNotificationAndObserver
    {
        // 添加播放完成通知
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
        // 添加打断播放的通知
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(interruptionComing:) name:AVAudioSessionInterruptionNotification object:nil];
        // 添加插拔耳机的通知
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChanged:) name:AVAudioSessionRouteChangeNotification object:nil];
        // 添加观察者监控播放器状态
        [self addObserver:self forKeyPath:@"playStatus" options:NSKeyValueObservingOptionNew context:nil];
        // 添加观察者监控进度
        __weak typeof(self) weakSelf = self;
        _timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
            __strong typeof(self) strongSelf = weakSelf;
            
            if (strongSelf.currentPlayTimeCallBack) {
                float currentPlayTime = (double)strongSelf.currentItem.currentTime.value / strongSelf.currentItem.currentTime.timescale;
                strongSelf.currentPlayTimeCallBack(strongSelf.player, currentPlayTime);
            }
        }];
    }
    
    // 移除通知、观察者
    - (void)removeNotificationAndObserver
    {
        [[NSNotificationCenter defaultCenter] removeObserver:self];
        [self removeObserver:self forKeyPath:@"playStatus"];
        [_player removeTimeObserver:_timeObserver];
        if (_currentItem) {
            [_currentItem removeObserver:self forKeyPath:@"status"];
            [_currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
        }
    }
    
    #pragma mark - 观察者
    
    // 观察者
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    {
        if ([keyPath isEqualToString:@"status"]) {
            
            AVPlayerStatus status = [[change objectForKey:@"new"] intValue];
            
            if (status == AVPlayerStatusReadyToPlay) {
                // 获取视频长度
                if (self.currentItemDurationCallBack) {
                    CGFloat duration = CMTimeGetSeconds(_currentItem.duration);
                    self.currentItemDurationCallBack(_player, duration);
                }
                
            } else if (status == AVPlayerStatusFailed) {
                self.playStatus = YDPlayerStatusFailed;
            } else {
                self.playStatus = YDPlayerStatusUnknown;
            }
            
        } else if ([keyPath isEqualToString:@"playStatus"]) {
            
            if (self.playStatusChangeCallBack) {
                self.playStatusChangeCallBack(_player, _playStatus);
            }
        } else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
            
            // 计算缓冲总进度
            NSArray *loadedTimeRanges = [_currentItem loadedTimeRanges];
            CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue];
            float startSeconds = CMTimeGetSeconds(timeRange.start);
            float durationSeconds = CMTimeGetSeconds(timeRange.duration);
            NSTimeInterval loadedTime = startSeconds + durationSeconds;
    
            if (self.playStatus == YDPlayerStatusPlaying && self.player.rate <= 0) {
                self.playStatus = YDPlayerStatusLoading;
            }
            
            // 卡顿时缓冲完成后自动播放
            if (self.playStatus == YDPlayerStatusLoading) {
                NSTimeInterval currentTime = self.player.currentTime.value / self.player.currentTime.timescale;
                if (loadedTime > currentTime + 5) {
                    [self play];
                }
            }
            
            if (self.currentLoadedTimeCallBack) {
                self.currentLoadedTimeCallBack(_player, loadedTime);
            }
        }
    }
    
    #pragma mark - 通知
    
    // 播放完成通知
    - (void)playbackFinished:(NSNotification *)notification
    {
        AVPlayerItem *playerItem = (AVPlayerItem *)notification.object;
        if (playerItem == _currentItem) {
            self.playStatus = YDPlayerStatusFinished;
        }
    }
    
    // 插拔耳机通知
    - (void)routeChanged:(NSNotification *)notification
    {
        NSDictionary *dic = notification.userInfo;
        int changeReason = [dic[AVAudioSessionRouteChangeReasonKey] intValue];
        // 旧输出不可用
        if (changeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
            AVAudioSessionRouteDescription *routeDescription = dic[AVAudioSessionRouteChangePreviousRouteKey];
            AVAudioSessionPortDescription *portDescription = [routeDescription.outputs firstObject];
            // 原设备为耳机则暂停
            if ([portDescription.portType isEqualToString:@"Headphones"]) {
                [self pause];
            }
        }
    }
    
    // 来电、闹铃打断播放通知
    - (void)interruptionComing:(NSNotification *)notification
    {
        NSDictionary *userInfo = notification.userInfo;
        AVAudioSessionInterruptionType type = [userInfo[AVAudioSessionInterruptionTypeKey] intValue];
        if (type == AVAudioSessionInterruptionTypeBegan) {
            [self pause];
        }
    }
    
    @end
    

    4、注意点

    在使用 AVPlayer 时需要注意的是,由于播放状态、缓冲状态等是通过 KVO 监控 AVPlayerItem 的 status、loadedTimeRanges 等属性来获得的,在使用 - (void)replaceCurrentItemWithPlayerItem:(AVPlayerItem *)item 切换视频后,当前的 AVPlayerItem 实际上已经被释放掉了,所以一定要及时移除观察者并重新添加,否则会引起崩溃。

    如果有大神发现文章中的错误,欢迎指正。有兴趣下载文中 Demo 的朋友,可以前往我的GitHub:GitHud地址

    相关文章

      网友评论

      本文标题:【iOS】AVPlayer 播放音视频

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