一甸饱、閑談
一直用著網(wǎng)易云音樂這個(gè)app,也一直想要模擬著它做一個(gè)卻總是懶得邁出第一步境蜕,最近終于下定決心,打算先做一點(diǎn)最基礎(chǔ)的凌停。一個(gè)音樂播放器最基礎(chǔ)的當(dāng)然就是播放管理器了粱年。
二、AVPlayer的入門
其實(shí)AVPlayer用起來很簡(jiǎn)單苦锨,我也就不說廢話,直接放上代碼逼泣。
第一步:初始化等操作
// 初始化一個(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)聽status屬性
[item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
// 監(jiān)聽loadedTimeRanges屬性
[item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
// 設(shè)置player的item
[self.player replaceCurrentItemWithPlayerItem:item];
下面再補(bǔ)充一下監(jiān)聽的兩個(gè)屬性趴泌。
1.status
這個(gè)屬性指的是item的狀態(tài),狀態(tài)一共有三種拉庶,如下:
typedef NS_ENUM(NSInteger, AVPlayerItemStatus) {
AVPlayerItemStatusUnknown,
AVPlayerItemStatusReadyToPlay,
AVPlayerItemStatusFailed
};
顯然嗜憔,當(dāng)status為AVPlayerItemStatusReadyToPlay的時(shí)候,就說明可以播放了氏仗。
2.loadedTimeRanges
這個(gè)屬性主要是用在獲取緩沖的進(jìn)度的吉捶,具體的使用在下文會(huì)說到。
第二步:響應(yīng)監(jiān)聽
既然有監(jiān)聽皆尔,那就肯定要響應(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)聽item的狀態(tài)的代碼就不需要說了,只需要在AVPlayerItemStatusReadyToPlay后加上
[self.player play];
就可以了慷蠕。
下面講一下計(jì)算緩沖的進(jìn)度百分比的思路珊拼,其實(shí)也很簡(jiǎn)單,先獲取到緩沖的進(jìn)度流炕,再獲取到總的時(shí)間澎现,然后 緩沖的進(jìn)度/總時(shí)間 就是緩沖的百分比。
自己寫一個(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;
}
自己寫一個(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表示的是開始時(shí)間剑辫,duration表示的是時(shí)間范圍的持續(xù)時(shí)間。所以這邊緩沖的總進(jìn)度就是兩者相加了渠欺。
入門寫到這里盟迟,下面是自定義一個(gè)播放管理器
三赋访、自定義播放管理器
1.創(chuàng)建類并聲明方法
想象一下一個(gè)播放管理器會(huì)有哪些方法罕容,播放萌抵,暫停,開始播放網(wǎng)絡(luò)音樂捐名,開始播放本地音樂旦万,甚至還有拉進(jìn)度條來快進(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)
趁熱打鐵镶蹋,先寫播放歌曲方法
// 播放網(wǎng)絡(luò)歌曲
- (void)prepareToPlayMusicWithUrl:(NSString *)url
{
if (!url) {
return;
}
//判斷當(dāng)前有沒有正在播放的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)前有沒有正在播放的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];
}
再寫相關(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)聽響應(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來時(shí)刻返回當(dāng)前的時(shí)間贺归。這里我用的是代理模式。同時(shí)加兩個(gè)開關(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];
}
}
//開啟定時(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)之前 先暫停音樂
[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];
}
}
//開啟定時(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)前有沒有正在播放的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)前有沒有正在播放的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)之前 先暫停音樂
[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];而不是還是原來的[self.player play]婶熬,否則無法開啟定時(shí)器=9础!赵颅!
[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é)果:
最后
到這里就全部搞定了饺谬,喜歡的點(diǎn)個(gè)贊唄~