音頻播放主要是基于系統(tǒng)的AVPlayer
和AVPlayerItem
,不像視頻需要顯示畫面需要AVPlayerLayer
實(shí)現(xiàn)思路:
建一個(gè)繼承于NSObject
類.h .m文件,或許你疑問為什么不寫在view或者viewcontrol中,因?yàn)橐纛l播放你只需要處理數(shù)據(jù)就行了,比如做快進(jìn)快退操作,下一曲上一曲操作,只需要把數(shù)據(jù)傳給它做數(shù)據(jù)操作就行了.下面看代碼;
建一個(gè)繼承與NSobject
類起名叫MAudioPlayer
的文件
導(dǎo)入
#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>
在.h中添加一些屬性,用于外部訪問,比如當(dāng)前播放器播放的model等,再寫一些方法用于外部調(diào)用,比如暫停,播放等
@property (nonatomic ,strong,readonly) AVPlayer *player;
@property (nonatomic, assign, readonly) NSInteger currentTime;/*!*當(dāng)前播放器時(shí)間*/
@property (nonatomic, assign, readonly) NSInteger totalTime;/*!*當(dāng)前播放器總時(shí)間*/
@property (nonatomic, assign,readonly) MAudioPlayState playerState;/*用枚舉定義了播放器狀態(tài)*/
@property (nonatomic, assign) float ratevalue;/*!*倍速*/
定義的枚舉類型,可自定義添加狀態(tài)
typedef NS_ENUM(NSUInteger, MAudioPlayState) {
MAudioStatePlaying = 1,
MAudioStatePaused,
MAudioStateWaiting,
};
還可以加一個(gè)model,在播放的時(shí)候?qū)?dāng)前播放的model傳給播放器,下面代碼中,在播放器代理里面有使用到這個(gè)model
/*
* 當(dāng)前音頻模型
*/
@property (nonatomic, strong) DetailCourseListModel *currentAudioModel;
定義了一個(gè)播放器代理,實(shí)現(xiàn)兩個(gè)方法,一個(gè)是實(shí)時(shí)回傳播放器時(shí)間刷新UI,一個(gè)是播放完畢,做暫托В或者下一曲操作
@protocol MAudioPlayerDelegate <NSObject>
- (void)audioUpdateWith:(float)time Totaltime:(float)totalTime;
-(void)audioPlayEnd;
@end
在.m中聲明AVPlayerItem對象
@property (nonatomic ,strong) AVPlayerItem *playerItem;
再添加一個(gè)時(shí)間觀察,用于實(shí)時(shí)刷新UI數(shù)據(jù)
@property (nonatomic, strong) id timeObserve;// 時(shí)間觀察
還可以再加一些輔助對象,比如:電話監(jiān)聽,耳機(jī)拔插監(jiān)聽,來電前播放狀態(tài),鎖屏播放的存儲(chǔ)的字典等
@property (nonatomic, strong) CTCallCenter *callCenter ;/*!*監(jiān)聽電話*/
@property (nonatomic, assign) BOOL isPlay;/*!*播放或者暫停*/
@property (nonatomic, strong) NSMutableDictionary *imageSpaceDict;/*!*存圖片字典*/
用單例初始化保證全局只有一個(gè)播放器
+ (instancetype)sharedMPlayer
預(yù)留一些播放暫停,播放,切換下一曲等操作方法
初始化播放器方法
/**
初始化播放器
@param url 播放地址
@param recordTime 指定時(shí)間播放
@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根據(jù)需求自定義
現(xiàn)在看一下.m中的代碼
單例初始化對象,保證整個(gè)app只存在一個(gè)播放器
+ (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];//拔插耳機(jī)
self.playerItem = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:localString]];
_ratevalue = ratevalue;//倍速
//AVPlayer
_player = [AVPlayer playerWithPlayerItem:self.playerItem];
[_player play];
//指定時(shí)間播放,類似記憶播放,拖動(dòng)進(jìn)度條也用這個(gè)方法
[_player seekToTime:CMTimeMake(recordTime, 1)];
}
因?yàn)閟elf.playerItem用的是懶加載,看一下,item怎么初始化,做了哪些操作
/**
* 根據(jù)playerItem樟遣,來添加移除觀察者
*
* @param playerItem playerItem
*/
- (void)setPlayerItem:(AVPlayerItem *)playerItem
{
//如果初始化的item與當(dāng)前item相等,則不做操作
if (_playerItem == playerItem) {return;}
//如果當(dāng)前item不為空,移除里面的屬性觀察
if (_playerItem) {
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:_playerItem];
[_playerItem removeObserver:self forKeyPath:@"status"];
}
_playerItem = playerItem;
if (playerItem) {
//當(dāng)前音頻播放完畢監(jiān)聽,我這里寫的代理,方便數(shù)據(jù)傳遞
[[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)可以百度查找
}
}
增加一個(gè)觀察,用來觀察播放器,暫停,播放等狀態(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];
//如果想實(shí)現(xiàn)倍速播放,必須調(diào)用此方法
[self enableAudioTracks:YES inPlayerItem:_playerItem];
self.player.rate = _ratevalue;
//增加一個(gè)時(shí)間觀察,為了實(shí)時(shí)拿到當(dāng)前播放時(shí)間,刷新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:)]) {
//播放器時(shí)間代理
[weakSelf.delegateM audioUpdateWith:currentTime Totaltime:totalTime];
}
//根據(jù)系統(tǒng)方法來判斷播放器狀態(tài),供外部屬性調(diào)用實(shí)時(shí)刷新UI,比如:外部播放器按鈕狀態(tài)可根據(jù)可狀態(tài)播放,點(diǎn)擊播放還是暫停,也可以通過此狀態(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;
}
}
}
下面就是實(shí)現(xiàn)播放器播放,暫停等方法了
//播放暫停
- (void)playerPaused {
[self.player pause];
}
//播放繼續(xù)
- (void)playerPlay {
[self.player play];
self.player.rate = _ratevalue;
}
關(guān)閉播放器,記得移除通知,置空播放器
- (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];
}
對,還有播放器鎖屏的實(shí)現(xiàn)方法,可在.h里面寫一個(gè)鎖屏方法供外部調(diào)用,鎖屏方法就是在外部播放器代理里面調(diào)用,如果鎖屏中加載圖片為網(wǎng)絡(luò)圖片的話,最好做一個(gè)字典通過key-value來存儲(chǔ)
-(void)SetlockScreenInformation:(DetailCourseListModel *)model{
//model是項(xiàng)目中用到的,可根據(jù)自己需求定義
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:[[MPNowPlayingInfoCenter defaultCenter] nowPlayingInfo]];
[dict setObject:model.teacherName==nil?@"1":model.name forKey:MPMediaItemPropertyTitle];
//此表現(xiàn)形式 name-title 副標(biāo)題
// [dict setObject:recordUploadModel.teacherName==nil?@"1":recordUploadModel.teacherName forKey:MPMediaItemPropertyArtist];
[dict setObject:model.name==nil?@"1":model.name forKey:MPMediaItemPropertyAlbumTitle];
NSString *imageUrl;
//判斷當(dāng)前是個(gè)鏈接還是上傳路徑
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];
//當(dāng)前已經(jīng)過時(shí)間
[dict setObject:[NSNumber numberWithDouble:_currentTime] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
//總時(shí)間
[dict setObject:[NSNumber numberWithDouble:_totalTime] forKey:MPMediaItemPropertyPlaybackDuration];
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
}
寫了那么多了,估計(jì)看的也沒有耐心了.鎖屏后怎么在鎖屏界面或控制中心對播放器做暫停,上一曲下一曲等操作呢,可以在控制器中加入以下代碼:
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
// 開始接受遠(yuǎn)程控制
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
//成為第一響應(yīng)者
[self becomeFirstResponder];
// 開啟界面常亮
[[UIApplication sharedApplication] setIdleTimerDisabled:YES];
}
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
//接觸遠(yuǎn)程控制
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
[self resignFirstResponder];
// 關(guān)閉界面常亮
[[UIApplication sharedApplication] setIdleTimerDisabled:NO];
}
重寫父類成為響應(yīng)者方法
// 重寫父類成為響應(yīng)者方法
- (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;
}
}
}
給鎖屏界面實(shí)時(shí)傳數(shù)據(jù)呢,就在播放器代理里面
#pragma mark - 播放器代理時(shí)間
- (void)audioUpdateWith:(float)time Totaltime:(float)totalTime{
//滑動(dòng)進(jìn)度條
_slider.value = time/totalTime;
//刷新當(dāng)前時(shí)間,通過擴(kuò)展方法轉(zhuǎn)化成00:00:00格式
_currentTimeLB.text = [NSString timeTransformString:(float)time];
//刷新總時(shí)間
_totalTimeLB.text = [NSString timeTransformString:(float)totalTime];
//當(dāng)前標(biāo)題,就是model里面的
_titleLB.text = [NSString stringWithFormat:@"%@",MAudioPlay.currentAudioModel.name];
//在代理里面調(diào)用鎖屏方法,傳數(shù)據(jù)
[MAudioPlay SetlockScreenInformation:MAudioPlay.currentAudioModel];
}
秒轉(zhuǎn)換成00:00:00格式,我寫的是NSString的擴(kuò)展屬性,此方法也是在網(wǎng)上找的,用到手動(dòng)釋放,需要在工程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;
}