ios 在线本地音乐视频播放器

前言

文章没有涉及在线音频流视频流播放
此播放器针对在线、离线音频播放、离线、在线视频播放
本文重点是对AVPlayer 、AVAudioPlayer在音频视频播放中应用,以及对这两个类的二次封装

正文

实现功能

1、在线音乐、本地音乐、在线视频、本地视频 播放
2、断点续播功能
3、后台播放、远程播放

概述
AVAudioPlayer 在网上有很多资料。其特点是只能播放一个完整的视频或者是音频文件,因此无法实现断点续播,一般用于本地播放。
AVPlayer 特点可以播放在线URL,即音频视频链接,可以实现断点续播,但是不会下载该文件。

工程准备
1、添加相应的库文件

ios 在线本地音乐视频播放器_第1张图片
Paste_Image.png

2、涉及到网络播放,在info.plist中添加

Paste_Image.png

NOTE:这是必须添加,否则无法播放在线音频视频

3、后台播放,开启background modes ,并且选中第一个模式


ios 在线本地音乐视频播放器_第2张图片
Paste_Image.png

在info.plist 文件中添加相应的字段


Paste_Image.png

为方便,我把改字段贴出来: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 

@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 *)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 

@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 
@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

你可能感兴趣的:(ios 在线本地音乐视频播放器)