音頻播放主要是基于系統(tǒng)的AVPlayer和AVPlayerItem,不像視頻需要顯示畫面需要AVPlayerLayer
實現思路:
建一個繼承于NSObject類.h .m文件,或許你疑問為什么不寫在view或者viewcontrol中,因為音頻播放你只需要處理數據就行了,比如做快進快退操作,下一曲上一曲操作,只需要把數據傳給它做數據操作就行了.下面看代碼;
建一個繼承與NSobject類起名叫MAudioPlayer的文件
導入
#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>
在.h中添加一些屬性,用于外部訪問,比如當前播放器播放的model等,再寫一些方法用于外部調用,比如暫停,播放等
@property (nonatomic ,strong,readonly) AVPlayer *player;
@property (nonatomic, assign, readonly) NSInteger currentTime;/*!*當前播放器時間*/
@property (nonatomic, assign, readonly) NSInteger totalTime;/*!*當前播放器總時間*/
@property (nonatomic, assign,readonly) MAudioPlayState playerState;/*用枚舉定義了播放器狀態(tài)*/
@property (nonatomic, assign) float ratevalue;/*!*倍速*/
定義的枚舉類型,可自定義添加狀態(tài)
typedef NS_ENUM(NSUInteger, MAudioPlayState) {
MAudioStatePlaying = 1,
MAudioStatePaused,
MAudioStateWaiting,
};
還可以加一個model,在播放的時候將當前播放的model傳給播放器,下面代碼中,在播放器代理里面有使用到這個model
/*
* 當前音頻模型
*/
@property (nonatomic, strong) DetailCourseListModel *currentAudioModel;
定義了一個播放器代理,實現兩個方法,一個是實時回傳播放器時間刷新UI,一個是播放完畢,做暫?;蛘呦乱磺僮?/p>
@protocol MAudioPlayerDelegate <NSObject>
- (void)audioUpdateWith:(float)time Totaltime:(float)totalTime;
-(void)audioPlayEnd;
@end
在.m中聲明AVPlayerItem對象
@property (nonatomic ,strong) AVPlayerItem *playerItem;
再添加一個時間觀察,用于實時刷新UI數據
@property (nonatomic, strong) id timeObserve;// 時間觀察
還可以再加一些輔助對象,比如:電話監(jiān)聽,耳機拔插監(jiān)聽,來電前播放狀態(tài),鎖屏播放的存儲的字典等
@property (nonatomic, strong) CTCallCenter *callCenter ;/*!*監(jiān)聽電話*/
@property (nonatomic, assign) BOOL isPlay;/*!*播放或者暫停*/
@property (nonatomic, strong) NSMutableDictionary *imageSpaceDict;/*!*存圖片字典*/
用單例初始化保證全局只有一個播放器
+ (instancetype)sharedMPlayer
預留一些播放暫停,播放,切換下一曲等操作方法
初始化播放器方法
/**
初始化播放器
@param url 播放地址
@param recordTime 指定時間播放
@param ratevalue 倍速,1.0~2.0倍速播放
*/
- (void)initWithUrl:(NSString *)url seekTotime:(NSInteger)recordTime rateValue:(float)ratevalue;
- (void)playerPaused;//暫停播放方法
- (void)playerPlay//開啟播放
- (void)closePlayer//移除播放器
-(void)SetlockScreenInformation:(DetailCourseListModel *)model;//鎖屏方法,model根據需求自定義
現在看一下.m中的代碼
單例初始化對象,保證整個app只存在一個播放器
+ (instancetype)sharedMPlayer
{
static MAudioPlayer *audioPlayer = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
audioPlayer = [[MAudioPlayer alloc] init];
});
return audioPlayer;
}
初始化播放器
- (void)initWithUrl:(NSString *)url seekTotime :(NSInteger)recordTime rateValue:(float)ratevalue{
//[self callStatCenter];//監(jiān)聽電話
//[self audioRouteChangeListener];//拔插耳機
self.playerItem = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:localString]];
_ratevalue = ratevalue;//倍速
//AVPlayer
_player = [AVPlayer playerWithPlayerItem:self.playerItem];
[_player play];
//指定時間播放,類似記憶播放,拖動進度條也用這個方法
[_player seekToTime:CMTimeMake(recordTime, 1)];
}
因為self.playerItem用的是懶加載,看一下,item怎么初始化,做了哪些操作
/**
* 根據playerItem,來添加移除觀察者
*
* @param playerItem playerItem
*/
- (void)setPlayerItem:(AVPlayerItem *)playerItem
{
//如果初始化的item與當前item相等,則不做操作
if (_playerItem == playerItem) {return;}
//如果當前item不為空,移除里面的屬性觀察
if (_playerItem) {
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:_playerItem];
[_playerItem removeObserver:self forKeyPath:@"status"];
}
_playerItem = playerItem;
if (playerItem) {
//當前音頻播放完畢監(jiān)聽,我這里寫的代理,方便數據傳遞
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayDidEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem];
//監(jiān)聽播放器狀態(tài)
[playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
//除了播放器狀態(tài),還可以監(jiān)聽緩沖狀態(tài):無緩沖playbackBufferEmpty,緩沖足夠可以播放:playbackBufferEmpty等,具體狀態(tài)可以百度查找
}
}
增加一個觀察,用來觀察播放器,暫停,播放等狀態(tài),方便刷新UI
//觀察播放器狀態(tài)
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSString*, id> *)change context:(nullable void *)context {
if ([keyPath isEqualToString:@"status"]) {
AVPlayerItemStatus status = _playerItem.status;
switch (status) {
case AVPlayerItemStatusReadyToPlay:
{
self.playerItem.audioTimePitchAlgorithm = AVAudioTimePitchAlgorithmTimeDomain;
[_player play];
//如果想實現倍速播放,必須調用此方法
[self enableAudioTracks:YES inPlayerItem:_playerItem];
self.player.rate = _ratevalue;
//增加一個時間觀察,為了實時拿到當前播放時間,刷新UI,鎖屏操作等
[self addTimeObserve];
}
break;
case AVPlayerItemStatusUnknown:
{
BLLog(@"AVPlayerItemStatusUnknown");
}
break;
case AVPlayerItemStatusFailed:
{
BLLog(@"AVPlayerItemStatusFailed");
BLLog(@"%@",_playerItem.error);
}
break;
default:
break;
}
}
}
每一秒刷新UI
- (void)addTimeObserve{
__weak typeof(self) weakSelf = self;
self.timeObserve = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1, 1) queue:nil usingBlock:^(CMTime time){
AVPlayerItem *currentItem = weakSelf.playerItem;
NSArray *loadedRanges = currentItem.seekableTimeRanges;
NSInteger currentTime = (NSInteger)CMTimeGetSeconds([currentItem currentTime]);
CGFloat totalTime = (CGFloat)currentItem.duration.value / currentItem.duration.timescale;
if (self.delegateM && [self.delegateM respondsToSelector:@selector(audioUpdateWith:Totaltime:)]) {
//播放器時間代理
[weakSelf.delegateM audioUpdateWith:currentTime Totaltime:totalTime];
}
//根據系統(tǒng)方法來判斷播放器狀態(tài),供外部屬性調用實時刷新UI,比如:外部播放器按鈕狀態(tài)可根據可狀態(tài)播放,點擊播放還是暫停,也可以通過此狀態(tài)判斷
if (self.player.timeControlStatus == AVPlayerTimeControlStatusPlaying) {
_playerState = MAudioStatePlaying;
}
if (self.player.timeControlStatus == AVPlayerTimeControlStatusPaused) {
_playerState = MAudioStatePaused;
}
if (self.player.timeControlStatus == AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate) {
_playerState = MAudioStateWaiting;
}
if (loadedRanges.count > 0 && currentItem.duration.timescale != 0) {
_totalTime = totalTime;
_currentTime = currentTime;
}
}];
}
倍速切換方法
- (void)enableAudioTracks:(BOOL)enable inPlayerItem:(AVPlayerItem*)playerItem
{
for (AVPlayerItemTrack *track in playerItem.tracks)
{
if ([track.assetTrack.mediaType isEqual:AVMediaTypeAudio])
{
track.enabled = enable;
}
}
}
下面就是實現播放器播放,暫停等方法了
//播放暫停
- (void)playerPaused {
[self.player pause];
}
//播放繼續(xù)
- (void)playerPlay {
[self.player play];
self.player.rate = _ratevalue;
}
關閉播放器,記得移除通知,置空播放器
- (void)closePlayer{
[self.player.currentItem cancelPendingSeeks];
[self.player.currentItem.asset cancelLoading];
self.playerItem = nil;
[self.player replaceCurrentItemWithPlayerItem:nil];
_player = nil;
self.ratevalue = 1.0;
self.callCenter = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
對,還有播放器鎖屏的實現方法,可在.h里面寫一個鎖屏方法供外部調用,鎖屏方法就是在外部播放器代理里面調用,如果鎖屏中加載圖片為網絡圖片的話,最好做一個字典通過key-value來存儲
-(void)SetlockScreenInformation:(DetailCourseListModel *)model{
//model是項目中用到的,可根據自己需求定義
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:[[MPNowPlayingInfoCenter defaultCenter] nowPlayingInfo]];
[dict setObject:model.teacherName==nil?@"1":model.name forKey:MPMediaItemPropertyTitle];
//此表現形式 name-title 副標題
// [dict setObject:recordUploadModel.teacherName==nil?@"1":recordUploadModel.teacherName forKey:MPMediaItemPropertyArtist];
[dict setObject:model.name==nil?@"1":model.name forKey:MPMediaItemPropertyAlbumTitle];
NSString *imageUrl;
//判斷當前是個鏈接還是上傳路徑
if ([model.imgUrl hasPrefix:@"http"]) {
imageUrl = model.imgUrl;
}else{
//拼接圖片url
imageUrl = [NSString stringWithFormat:@"%@%@", imageUrlString, model.imgUrl];
}
if (![self.imageSpaceDict objectForKey:imageUrl]) {
NSLog(@"走了幾次啊");
UIImage *imageM = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]]];
[self.imageSpaceDict setValue:imageM forKey:imageUrl];
}
UIImage *tempImage = [UIImage imageNamed:@"加載中"];
[dict setObject:[[MPMediaItemArtwork alloc] initWithImage:[self.imageSpaceDict objectForKey:imageUrl] == nil?tempImage:[self.imageSpaceDict objectForKey:imageUrl]] forKey:MPMediaItemPropertyArtwork];
//當前已經過時間
[dict setObject:[NSNumber numberWithDouble:_currentTime] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
//總時間
[dict setObject:[NSNumber numberWithDouble:_totalTime] forKey:MPMediaItemPropertyPlaybackDuration];
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
}
寫了那么多了,估計看的也沒有耐心了.鎖屏后怎么在鎖屏界面或控制中心對播放器做暫停,上一曲下一曲等操作呢,可以在控制器中加入以下代碼:
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
// 開始接受遠程控制
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
//成為第一響應者
[self becomeFirstResponder];
// 開啟界面常亮
[[UIApplication sharedApplication] setIdleTimerDisabled:YES];
}
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
//接觸遠程控制
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
[self resignFirstResponder];
// 關閉界面常亮
[[UIApplication sharedApplication] setIdleTimerDisabled:NO];
}
重寫父類成為響應者方法
// 重寫父類成為響應者方法
- (BOOL)canBecomeFirstResponder
{
return YES;
}
//重寫父類方法,接受外部事件的處理
- (void)remoteControlReceivedWithEvent: (UIEvent *) receivedEvent {
if (receivedEvent.type == UIEventTypeRemoteControl) {
switch (receivedEvent.subtype) { // 得到事件類型
case UIEventSubtypeRemoteControlTogglePlayPause: // 暫停ios6
break;
case UIEventSubtypeRemoteControlPreviousTrack: // 上一首
break;
case UIEventSubtypeRemoteControlNextTrack: // 下一首
break;
case UIEventSubtypeRemoteControlPlay: //播放
break;
case UIEventSubtypeRemoteControlPause: // 暫停 ios7
break;
default:
break;
}
}
}
給鎖屏界面實時傳數據呢,就在播放器代理里面
#pragma mark - 播放器代理時間
- (void)audioUpdateWith:(float)time Totaltime:(float)totalTime{
//滑動進度條
_slider.value = time/totalTime;
//刷新當前時間,通過擴展方法轉化成00:00:00格式
_currentTimeLB.text = [NSString timeTransformString:(float)time];
//刷新總時間
_totalTimeLB.text = [NSString timeTransformString:(float)totalTime];
//當前標題,就是model里面的
_titleLB.text = [NSString stringWithFormat:@"%@",MAudioPlay.currentAudioModel.name];
//在代理里面調用鎖屏方法,傳數據
[MAudioPlay SetlockScreenInformation:MAudioPlay.currentAudioModel];
}
秒轉換成00:00:00格式,我寫的是NSString的擴展屬性,此方法也是在網上找的,用到手動釋放,需要在工程TARGETS->Build Phases 找到你寫此方法的文件,雙擊加入-fno-objc-arc 方法如下:
+(NSString *)timeTransformString:(unsigned long)ms
{
unsigned long seconds, h, m, s;
char buff[128] = { 0 };
NSString *time = nil;
seconds = ms ;
h = seconds / 3600;
m = (seconds - h * 3600) / 60;
s = seconds - h * 3600 - m * 60;
snprintf(buff, sizeof(buff), "%02ld:%02ld:%02ld", h, m, s);
time = [[[NSString alloc] initWithCString:buff
encoding:NSUTF8StringEncoding] autorelease];
return time;
}