前言
文章没有涉及在线音频流视频流播放
此播放器针对在线、离线音频播放、离线、在线视频播放
本文重点是对AVPlayer 、AVAudioPlayer在音频视频播放中应用,以及对这两个类的二次封装
正文
实现功能
1、在线音乐、本地音乐、在线视频、本地视频 播放
2、断点续播功能
3、后台播放、远程播放
概述
AVAudioPlayer 在网上有很多资料。其特点是只能播放一个完整的视频或者是音频文件,因此无法实现断点续播,一般用于本地播放。
AVPlayer 特点可以播放在线URL,即音频视频链接,可以实现断点续播,但是不会下载该文件。
工程准备
1、添加相应的库文件
2、涉及到网络播放,在info.plist中添加
NOTE:这是必须添加,否则无法播放在线音频视频
3、后台播放,开启background modes ,并且选中第一个模式
在info.plist 文件中添加相应的字段
为方便,我把改字段贴出来:App plays audio or streams audio/video using AirPlay
代码实现
为了方便实现多功能,开放出去的接口应该是一致,也就是说,只需要外接传入一个本地的URL或者是网络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可以执行之后,应该给AVAudioPlayer添加一个帧数定时器,根据帧数来获取当前AVAudioPlayer执行情况
- (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 ) {
//播放时间超过了总时间,做相应的处理
}
}
实现 AVAudioPlayerDelegate的代理方法,在相应代理做业务处理,而后,对AVAudioPlayer状态处理,包括play、pause、stop、resume、error 等这里代码不贴出来,具体看demo。
在.h文件声明代理方法,用于记录当前AVAudioPlayer的状态,包括当前播放进度、总时长、能否播放状态(rate)、状态改变通知等。
@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,而要实现AVPlayer播放,需要用到KVO,监听属性变化,包括AVPlayer 的状态status 和加载进度loadedTimeRanges 。
NSURL *soundUrl =[NSURL URLWithString:filePath];
AVPlayerItem *playerItem = [[AVPlayerItem alloc] initWithURL:soundUrl];
[self.player replaceCurrentItemWithPlayerItem:playerItem];
[self.player seekToTime:CMTimeMake(timeInterval, 1)];
添加KVO,同时监听加载进度
[self.player.currentItem addObserver:self forKeyPath:@"status" options:(NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew) context:nil];
//监控缓冲加载情况属性
[self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
//监控播放完成通知
[[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里面,通过属性变化做相应处理
- (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 添加代理,监听AVPlayer 各种状态变化
@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、本地视频、网络视频播放
视频播放用的基类也是AVPlayer ,区别在于,为了显示视频,必须要传入一个指定的view 。我们知道要UIView 之所以能够显示,是因为view下面的layer 。因此在view.layer 下面必须要添加AVPlayerLayer 。要加载初始化AVPlayerLayer,首先要添加AVURLAsset。具体如下
NSURL *videoUrl;
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {// 本地视频
videoUrl = [NSURL fileURLWithPath:filePath];
}
else{// 网络视频
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处理相应的逻辑,这里不再赘述。
4、两大基类的二次封装
上述的四个功能可以封装成两个类MyMusicPlayerAVPlayer 类和MyMusicPlayerAudioSession 里面。如果要实现上述的四个功能,那么外界必须要实现这两个类的代理,这样外界才可以接收到数据进行处理。。这样会导致过多冗余的代码出现。因此,将两大基类再进行一次封装到一个统一类,由这个统一类暴露出接口,统一接收数据,处理。
@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
上述的代理是暴露出来的接口,外接只需要在这几个代理那边接收到数据做相应处理即可。
5、断点续播
这个其实很好实现,只需要在同一类的代理里面将当前播放时间保存在本地,然后在初始化播放器的时候,将时间传入即可实现。
- (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];
}
}
在初始化控制器的时候,将保存的时间传入
// 断点续播
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、远程控制
首先在初始化播放器的时候,注册通知
[[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中相应通知
- (void)applicationDidEnterBackground:(UIApplication *)application {
[[NSNotificationCenter defaultCenter] postNotificationName:[NSString stringWithUTF8String:APPLICATION_WILL_ENTER_BACKGROUND] object:nil];
}
实现远程控制的代理方法
-(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]; //音乐当前已经播放时间
[dict setObject:[NSNumber numberWithFloat:[[MyMusicPlayerEngine shareInstance] getPlayRate]] forKey:MPNowPlayingInfoPropertyPlaybackRate];//音乐播放的状态
[dict setObject:[NSNumber numberWithDouble:[[MyMusicPlayerEngine shareInstance] getPlayDurationTime]] forKey:MPMediaItemPropertyPlaybackDuration];//歌曲总时间设置
if (systemVersionUp(10.0)) {
[dict setObject:[NSNumber numberWithFloat:self.slider.value * [[MyMusicPlayerEngine shareInstance] getPlayDurationTime]] forKey:MPNowPlayingInfoPropertyPlaybackProgress];
}
// 标题
[dict setObject:self.musicTitle.text forKey:MPMediaItemPropertyTitle];
// 章节名
[dict setObject:@"Eason-陈奕迅" forKey:MPMediaItemPropertyAlbumTitle];
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
}
总结
视频播放也可以使用MPMoviePlayerViewController,考虑到减少代码量,便于统一管理,就没有采用。
整体来说,这个播放器难度不大,重点在于对于类的封装,最大化简化代码,减少没有营养重复冗余的代码。
demo已经放到github 上面,有兴趣可以下载查看。
https://github.com/iosFarmer/MyMusicPlayer