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

前言

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

正文

实现功能

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

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

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

Paste_Image.png

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

Paste_Image.png

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

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


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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容