既支持本地音频播放,不仅能够播放音频

作者: 编程  发布:2019-09-19

最近项目需求做个播放视频功能,之前对这方面接触的也不多,阅读了一些开源播放器的源码学习了一下,总结了一些使用方法,主要讲述使用AVPlayer播放网络音乐

1、常见的音视频播放器

AVPlayer属于AVFoundation框架,不仅能够播放音频,还可以播放视频,支持本地和网链,更加接近底层,定制也更加灵活。

AVPlayer介绍

iOS系统中音频的四种播放方式

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

为什么要写这篇文章呢?其因有二:

AVPlayer属于AVFoundation框架,它的强大之处在于,不仅能够播放音频,还可以播放视频,支持本地和网链,而且使用起来非常方便.

1)AVAudioPlayer 在<AVFoundation/AVFoundation.h>框架里面 使用简单方便,但只能播放本地音频,不支持流媒体播放,每一个audioplayer对象就是一段音频 2) AVPlayer 也在 在<AVFoundation/AVFoundation.h>框架里面 iOS 4.0以后,可以使用AVPlayer播放本地音频和支持流媒体播放,但提供接口较少,处理音频不够灵活 3)系统声音 在<AudioToolbox/AudioToolbox.h>框架里面 音频数据文件可分为压缩和非压缩的存储类型。压缩的音频文件虽然文件体积较小,但需要耗费处理器的性能进行解压和解码。如果音频文件体积较小,压缩后的音频文件,也不会节省较大的磁盘空间。像这一类小型非压缩的文件可以注册成为系统声音 格式为:caf/wav/aiff格式,且时长小于30s 4)音频队列 音频队列主要处理流媒体播放,提供了强大且灵活的API接口,但处理起来较为复杂

 // 1.获取要播放音频文件的URL NSURL *fileURL = [[NSBundle mainBundle]URLForResource:@"刘若英 - 原来你也在这里" withExtension:@".mp3"]; // 2.创建 AVAudioPlayer 对象 AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc]initWithContentsOfURL:fileURL error:nil]; // 3.打印歌曲信息 NSString *msg = [NSString stringWithFormat:@"音频文件声道数:%ldn 音频文件持续时间:%g",audioPlayer.numberOfChannels,audioPlayer.duration]; NSLog(@"%@",msg); // 4.设置循环播放 audioPlayer.numberOfLoops = -1; audioPlayer.delegate = self; // 5.开始播放 [audioPlayer play];

1、需导入引入AudioToolbox框架2、使用实例 NSBundle *bundle = [NSBundle mainBundle]; NSString *path = [bundle pathForResource:@"44th Street Medium" ofType:@"caf"]; // 初始化本地文件url NSURL *url = [NSURL fileURLWithPath:path]; UInt32 soundID; // 将URL所在的音频文件注册为系统声音,soundID音频ID标示该音频 AudioServicesCreateSystemSoundID((__bridge CFURLRef)url, &soundID); // 播放音频 AudioServicesPlaySystemSound; //播放系统震动 AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); //销毁声音 AudioServicesDisposeSystemSoundID;

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

  • 1、github上有很多播放音频的优秀三方的框架,很方便,也很容易集成,但问题也比较多,底层都是C或者C++,要修改一个小BUG,难度系数比较高,例如:
    StreamingKit
    FreeStreamer
    AudioStreamer
    AFSoundManager
    DOUAudioStreamer
    以上,除了FreeStreamer,其他的几个框架都使用过,其中StreamingKit和DOUAudioStreamer在线上版本使用过,性能都比较不错。

AVPlayer之音频

AVPlayer存在于AVFoundation中,其实它是一个视频播放器,不仅能够播放音频,还可以播放视频,支持本地和网链,更加接近底层,定制也更加灵活。

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

DOUAudioStreamer 唯一的问题就是在使用缓存继续播放,有问题,并且不支持seekToTime,这句话的意思呢就是,整个音频缓冲完毕才能继续播放,如果快进的时候整个音频没有缓冲完成(网络较差的时候,音频较大),这个就比较坑了。

StreamingKit 这个有个问题就是缓冲的进度无法回调,无法获取,源码都是互斥锁,自旋锁,看着各种晕菜,有个问题就是,可能是我使用的姿势不对,缓存文件比较大,但是在沙盒又找不见这个文件的存在,无法删除,API也没有提供对应的接口,在手机的空间存储中查看,占用空间很大。

  1. 使用AVPlayer播放音频必须知道的三个类
    1.1 AVPlayer : 可以理解成播放器
    1.2 AVPlayerItem : 播放器需要播放的资源,比如一首歌曲
    1.3 CMTime : 记录AVPlayerItem资源的播放进度以及这个资源的其他信息,当你需要显示播放进度的时候可以用到它,它本身是个结构体

  2. 音频播放示例
    2.1 说明 : 此处只介绍一下简单的使用过程
    2.2 代码 :

AVPlayer首先了解一下几个常用的类:

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

  • 2、这些框架很久都没人维护,面对现在的产品需求,不能满足。

AVPlayerManager.h
#import <Foundation/Foundation.h>

1、AVAsset:AVAsset类专门用于获取多媒体的相关信息,包括获取多媒体的画面、声音等信息,属于一个抽象类,不能直接使用。2、AVURLAsset:AVAsset的子类,可以根据一个URL路径创建一个包含媒体信息的AVURLAsset对象。3、AVPlayerItem:一个媒体资源管理对象,管理者视频的一些基本信息和状态,一个AVPlayerItem对应着一个视频资源。4、AVPlayer:播放器。5、CMTime:是一个结构体,里面存储着当前的播放进度,总的播放时长。

2、AVPlayer

基于以上原因,结合自己项目中出现的问题,决定用强悍的AV框架中AVPlayer,自己写一个, 功能如下:

@interfaceAVPlayerManager : NSObject
+ (instancetype)shareManager;
- (void)musicPlayerWithURL:(NSURL *)playerItemURL;
- (void)pause;
@end

1、实例化一个AVPlayer:

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

不要求实现流播
能支持缓存播放
有缓存进度回调
可以清除缓存文件即可

AVPlayerManager.m

- (AVPlayer *)player { if (_player == nil) { _player = [[AVPlayer alloc] init]; _player.volume = 1.0; // 默认最大音量 } return _player;}

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

AVPlayer基础用法介绍

以前做视频开发,在播放视频时,只是简单的播放一个视频,而不需要考虑播放器的界面。

1、iOS9.0 之前使用 MPMoviePlayerController, 或者自带一个 view 的 MPMoviePlayerViewController。
2、iOS9.0 之后,可以使用新的API AVPictureInPictureController, AVPlayerViewController。
3、甚至使用WKWebView。

以上播放器都是系统提供,优点:封装性很强,使用简单方便。缺点:自由定制度太低。
所以在我们需要自己定制播放器的时候,就需要时用AVPlayer

AVPlayer继承NSObject,所以单独使用AVPlayer时无法显示视频的,必须将视频图层添加到AVPlayerLayer中方能显示视频。使用AVPlayer首先了解一下几个常用的类:

1、AVAsset:AVAsset类专门用于获取多媒体的相关信息,包括获取多媒体的画面、声音等信息,属于一个抽象类,不能直接使用。
2、AVURLAsset:AVAsset的子类,可以根据一个URL路径创建一个包含媒体信息的AVURLAsset对象。
3、AVPlayerItem:一个媒体资源管理对象,管理者视频的一些基本信息和状态,一个AVPlayerItem对应着一个视频资源。
4、AVPlayer:播放器。
5、CMTime9159.com ,:是一个结构体,里面存储着当前的播放进度,总的播放时长。

一般项目中会初始化一个播放管理的工具类(单例):

#import "AVPlayerManager.h"
#import <AVFoundation/AVFoundation.h>
@interface AVAudioManager(){
    BOOL isPlaying;//是否正在播放
    BOOL isPrepare;//资源是否准备完毕
}
@property (nonatomic, strong) AVPlayer *player;//播放器
@end
@implementation AVAudioManager

//单例
+ (instancetype)shareManager{
    static AVPlayerManager *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [AVPlayerManager new];
    });
    return manager;
}

//播放音频的方法(下面会在控制器调用)
- (void)musicPlayerWithURL:(NSURL *)playerItemURL{
    //创建要播放的资源
    AVPlayerItem *playerItem = [[AVPlayerItem alloc]initWithURL:playerItemURL];
    //添加观察者
    //当资源的status发生改变时就会触发观察者事件
    [playerItem addObserver:self forKeyPath:@"status" options:(NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew) context:nil];
    //播放当前资源
    [self.player replaceCurrentItemWithPlayerItem:playerItem];

}


//播放
- (void)play{
    if (!isPrepare) {
        return;
    }
    [self.player play];
    isPlaying = YES;
}
//暂停
- (void)pause{
    if (!isPlaying) {
        return;
    }
    [self.player pause];
    isPlaying = NO;
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    AVPlayerItemStatus status = [change[@"new"] integerValue];
    switch (status) {
        case AVPlayerItemStatusReadyToPlay:
            isPrepare = YES;
            [self play];
            break;
        case AVPlayerItemStatusFailed:
            NSLog(@"加载失败");
            break;
        case AVPlayerItemStatusUnknown:
            NSLog(@"未知资源");
            break;
        default:
            break;
    }
}

//播放器懒加载
-(AVPlayer *)player{
    if (!_player) {
        _player = [AVPlayer new];
    }
    return _player;
}

@end



#import "ViewController.h"
#import "AVPlayerManager.h"
@interface ViewController ()

@end

播放音频相关状态监听和移除

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

1、实例化一个AVPlayer:

- (AVPlayer *)player {
    if (_player == nil) {
        _player = [[AVPlayer alloc] init];
        _player.volume = 1.0; // 默认最大音量
    }
    return _player;
}

ViewController.m中调用单例里的方法

1、监听status,AVPlayerItemStatus有三种状态:2、监听loadedTimeRanges,这个就是缓冲进度,可以进行缓冲进度条的设置3、AVPlayerItemDidPlayToEndTimeNotification,注册这个通知,当播放器播放完成的时候进行回调。4、addPeriodicTimeObserverForInterval,监听当前播放进度。

//播放音频的方法(下面会在控制器调用)- p_musicPlayerWithURL:playerItemURL{ // 移除监听 [self p_currentItemRemoveObserver]; // 创建要播放的资源 AVPlayerItem *playerItem = [[AVPlayerItem alloc]initWithURL:playerItemURL]; // 播放当前资源 [self.player replaceCurrentItemWithPlayerItem:playerItem]; // 添加观察者 [self p_currentItemAddObserver];}- p_playerAddObserver { [self.player addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil]; [self.player addObserver:self forKeyPath:@"currentItem" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];}- p_playerRemoveObserver { [self.player removeObserver:self forKeyPath:@"rate"]; [self.player removeObserver:self forKeyPath:@"currentItem"];}- p_currentItemAddObserver { //监控状态属性,注意AVPlayer也有一个status属性,通过监控它的status也可以获得播放状态 [self.player.currentItem addObserver:self forKeyPath:@"status" options:(NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew) context:nil]; //监控缓冲加载情况属性 [self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil]; //监控播放完成通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem]; __weak typeof weakSelf = self; //监控时间进度 self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { NSLog(@"%@---%@",[weakSelf currentTimeStr],[weakSelf durationStr]); }];}- p_currentItemRemoveObserver { [self.player.currentItem removeObserver:self forKeyPath:@"status"]; [self.player.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"]; [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; [self.player removeTimeObserver:self.timeObserver];}#pragma mark - KVO- observeValueForKeyPath:(NSString *)keyPath ofObject:object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:context { AVPlayerItem *playerItem = object; if ([keyPath isEqualToString:@"status"]) { AVPlayerItemStatus status = [change[@"new"] integerValue]; switch  { case AVPlayerItemStatusReadyToPlay: { // 开始播放 [self play]; // 保存我听过的// [self p_saveHistory];// // 代理回调,开始初始化状态// if (self.delegate && [self.delegate respondsToSelector:@selector(startPlayWithplayer:)]) {// [self.delegate startPlayWithplayer:self.player];// } } break; case AVPlayerItemStatusFailed: { NSLog; } break; case AVPlayerItemStatusUnknown: { NSLog; } break; default: break; } } else if([keyPath isEqualToString:@"loadedTimeRanges"]){ NSArray *array=playerItem.loadedTimeRanges; //本次缓冲时间范围 CMTimeRange timeRange = [array.firstObject CMTimeRangeValue]; float startSeconds = CMTimeGetSeconds(timeRange.start); float durationSeconds = CMTimeGetSeconds(timeRange.duration); //缓冲总长度 NSTimeInterval totalBuffer = startSeconds + durationSeconds; NSLog(@"共缓冲:%.2f",totalBuffer);// if (self.delegate && [self.delegate respondsToSelector:@selector(updateBufferProgress:)]) {// [self.delegate updateBufferProgress:totalBuffer];// } } else if ([keyPath isEqualToString:@"rate"]) { float rate = self.player.rate; NSLog(@"%f---rate",rate);// if (self.delegate && [self.delegate respondsToSelector:@selector(player:changeRate:)]) {// [self.delegate player:self.player changeRate:rate];// } } else if ([keyPath isEqualToString:@"currentItem"]) { NSLog(@"新的currentItem");// if (self.delegate && [self.delegate respondsToSelector:@selector(changeNewPlayItem:)]) {// [self.delegate changeNewPlayItem:self.player];// } }}- playbackFinished:(NSNotification *)notifi { NSLog;// // 需要自动播放下一首 if (self.playingSortType == KSPlayingSortTypeSequence1) { // 播放列表中的最后一个故事 if (self.currentVoiceIndex == self.voices.count-1) { [self pause]; [self playAnyVoiceWithIndex:0]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self pause]; [[NSNotificationCenter defaultCenter] postNotificationName:@"kKSPlayAlreadyStopNotification" object:nil userInfo:nil]; }); } else { [self playNext]; } } else if (self.playingSortType == KSPlayingSortTypeSingleloop1) { [self playAnyVoiceWithIndex:self.currentVoiceIndex]; } else { [self playNext]; }}

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

2、播放一个音频(本地和网络都可以)

//播放音频的方法
- (void)p_musicPlayerWithURL:(NSURL *)playerItemURL{
    // 移除监听
    [self p_currentItemRemoveObserver];
    // 创建要播放的资源
    AVPlayerItem *playerItem = [[AVPlayerItem alloc]initWithURL:playerItemURL];
    // 播放当前资源
    [self.player replaceCurrentItemWithPlayerItem:playerItem];
    // 添加观察者
    [self p_currentItemAddObserver];
}
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    AVPlayerManager *manger = [AVPlayerManager shareManager];
    NSString *Path = [[NSBundle mainBundle] pathForResource:@"1" ofType:@"mp3"];//我在本地有一个1.mp3的歌曲,当然也可以直接链接网上的URL
    NSURL *Url = [NSURL fileURLWithPath:Path];
    [manger musicPlayerWithURL:Url];//根据url播放
}

@end

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

3、注册,使用KVO监听self.player.currentItem

1、监听status,AVPlayerItemStatus有三种状态:

typedef NS_ENUM(NSInteger, AVPlayerItemStatus) {
    AVPlayerItemStatusUnknown,
    AVPlayerItemStatusReadyToPlay,
    AVPlayerItemStatusFailed
};

2、监听loadedTimeRanges,这个就是缓冲进度,可以进行缓冲进度条的设置
3、AVPlayerItemDidPlayToEndTimeNotification,注册这个通知,当播放器播放完成的时候进行回调。
4、addPeriodicTimeObserverForInterval,监听当前播放进度。

监听和移除代码如下:

- (void)p_currentItemRemoveObserver {
    [self.player.currentItem removeObserver:self  forKeyPath:@"status"];
    [self.player.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
    [self.player removeTimeObserver:self.timeObserver];
}

- (void)p_currentItemAddObserver {

    //监控状态属性,注意AVPlayer也有一个status属性,通过监控它的status也可以获得播放状态
    [self.player.currentItem addObserver:self forKeyPath:@"status" options:(NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew) context:nil];

    //监控缓冲加载情况属性
    [self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];

    //监控播放完成通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem];

    //监控时间进度
    @weakify(self);
    self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
        @strongify(self);
        // 在这里将监听到的播放进度代理出去,对进度条进行设置
        if (self.delegate && [self.delegate respondsToSelector:@selector(updateProgressWithPlayer:)]) {
            [self.delegate updateProgressWithPlayer:self.player];
        }
    }];
}

AVPlayer之视频

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

4、KVO处理

#pragma mark - KVO

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {

    AVPlayerItem *playerItem = object;
    if ([keyPath isEqualToString:@"status"]) {
        AVPlayerItemStatus status = [change[@"new"] integerValue];
        switch (status) {
            case AVPlayerItemStatusReadyToPlay:
            {
                // 开始播放
                [self play];
                // 代理回调,开始初始化状态
                if (self.delegate && [self.delegate respondsToSelector:@selector(startPlayWithplayer:)]) {
                    [self.delegate startPlayWithplayer:self.player];
                }
            }
                break;
            case AVPlayerItemStatusFailed:
            {
                NSLog(@"加载失败");
                TOAST_MSG(@"播放错误");
            }
                break;
            case AVPlayerItemStatusUnknown:
            {
                NSLog(@"未知资源");
                TOAST_MSG(@"播放错误");
            }
                break;
            default:
                break;
        }
    } else if([keyPath isEqualToString:@"loadedTimeRanges"]){
        NSArray *array=playerItem.loadedTimeRanges;
       //本次缓冲时间范围
        CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];
        float startSeconds = CMTimeGetSeconds(timeRange.start);
        float durationSeconds = CMTimeGetSeconds(timeRange.duration);
        //缓冲总长度
        NSTimeInterval totalBuffer = startSeconds + durationSeconds;
        NSLog(@"共缓冲:%.2f",totalBuffer);
        if (self.delegate && [self.delegate respondsToSelector:@selector(updateBufferProgress:)]) {
            [self.delegate updateBufferProgress:totalBuffer];
        }

    } else if ([keyPath isEqualToString:@"rate"]) {
        // rate=1:播放,rate!=1:非播放
        float rate = self.player.rate;
        if (self.delegate && [self.delegate respondsToSelector:@selector(player:changeRate:)]) {
            [self.delegate player:self.player changeRate:rate];
        }
    } else if ([keyPath isEqualToString:@"currentItem"]) {
        NSLog(@"新的currentItem");
        if (self.delegate && [self.delegate respondsToSelector:@selector(changeNewPlayItem:)]) {
            [self.delegate changeNewPlayItem:self.player];
        }
    }
}

- (void)playbackFinished:(NSNotification *)notifi {
    NSLog(@"播放完成");
}

以上只是demo片段,核心代码。
后面会介绍如何处理缓冲进度条、如何使用缓存进行播放。
未完待续...

  1. 使用AVPlayer播放视频必须知道的三个类
    1.1 AVPlayer : 同样理解成播放器
    1.2 AVPlayerItem : 同样是播放器需要播放的资源,比如一首歌曲
    1.3 AVPlayerLayer : 要显示视频我们就要把AVPlayerLayer对象加到要显示的视图的layer层上,因此我们只要能拿到AVPlayer的layer,然后把拿到的layer 赋值给 AVPlayerLayer对象即可

  2. 视频播放示例
    2.1 说明 : 此处只介绍一下简单的使用过程
    2.2 代码 :

3、自定义AVPlayer

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

 //创建一个item
 AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:@"xxxxx.mp4"]];//以前用的链接丢了,自己找个添上吧
 //初始化播放器
 self.player = [[AVPlayer alloc] initWithPlayerItem:item];
 //获取播放器的layer
 AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
 //设置播放器的layer
 playerLayer.frame = self.view.frame;
 playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
 playerLayer.backgroundColor = [[UIColor blueColor] CGColor];
 //讲layer添加到当期页面的layer层中
 [self.view.layer addSublayer:playerLayer];
 //播发器开始播放
 [self.player play];
#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:url;/** 创建单例对象 @return YDPlayerMananger单例对象 */+ (instancetype)shareManager;/** 将播放器展示在某个View @param view 展示播放器的View */- showPlayerInView:view withFrame:frame;/** 替换PlayerItem @param url 需要播放的链接 */- replaceCurrentItemWithURL:url;/** 播放某个链接 @param urlStr 需要播放的链接 */- playWithUrl:(NSString *)urlStr;/** 开始播放 */- play;/** 暂停播放 */- pause;/** 停止播放 */- stop;/** 跳转到指定时间 @param time 指定的时间 */- seekToTime: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: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;}- dealloc{ [self removeNotificationAndObserver];}#pragma mark - 公开方法- showPlayerInView:view withFrame:frame{ self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player]; _playerLayer.frame = frame; _playerLayer.backgroundColor = [UIColor blackColor].CGColor; _playerLayer.videoGravity = AVLayerVideoGravityResizeAspect; [view.layer addSublayer:_playerLayer];}- replaceCurrentItemWithURL: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];}- playWithUrl:(NSString *)urlStr{ [self replaceCurrentItemWithURL:[NSURL URLWithString:urlStr]]; [self play];}- play{ [self.player play]; self.playStatus = YDPlayerStatusPlaying; // 发起开始播放的通知 [[NSNotificationCenter defaultCenter] postNotificationName:YDPlayerDidStartPlayNotification object:_player];}- pause{ [self.player pause]; self.playStatus = YDPlayerStatusPausing;}- stop{ [self.player pause]; [_currentItem cancelPendingSeeks]; self.playStatus = YDPlayerStatusFinished;}- seekToTime:time{ [_currentItem seekToTime:CMTimeMakeWithSeconds(time, 1.0) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];}#pragma mark - 私有方法// 添加通知、观察者- 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 weakSelf = self; _timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { __strong typeof strongSelf = weakSelf; if (strongSelf.currentPlayTimeCallBack) { float currentPlayTime = strongSelf.currentItem.currentTime.value / strongSelf.currentItem.currentTime.timescale; strongSelf.currentPlayTimeCallBack(strongSelf.player, currentPlayTime); } }];}// 移除通知、观察者- 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 - 观察者// 观察者- observeValueForKeyPath:(NSString *)keyPath ofObject:object change:(NSDictionary *)change context: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 - 通知// 播放完成通知- playbackFinished:(NSNotification *)notification{ AVPlayerItem *playerItem = (AVPlayerItem *)notification.object; if (playerItem == _currentItem) { self.playStatus = YDPlayerStatusFinished; }}// 插拔耳机通知- 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]; } }}// 来电、闹铃打断播放通知- interruptionComing:(NSNotification *)notification{ NSDictionary *userInfo = notification.userInfo; AVAudioSessionInterruptionType type = [userInfo[AVAudioSessionInterruptionTypeKey] intValue]; if (type == AVAudioSessionInterruptionTypeBegan) { [self pause]; }}@end

容易出现的问题:AVPlayer不释放网络导致memory占用越来越大

4、注意点

  1. 问题:当在项目中使用AVPlayer,可以正常播放的时候,此时你退出了播放器,即使播放器被置空,观察者也已经移除,但是在Xcode中会发现播放器依然在缓存资源,导致memory占用越来越高.
  2. 解决方法(亲测:可以解决)
    2.1 在播放器走dealloc方法之前,重新把当前资源替换为nil
    [self.player replaceCurrentItemWithPlayerItem:nil];
    2.2 然后播放器会走到下面的方法
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
    此时播放器资源就不会是AVPlayerItemStatusReadyToPlay状态,就不会再继续缓冲了
    2.3 最后在dealloc方法里面把通知中心之类的移除就好了(其实到上面一步已经解决问题了)

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

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

本文由9159.com发布于编程,转载请注明出处:既支持本地音频播放,不仅能够播放音频

关键词:

上一篇:没有了
下一篇:没有了