【iOS】AVPlayer 播放音视频

1、常见的音视频播放器

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

常用的音频播放器有 AVAudioPlayer、AVPlayer 等。不同的是,AVAudioPlayer 只支持本地音频的播放,而 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地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容