ios 在線本地音樂視頻播放器

前言

文章沒有涉及在線音頻流視頻流播放
此播放器針對在線、離線音頻播放、離線、在線視頻播放
本文重點是對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)的庫文件

Paste_Image.png

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

Paste_Image.png

NOTE:這是必須添加,否則無法播放在線音頻視頻

3、后臺播放,開啟background modes ,并且選中第一個模式


Paste_Image.png

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


Paste_Image.png

為方便,我把改字段貼出來: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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容