iOS基礎(chǔ):AVPlayer的入門以及應(yīng)用(自定義播放管理器的開(kāi)發(fā))

一、閑談

一直用著網(wǎng)易云音樂(lè)這個(gè)app,也一直想要模擬著它做一個(gè)卻總是懶得邁出第一步,最近終于下定決心,打算先做一點(diǎn)最基礎(chǔ)的。一個(gè)音樂(lè)播放器最基礎(chǔ)的當(dāng)然就是播放管理器了。

二、AVPlayer的入門

其實(shí)AVPlayer用起來(lái)很簡(jiǎn)單,我也就不說(shuō)廢話,直接放上代碼。

第一步:初始化等操作

// 初始化一個(gè)AVPlayer
self.player = [[AVPlayer alloc] init];
// 創(chuàng)建一個(gè)item
AVPlayerItem *item =[AVPlayerItem playerItemWithURL:[NSURL URLWithString:@"http://m2.music.126.net/lpeVipLJshTxA-T7xjFI2g==/1046735069650768.mp3"]];
// 監(jiān)聽(tīng)status屬性
[item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
// 監(jiān)聽(tīng)loadedTimeRanges屬性
[item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
// 設(shè)置player的item
[self.player replaceCurrentItemWithPlayerItem:item];

下面再補(bǔ)充一下監(jiān)聽(tīng)的兩個(gè)屬性。

1.status

這個(gè)屬性指的是item的狀態(tài),狀態(tài)一共有三種,如下:

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

顯然,當(dāng)status為AVPlayerItemStatusReadyToPlay的時(shí)候,就說(shuō)明可以播放了。

2.loadedTimeRanges

這個(gè)屬性主要是用在獲取緩沖的進(jìn)度的,具體的使用在下文會(huì)說(shuō)到。

第二步:響應(yīng)監(jiān)聽(tīng)

既然有監(jiān)聽(tīng),那就肯定要響應(yīng)。先上代碼:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"status"]) {
        AVPlayerItemStatus status =[change[@"new"]integerValue];//change為string類型
        switch (status) {
            case AVPlayerItemStatusUnknown:
                NSLog(@"AVPlayerItemStatusUnknown");
                break;          
            case AVPlayerItemStatusReadyToPlay:
                //調(diào)用播放方法
                [self.player play];
                break;
            case AVPlayerItemStatusFailed:{
                NSLog(@"AVPlayerItemStatusFailed");
                break;
            }          
            default:
                break;
        }
    } else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
        // 計(jì)算緩沖的進(jìn)度百分比
        float timeInterval= [self availableDuration];
        NSInteger totalDuration = [self fetchTotalTime];
        // 打印緩沖進(jìn)度的百分比
        NSLog(@"進(jìn)度百分比%f", timeInterval / totalDuration);
    }
}

其中監(jiān)聽(tīng)item的狀態(tài)的代碼就不需要說(shuō)了,只需要在AVPlayerItemStatusReadyToPlay后加上

[self.player play];

就可以了。
下面講一下計(jì)算緩沖的進(jìn)度百分比的思路,其實(shí)也很簡(jiǎn)單,先獲取到緩沖的進(jìn)度,再獲取到總的時(shí)間,然后 緩沖的進(jìn)度/總時(shí)間 就是緩沖的百分比。

自己寫(xiě)一個(gè)獲取總時(shí)間的方法:
// 獲取總時(shí)間,總秒數(shù)
- (NSInteger)fetchTotalTime
{
    //獲取當(dāng)前播放歌曲的總時(shí)間
    CMTime time = self.player.currentItem.duration;
    
    if (time.timescale == 0) {
        return 0;
    }
    //播放秒數(shù) = time.value/time.timescale
    return time.value/time.timescale;
}
自己寫(xiě)一個(gè)獲取緩沖總進(jìn)度的方法:
- (float)availableDuration
{
    NSArray *loadedTimeRanges = [[self.player currentItem] loadedTimeRanges];
    CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue];// 獲取緩沖區(qū)域
    float startSeconds = CMTimeGetSeconds(timeRange.start);
    float durationSeconds = CMTimeGetSeconds(timeRange.duration);
    float result = startSeconds + durationSeconds;// 計(jì)算緩沖總進(jìn)度
    return result;
}

補(bǔ)充一點(diǎn)CMTimeRange的知識(shí):

typedef struct
{
    CMTime          start;      /*! @field start The start time of the time range. */
    CMTime          duration;   /*! @field duration The duration of the time range. */
} CMTimeRange;

start表示的是開(kāi)始時(shí)間,duration表示的是時(shí)間范圍的持續(xù)時(shí)間。所以這邊緩沖的總進(jìn)度就是兩者相加了。

入門寫(xiě)到這里,下面是自定義一個(gè)播放管理器

三、自定義播放管理器

1.創(chuàng)建類并聲明方法

想象一下一個(gè)播放管理器會(huì)有哪些方法,播放,暫停,開(kāi)始播放網(wǎng)絡(luò)音樂(lè),開(kāi)始播放本地音樂(lè),甚至還有拉進(jìn)度條來(lái)快進(jìn)快退。代碼如下:

//單例
+ (instancetype)sharedPlayerManager;

//暫停
- (void)pasuseMusic;

//播放
- (void)playMusic;

//準(zhǔn)備去播放
- (void)prepareToPlayMusicWithUrl:(NSString *)url;

//本地播放方法
- (void)prepareToPlayMusicWithFilePath:(NSString *)musicFilePath;

//快進(jìn)快退方法
- (void)playMusicWithSliderValue:(CGFloat)peogress;

2.相關(guān)方法的實(shí)現(xiàn)

趁熱打鐵,先寫(xiě)播放歌曲方法

// 播放網(wǎng)絡(luò)歌曲
- (void)prepareToPlayMusicWithUrl:(NSString *)url
{
    if (!url) {
        return;
    }
    //判斷當(dāng)前有沒(méi)有正在播放的item
    if (self.player.currentItem) {
        //移除觀察者
        [self.player.currentItem removeObserver:self forKeyPath:@"status"];
        [self.player.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
    }
    //創(chuàng)建一個(gè)item
    AVPlayerItem *item =[AVPlayerItem playerItemWithURL:[NSURL URLWithString:url]];
    //觀察item的加載狀態(tài)
    [item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    [item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    //替換當(dāng)前item
    [self.player replaceCurrentItemWithPlayerItem:item];
    //播放完成后自動(dòng)跳到下一首
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nextMusic) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];

}

// 播放本地歌曲
- (void)prepareToPlayMusicWithFilePath:(NSString *)musicFilePath
{
    //判斷當(dāng)前有沒(méi)有正在播放的item
    if (self.player.currentItem) {
        //移除觀察者
        [self.player.currentItem removeObserver:self forKeyPath:@"status"];
        [self.player.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
    }
    //創(chuàng)建一個(gè)item
    AVPlayerItem *item =[AVPlayerItem playerItemWithURL:[NSURL fileURLWithPath:musicFilePath]];
    
    //觀察item的加載狀態(tài)
    [item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    [item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    //替換當(dāng)前item
    [self.player replaceCurrentItemWithPlayerItem:item];
    //播放完成后自動(dòng)跳到下一首
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(nextMusic) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
}

再寫(xiě)相關(guān)私有方法

#pragma mark - 私有方法
// 獲取當(dāng)前時(shí)間
- (NSInteger)fetchCurrentTime
{
    CMTime time = self.player.currentItem.currentTime;
    if (time.timescale == 0) {
        return 0;
    }
    return time.value/time.timescale;
}

// 獲取總時(shí)間時(shí)間
- (NSInteger)fetchTotalTime
{
    CMTime time = self.player.currentItem.duration;
    if (time.timescale == 0) {
        return 0;
    }
    return time.value/time.timescale;
}

// 獲取當(dāng)前播放進(jìn)度
- (CGFloat)fetchProgressValue
{
    return [self fetchCurrentTime]/(CGFloat)[self fetchTotalTime];
}

// 獲取緩沖進(jìn)度
- (float)availableDuration
{
    NSArray *loadedTimeRanges = [[self.player currentItem] loadedTimeRanges];
    CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue];// 獲取緩沖區(qū)域
    float startSeconds = CMTimeGetSeconds(timeRange.start);
    float durationSeconds = CMTimeGetSeconds(timeRange.duration);
    float result = startSeconds + durationSeconds;// 計(jì)算緩沖總進(jìn)度
    return result;
}

//將秒數(shù)轉(zhuǎn)化成類似00:00的格式,用于界面顯示
- (NSString *)changeSecondsTime:(NSInteger)time
{
    NSInteger min =time/60;
    NSInteger seconds =time % 60;
    return [NSString stringWithFormat:@"%.2ld:%.2ld",min,seconds];
}

監(jiān)聽(tīng)響應(yīng)

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"status"]) {
        AVPlayerItemStatus status =[change[@"new"]integerValue];//change為string類型
        switch (status) {
            case AVPlayerItemStatusUnknown:
                NSLog(@"未知錯(cuò)誤");
                break;
            case AVPlayerItemStatusReadyToPlay:
                //調(diào)用播放方法
                [self.player play];
                break;
            case AVPlayerItemStatusFailed:{
                NSLog(@"錯(cuò)誤");
                break;
            }
            default:
                break;
        }
    } else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
        float timeInterval= [self availableDuration];
        NSInteger totalDuration = [self fetchTotalTime];
        NSLog(@"==%f", timeInterval / totalDuration);
    }
}

由于始終要刷新界面的進(jìn)度條,所以我們要?jiǎng)?chuàng)建一個(gè)timer來(lái)時(shí)刻返回當(dāng)前的時(shí)間。這里我用的是代理模式。同時(shí)加兩個(gè)開(kāi)關(guān)定時(shí)器的私有方法。

// timer懶加載
- (NSTimer *)timer {
    if (!_timer) {
        _timer =[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    }
    return _timer;
}

//定時(shí)器方法
- (void)timerAction {
    if ([self.delegate respondsToSelector:@selector(playManagerDelegateFetchTotalTime:currentTime:progress:)]) {
        //1.總時(shí)間  2.當(dāng)期時(shí)間 3.當(dāng)前進(jìn)度
        NSString *totalTime = [self changeSecondsTime:[self fetchTotalTime]];//總時(shí)間
        NSString *currentTime =[self changeSecondsTime:[self fetchCurrentTime]];//當(dāng)前時(shí)間
        CGFloat progress = [self fetchProgressValue];
        
        [self.delegate playManagerDelegateFetchTotalTime:totalTime  currentTime:currentTime  progress:progress];
    }
}

//開(kāi)啟定時(shí)器
- (void)startTimer
{
    [self.timer fire];
}

//關(guān)閉定時(shí)器
- (void)closeTimer
{
    [self.timer invalidate];
    self.timer = nil;  //置空
}

最后完成剩下的方法

// 暫停
- (void)pasuseMusic
{
    [self.player pause];
    [self closeTimer];
}

// 播放
- (void)playMusic
{
    [self.player play];
    [self startTimer];
}

//快進(jìn)快退
- (void)playMusicWithSliderValue:(CGFloat)peogress{
    //滑動(dòng)之前 先暫停音樂(lè)
    [self pasuseMusic];
    [self.player seekToTime:CMTimeMake([self fetchTotalTime] * peogress, 1) completionHandler:^(BOOL finished) {
        if (finished) {
            //活動(dòng)結(jié)束繼續(xù)播放
            [self playMusic];
        }
    }];
}

好了,到這邊所有的方法都完成了。我把代碼全部再貼上。
.h文件

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

@protocol YQPlayerManagerDelegate <NSObject>
// 傳遞播放時(shí)間總時(shí)間等信息
-(void)playManagerDelegateFetchTotalTime:(NSString *)totalTime currentTime:(NSString *)currentTime progress:(CGFloat)progress;
// 傳遞播放的進(jìn)度百分比
- (void)playManagerDelegateFetchLoadedTimeRanges:(CGFloat)loadPercent;
// 下一首歌
- (void)playNextMusic;
@end

@interface YQPlayerManager : NSObject

@property (nonatomic, weak) id <YQPlayerManagerDelegate> delegate;

//單例
+ (instancetype)sharedPlayerManager;

//暫停
- (void)pasuseMusic;

//播放
- (void)playMusic;

//準(zhǔn)備去播放
- (void)prepareToPlayMusicWithUrl:(NSString *)url;

//本地播放方法
- (void)prepareToPlayMusicWithFilePath:(NSString *)musicFilePath;

//快進(jìn)快退方法
- (void)playMusicWithSliderValue:(CGFloat)peogress;
@end

.m文件

#import "YQPlayerManager.h"

@interface YQPlayerManager()
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation YQPlayerManager

// timer懶加載
- (NSTimer *)timer {
    if (!_timer) {
        _timer =[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    }
    return _timer;
}

//定時(shí)器方法
- (void)timerAction {
    if ([self.delegate respondsToSelector:@selector(playManagerDelegateFetchTotalTime:currentTime:progress:)]) {
        //1.總時(shí)間  2.當(dāng)期時(shí)間 3.當(dāng)前進(jìn)度
        NSString *totalTime = [self changeSecondsTime:[self fetchTotalTime]];//總時(shí)間
        NSString *currentTime =[self changeSecondsTime:[self fetchCurrentTime]];//當(dāng)前時(shí)間
        CGFloat progress = [self fetchProgressValue];
        [self.delegate playManagerDelegateFetchTotalTime:totalTime  currentTime:currentTime  progress:progress];
    }
}

//開(kāi)啟定時(shí)器
- (void)startTimer
{
    [self.timer fire];
}

//關(guān)閉定時(shí)器
- (void)closeTimer
{
    [self.timer invalidate];
    self.timer = nil;  //置空
}


//player懶加載
- (AVPlayer *)player
{
    if (_player == nil) {
        _player = [[AVPlayer alloc] init];
    }
    return _player;
}


//單例
+ (instancetype)sharedPlayerManager {
    static YQPlayerManager *handle = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        handle = [[YQPlayerManager alloc] init];
    });
    return handle;
}

// 暫停
- (void)pasuseMusic
{
    [self.player pause];
    [self closeTimer];
}

// 播放
- (void)playMusic
{
    [self.player play];
    [self startTimer];
}

// 播放網(wǎng)絡(luò)歌曲
- (void)prepareToPlayMusicWithUrl:(NSString *)url
{
    if (!url) {
        return;
    }
    //判斷當(dāng)前有沒(méi)有正在播放的item
    if (self.player.currentItem) {
        //移除觀察者
        [self.player.currentItem removeObserver:self forKeyPath:@"status"];
        [self.player.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
    }
    //創(chuàng)建一個(gè)item
    AVPlayerItem *item =[AVPlayerItem playerItemWithURL:[NSURL URLWithString:url]];
    //觀察item的加載狀態(tài)
    [item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    [item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    //替換當(dāng)前item
    [self.player replaceCurrentItemWithPlayerItem:item];
    //播放完成后自動(dòng)跳到下一首
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nextMusic) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];

}

// 播放本地歌曲
- (void)prepareToPlayMusicWithFilePath:(NSString *)musicFilePath
{
    //判斷當(dāng)前有沒(méi)有正在播放的item
    if (self.player.currentItem) {
        //移除觀察者
        [self.player.currentItem removeObserver:self forKeyPath:@"status"];
        [self.player.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
    }
    //創(chuàng)建一個(gè)item
    AVPlayerItem *item =[AVPlayerItem playerItemWithURL:[NSURL fileURLWithPath:musicFilePath]];
    
    //觀察item的加載狀態(tài)
    [item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    [item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    //替換當(dāng)前item
    [self.player replaceCurrentItemWithPlayerItem:item];
    //播放完成后自動(dòng)跳到下一首
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(nextMusic) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
}

//快進(jìn)快退
- (void)playMusicWithSliderValue:(CGFloat)peogress{
    //滑動(dòng)之前 先暫停音樂(lè)
    [self pasuseMusic];
    [self.player seekToTime:CMTimeMake([self fetchTotalTime] * peogress, 1) completionHandler:^(BOOL finished) {
        if (finished) {
            //活動(dòng)結(jié)束繼續(xù)播放
            [self playMusic];
        }
    }];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"status"]) {
        AVPlayerItemStatus status =[change[@"new"]integerValue];//change為string類型
        switch (status) {
            case AVPlayerItemStatusUnknown:
                NSLog(@"未知錯(cuò)誤");
                break;
            case AVPlayerItemStatusReadyToPlay:
                // 調(diào)用播放方法 
                // 注意這里要改成[self playMusic];而不是還是原來(lái)的[self.player play],否則無(wú)法開(kāi)啟定時(shí)器?。?!
                [self playMusic];
                break;
            case AVPlayerItemStatusFailed:{
                NSLog(@"錯(cuò)誤");
                break;
            }
            default:
                break;
        }
    } else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
        float timeInterval= [self availableDuration];
        NSInteger totalDuration = [self fetchTotalTime];
        float percent = timeInterval /totalDuration;
        if (percent > 1) {
            percent = 1;
        }
        if ([self.delegate respondsToSelector:@selector(playManagerDelegateFetchLoadedTimeRanges:)]) {
            [self.delegate playManagerDelegateFetchLoadedTimeRanges:percent];
        }
    }
}

#pragma mark - 私有方法
- (void)nextMusic
{
    if ([self.delegate respondsToSelector:@selector(playNextMusic)]) {
        [self.delegate playNextMusic];
    }
}

// 獲取當(dāng)前時(shí)間
- (NSInteger)fetchCurrentTime
{
    CMTime time = self.player.currentItem.currentTime;
    if (time.timescale == 0) {
        return 0;
    }
    return time.value/time.timescale;
}

// 獲取總時(shí)間時(shí)間
- (NSInteger)fetchTotalTime
{
    CMTime time = self.player.currentItem.duration;
    if (time.timescale == 0) {
        return 0;
    }
    return time.value/time.timescale;
}

// 獲取當(dāng)前播放進(jìn)度
- (CGFloat)fetchProgressValue
{
    return [self fetchCurrentTime]/(CGFloat)[self fetchTotalTime];
}

// 獲取緩沖進(jìn)度
- (float)availableDuration
{
    NSArray *loadedTimeRanges = [[self.player currentItem] loadedTimeRanges];
    CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue];// 獲取緩沖區(qū)域
    float startSeconds = CMTimeGetSeconds(timeRange.start);
    float durationSeconds = CMTimeGetSeconds(timeRange.duration);
    float result = startSeconds + durationSeconds;// 計(jì)算緩沖總進(jìn)度
    return result;
}

//將秒數(shù)轉(zhuǎn)化成類似00:00的格式,用于界面顯示
- (NSString *)changeSecondsTime:(NSInteger)time
{
    NSInteger min =time/60;
    NSInteger seconds =time % 60;
    return [NSString stringWithFormat:@"%.2ld:%.2ld",min,seconds];
}

3.使用管理器

在視圖控制器中使用,上代碼:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    YQPlayerManager *manager = [YQPlayerManager sharedPlayerManager];
    [manager prepareToPlayMusicWithUrl:@"http://m2.music.126.net/lpeVipLJshTxA-T7xjFI2g==/1046735069650768.mp3"];
    manager.delegate = self;
}

-  (void)playManagerDelegateFetchTotalTime:(NSString *)totalTime currentTime:(NSString *)currentTime progress:(CGFloat)progress
{
    NSLog(@"%@---%@---%f", totalTime, currentTime, progress);
}

- (void)playManagerDelegateFetchLoadedTimeRanges:(CGFloat)loadPercent
{
    NSLog(@"%f", loadPercent);
}

- (void)playNextMusic
{
    NSLog(@"playNextMusic");
}

結(jié)果:

結(jié)果展示.gif

最后

到這里就全部搞定了,喜歡的點(diǎn)個(gè)贊唄~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容