前言
文章沒有涉及在線音頻流視頻流播放
此播放器針對在線、離線音頻播放、離線、在線視頻播放
本文重點是對AVPlayer 、AVAudioPlayer在音頻視頻播放中應(yīng)用,以及對這兩個類的二次封裝
正文
實現(xiàn)功能
1、在線音樂、本地音樂、在線視頻、本地視頻 播放
2、斷點續(xù)播功能
3、后臺播放、遠(yuǎn)程播放
概述
AVAudioPlayer 在網(wǎng)上有很多資料。其特點是只能播放一個完整的視頻或者是音頻文件,因此無法實現(xiàn)斷點續(xù)播,一般用于本地播放。
AVPlayer 特點可以播放在線URL,即音頻視頻鏈接,可以實現(xiàn)斷點續(xù)播,但是不會下載該文件。
工程準(zhǔn)備
1、添加相應(yīng)的庫文件

2、涉及到網(wǎng)絡(luò)播放,在info.plist中添加

NOTE:這是必須添加,否則無法播放在線音頻視頻
3、后臺播放,開啟background modes ,并且選中第一個模式

在info.plist 文件中添加相應(yīng)的字段

為方便,我把改字段貼出來:App plays audio or streams audio/video using AirPlay
代碼實現(xiàn)
為了方便實現(xiàn)多功能,開放出去的接口應(yīng)該是一致,也就是說,只需要外接傳入一個本地的URL或者是網(wǎng)絡(luò)URL即可播放音樂,因此對于兩大基類的封裝勢在必行,后面會提到二次封裝。首先先進行對基類的第一個封裝。
1、本地音樂播放
NSURL *musicUrl = [[NSURL alloc] initFileURLWithPath:_localFilePath isDirectory:NO];
NSError *error = nil;
self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:musicUrl error:&error];
self.player.delegate = self;
if (error) {
NSLog(@"[NCMusicEngine] AVAudioPlayer initial error: %@", error);
self.error = error;
}
_localFilePath是工程里面本地文件的路徑
在判斷AVAudioPlayer可以執(zhí)行之后,應(yīng)該給AVAudioPlayer添加一個幀數(shù)定時器,根據(jù)幀數(shù)來獲取當(dāng)前AVAudioPlayer執(zhí)行情況
- (void)startPlayCheckingTimer {
//
if (_playCheckingTimer) {
[_playCheckingTimer invalidate];
_playCheckingTimer = nil;
}
_playCheckingTimer = [NSTimer scheduledTimerWithTimeInterval:kNCMusicEngineCheckMusicInterval
target:self
selector:@selector(handlePlayCheckingTimer:)
userInfo:nil
repeats:YES];
}
- (void)handlePlayCheckingTimer:(NSTimer *)timer {
//
NSTimeInterval playerCurrentTime = self.player.currentTime;
NSTimeInterval playerDuration = [self getPlayDurationTime];//self.player.duration;
if (self.delegate && [self.delegate respondsToSelector:@selector(engine:playCurrentTime:playDuration:)]) {
if (playerDuration <= 0)
[self.delegate engine:self playCurrentTime:playerCurrentTime playDuration:playerDuration];
else
[self.delegate engine:self playCurrentTime:playerCurrentTime playDuration:playerDuration];
}
playerDuration = self.player.duration;
if (playerDuration - playerCurrentTime < kNCMusicEnginePauseMargin ) {
//播放時間超過了總時間,做相應(yīng)的處理
}
}
實現(xiàn) AVAudioPlayerDelegate的代理方法,在相應(yīng)代理做業(yè)務(wù)處理,而后,對AVAudioPlayer狀態(tài)處理,包括play、pause、stop、resume、error 等這里代碼不貼出來,具體看demo。
在.h文件聲明代理方法,用于記錄當(dāng)前AVAudioPlayer的狀態(tài),包括當(dāng)前播放進度、總時長、能否播放狀態(tài)(rate)、狀態(tài)改變通知等。
@protocol MyMusicPlayerAudioSessionDelegate <NSObject>
@optional
- (void)engine:(MyMusicPlayerAudioSession *)engine didChangePlayState:(MyAudioSessionState)playState;
- (void)engine:(MyMusicPlayerAudioSession *)engine downloadProgress:(CGFloat)progress;
- (void)engine:(MyMusicPlayerAudioSession *)engine playCurrentTime:(NSTimeInterval)currentTime playDuration:(NSTimeInterval)duration;
- (void)engineDidFinishPlaying:(MyMusicPlayerAudioSession *)engine successfully:(BOOL)flag;
- (void)engineBeginInterruptionPlaying:(MyMusicPlayerAudioSession *)engine;
- (void)engineEndInterruptionPlaying:(MyMusicPlayerAudioSession *)engine;
- (void)engine:(MyMusicPlayerAudioSession *)engine playFail:(NSError *)error;
@end
2、在線音樂播放
在線播放主要引用到的是AVPlayer,而要實現(xiàn)AVPlayer播放,需要用到KVO,監(jiān)聽屬性變化,包括AVPlayer 的狀態(tài)status 和加載進度loadedTimeRanges 。
NSURL *soundUrl =[NSURL URLWithString:filePath];
AVPlayerItem *playerItem = [[AVPlayerItem alloc] initWithURL:soundUrl];
[self.player replaceCurrentItemWithPlayerItem:playerItem];
[self.player seekToTime:CMTimeMake(timeInterval, 1)];
添加KVO,同時監(jiān)聽加載進度
[self.player.currentItem addObserver:self forKeyPath:@"status" options:(NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew) context:nil];
//監(jiān)控緩沖加載情況屬性
[self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
//監(jiān)控播放完成通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem];
// 加載進度
self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
}];
在KVO里面,通過屬性變化做相應(yīng)處理
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
AVPlayerItem *playerItem = object;
if ([keyPath isEqualToString:@"status"]) {
AVPlayerItemStatus status = (AVPlayerItemStatus)[change[@"new"] integerValue];
switch (status) {
case AVPlayerItemStatusReadyToPlay:
{
// 開始播放
if (![change[@"new"] isEqual:change[@"old"]]) {
[self play];
}
}
break;
case AVPlayerItemStatusFailed:
{
}
break;
case AVPlayerItemStatusUnknown:
{
}
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;
CMTime duration = playerItem.duration;
float totalDuration = CMTimeGetSeconds(duration);
if (self.delegate && [self.delegate respondsToSelector:@selector(avplayer:updateBufferProgress:isCanPlay:)])
{
[self.delegate avplayer:self updateBufferProgress:totalBuffer / totalDuration isCanPlay:isCanPlay];
}
}
}
同理,給AVPlayer 添加代理,監(jiān)聽AVPlayer 各種狀態(tài)變化
@protocol MyMusicPlayerAVPlayerDelegate <NSObject>
@optional
- (void)avplayer:(MyMusicPlayerAVPlayer *)avplayer updateBufferProgress:(NSTimeInterval)progress isCanPlay:(BOOL)isCanPlay;
- (void)avplayer:(MyMusicPlayerAVPlayer *)avplayer updatePlayerTime:(NSTimeInterval)time DurationTime:(NSTimeInterval)duration;
- (void)avplayer:(MyMusicPlayerAVPlayer *)avplayer avPlayerDidFinished:(BOOL)isSuccessfully;
- (void)avplayer:(MyMusicPlayerAVPlayer *)avplayer avPlayerDidError:(NSError *)error;
- (void)avplayer:(MyMusicPlayerAVPlayer *)avplayer avPlayerStatusChange:(MyAVPlayerStatus)playerStatus;
@end
3、本地視頻、網(wǎng)絡(luò)視頻播放
視頻播放用的基類也是AVPlayer ,區(qū)別在于,為了顯示視頻,必須要傳入一個指定的view 。我們知道要UIView 之所以能夠顯示,是因為view下面的layer 。因此在view.layer 下面必須要添加AVPlayerLayer 。要加載初始化AVPlayerLayer,首先要添加AVURLAsset。具體如下
NSURL *videoUrl;
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {// 本地視頻
videoUrl = [NSURL fileURLWithPath:filePath];
}
else{// 網(wǎng)絡(luò)視頻
videoUrl = [NSURL URLWithString:filePath];
}
self.assetPlayer = [AVURLAsset URLAssetWithURL:videoUrl options:nil];
AVPlayerItem *assetItem = [AVPlayerItem playerItemWithAsset:_assetPlayer];
[self.player replaceCurrentItemWithPlayerItem:assetItem];
AVPlayerLayer *playerLayer =[AVPlayerLayer playerLayerWithPlayer:_player];
[playerLayer setFrame:view.bounds];
playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
[view.layer addSublayer:playerLayer];
[self.player seekToTime:CMTimeMake(timeInterval, 1)];
而同上面的在線播放音樂一樣,添加KVO,通過KVO處理相應(yīng)的邏輯,這里不再贅述。
4、兩大基類的二次封裝
上述的四個功能可以封裝成兩個類MyMusicPlayerAVPlayer 類和MyMusicPlayerAudioSession 里面。如果要實現(xiàn)上述的四個功能,那么外界必須要實現(xiàn)這兩個類的代理,這樣外界才可以接收到數(shù)據(jù)進行處理。。這樣會導(dǎo)致過多冗余的代碼出現(xiàn)。因此,將兩大基類再進行一次封裝到一個統(tǒng)一類,由這個統(tǒng)一類暴露出接口,統(tǒng)一接收數(shù)據(jù),處理。
@protocol MyMusicPlayerEngineDelegate <NSObject>
@optional
- (void)engine:(NSObject *)player didChangeEngineStatus:(EnginePlayerStatus)EngineStaus;
- (void)engine:(NSObject *)player bufferProgress:(CGFloat)progress isCanPlay:(BOOL)isCanPlay;
- (void)engine:(NSObject *)player playerCurrentTime:(NSTimeInterval)current durationTime:(NSTimeInterval)durationTime;
- (void)engine:(NSObject *)player didFinishedSuccessfully:(BOOL)isSuccessfully;
- (void)engine:(NSObject *)player didPlayMusicFailed:(NSError *)error;
@end
上述的代理是暴露出來的接口,外接只需要在這幾個代理那邊接收到數(shù)據(jù)做相應(yīng)處理即可。
5、斷點續(xù)播
這個其實很好實現(xiàn),只需要在同一類的代理里面將當(dāng)前播放時間保存在本地,然后在初始化播放器的時候,將時間傳入即可實現(xiàn)。
- (void)avplayer:(MyMusicPlayerAVPlayer *)avplayer updatePlayerTime:(NSTimeInterval)time DurationTime:(NSTimeInterval)duration{
if (self.delegate && [self.delegate respondsToSelector:@selector(engine:playerCurrentTime:durationTime:)]) {
[self.delegate engine:avplayer playerCurrentTime:time durationTime:duration];
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithFloat:time] forKey:KCurrentTimeRecoder];
}
}
在初始化控制器的時候,將保存的時間傳入
// 斷點續(xù)播
NSNumber *seekToTime = (NSNumber *)[[NSUserDefaults standardUserDefaults] objectForKey:KCurrentTimeRecoder];
NSTimeInterval timeInterval;
if (seekToTime == nil) {
timeInterval = 0;
}
else{
timeInterval = seekToTime.floatValue;
}
[self.player seekToTime:CMTimeMake(timeInterval, 1)];
6、遠(yuǎn)程控制
首先在初始化播放器的時候,注冊通知
[[NSNotificationCenter defaultCenter] removeObserver:self name:[NSString stringWithUTF8String:APPLICATION_WILL_ENTER_BACKGROUND] object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:)
name:[NSString stringWithUTF8String:APPLICATION_WILL_ENTER_BACKGROUND] object:nil];
而后在AppDelegate.m文件中applicationDidEnterBackground中相應(yīng)通知
- (void)applicationDidEnterBackground:(UIApplication *)application {
[[NSNotificationCenter defaultCenter] postNotificationName:[NSString stringWithUTF8String:APPLICATION_WILL_ENTER_BACKGROUND] object:nil];
}
實現(xiàn)遠(yuǎn)程控制的代理方法
-(void)remoteControlReceivedWithEvent:(UIEvent *)event
{
if (event.type == UIEventTypeRemoteControl) {
UIEventSubtype subtype = event.subtype;
switch (subtype) {
case UIEventSubtypeRemoteControlPlay:
[[MyMusicPlayerEngine shareInstance] resume];
break;
case UIEventSubtypeRemoteControlPause:
[[MyMusicPlayerEngine shareInstance] pause];
break;
case UIEventSubtypeRemoteControlNextTrack:
NSLog(@"下一首");
break;
case UIEventSubtypeRemoteControlPreviousTrack:
NSLog(@"上一首");
break;
default:
break;
}
}
}
鎖屏界面信息顯示
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
[self becomeFirstResponder];
NSMutableDictionary * dict = [[NSMutableDictionary alloc] init];
if (NSClassFromString(@"MPNowPlayingInfoCenter")) {
[dict setObject:[NSNumber numberWithDouble:[[MyMusicPlayerEngine shareInstance] getPlayCurrentTime]] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime]; //音樂當(dāng)前已經(jīng)播放時間
[dict setObject:[NSNumber numberWithFloat:[[MyMusicPlayerEngine shareInstance] getPlayRate]] forKey:MPNowPlayingInfoPropertyPlaybackRate];//音樂播放的狀態(tài)
[dict setObject:[NSNumber numberWithDouble:[[MyMusicPlayerEngine shareInstance] getPlayDurationTime]] forKey:MPMediaItemPropertyPlaybackDuration];//歌曲總時間設(shè)置
if (systemVersionUp(10.0)) {
[dict setObject:[NSNumber numberWithFloat:self.slider.value * [[MyMusicPlayerEngine shareInstance] getPlayDurationTime]] forKey:MPNowPlayingInfoPropertyPlaybackProgress];
}
// 標(biāo)題
[dict setObject:self.musicTitle.text forKey:MPMediaItemPropertyTitle];
// 章節(jié)名
[dict setObject:@"Eason-陳奕迅" forKey:MPMediaItemPropertyAlbumTitle];
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
}
總結(jié)
視頻播放也可以使用MPMoviePlayerViewController,考慮到減少代碼量,便于統(tǒng)一管理,就沒有采用。
整體來說,這個播放器難度不大,重點在于對于類的封裝,最大化簡化代碼,減少沒有營養(yǎng)重復(fù)冗余的代碼。
demo已經(jīng)放到github 上面,有興趣可以下載查看。
https://github.com/iosFarmer/MyMusicPlayer