美文网首页音视频播放
【iOS】AVPlayer 播放音视频

【iOS】AVPlayer 播放音视频

作者: 阳光下的叶子呵 | 来源:发表于2022-09-27 19:01 被阅读0次

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地址

相关文章

  • AVPlayer那些坑

    AVPlayer是iOS上常用的视频播放器组件,支持常见的音视频格式。对于一般应用而言,使用AVPlayer和一套...

  • iOS音频篇-AVPlayer

    iOS音频篇:使用AVPlayer播放网络音乐 iOS音频篇:AVPlayer的缓存实现

  • [iOS]调和 pop 手势导致 AVPlayer 播放卡顿

    [iOS]调和 pop 手势导致 AVPlayer 播放卡顿 [iOS]调和 pop 手势导致 AVPlayer ...

  • avplayer

    参考1.iOS通过AVPlayer打造自己的视频播放器2.基于 AVPlayer 自定义播放器3.AVPlayer...

  • iOS 音频

    iOS 音频播放 本地音频文件 AVPlayer / AVAudioPlayer 在线音频流 AVPlayer i...

  • iOS-AVPlayer音视频播放

    https://www.cnblogs.com/QianChia/p/5771172.html

  • 【iOS】AVPlayer 播放音视频

    1、常见的音视频播放器 iOS开发中不可避免地会遇到音视频播放方面的需求。 常用的音频播放器有 AVAudioPl...

  • 【iOS】AVPlayer 播放音视频

    1、常见的音视频播放器 iOS开发中不可避免地会遇到音视频播放方面的需求。 常用的音频播放器有 AVAudioPl...

  • 音视频知识梳理

    一、整体播放策略 (1)iOS自带的播放控件- AVPlayer、AVQueuePlayer只能通过路径播放,可以...

  • AVPlayer封装

    最近在写关于音视频播放的案例,所以就趁机会研究了一下AVPlayer的内容。我封装的目前只能播放网络音视频。还未添...

网友评论

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

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