流媒體開發(二)後臺音頻播放

簡介

我們發現現在我們所做的音頻播放只支持前臺播放,當我們退出後臺時,音頻播放就會停止。但是通常我們看到的播放器即使退出到後臺也是可以播放的,我們可以通過音頻會話來實現音頻的後臺播放。

1. 音頻會話

在iOS中每個應用都有一個音頻會話,這個會話就通過AVAudioSession來表示。AVAudioSession同樣存在於AVFoundation框架中,它是單例模式設計,通過sharedInstance進行訪問。在使用Apple設備時大家會發現有些應用只要打開其他音頻播放就會終止,而有些應用卻可以和其他應用同時播放,在多種音頻環境中如何去控制播放的方式就是通過音頻會話來完成的。

在獲得AVAudioSession類的實例後,你就能通過調用音頻會話對象的 setCategory:error: 實例方法,來從IOS應用可用的音頻會話的不同類別中作出選擇。但是注意一下,設置完音頻會話類型之後需要調用setActive:error:方法將會話激活才能起作用。類似的,如果一個應用已經在播放音頻,打開我們的應用之後設置了在後臺播放的會話類型,此時其他應用的音頻會停止而播放我們的音頻,如果希望我們的程序音頻播放完之後(關閉或退出到後臺之後)能夠繼續播放其他應用的音頻的話則可以調用setActive:error:方法關閉會話。

下面是音頻會話的幾種會話模式:

會話類型 說明 是否要求輸入 是否要求輸出 是否遵從靜音鍵
AVAudioSessionCategoryAmbient 混音播放,可以與其他音頻應用同時播放
AVAudioSessionCategorySoloAmbient 獨佔播放
AVAudioSessionCategoryPlayback 後臺播放,也是獨佔的
AVAudioSessionCategoryRecord 錄音模式,用於錄音時使用
AVAudioSessionCategoryPlayAndRecord 播放和錄音,此時可以錄音也可以播放
AVAudioSessionCategoryAudioProcessing 硬件解碼音頻,此時不能播放和錄製
AVAudioSessionCategoryMultiRoute}多種輸入輸出,例如可以耳機、USB設備同時播放

注意:

是否遵循靜音鍵表示在播放過程中如果用戶通過硬件設置爲靜音是否能關閉聲音。

下面是不變通音頻會話模式的使用場景及相關注意事項:

  • AVAudioSessionCategoryAmbient:後臺播放類型,會和其它音樂混合的音頻類型。 這個類別用於音頻比較次要的應用,應用的音頻會和其他應用的音頻實現混音,關閉屏幕或者靜音開關打開時音頻將靜音.
  • AVAudioSessionCategorySoloAmbient:後臺播放類型,其它音樂會停止播放, 是默認類別。這個類別非常像AVAudioSessionCategoryAmbient 類別,除了會停止其他程序的音頻回放。
  • AVAudioSessionCategoryPlayback:這個類別會靜止其他應用的音頻回放。你可以使用AVAudioPlaye的prepareToPlay和play方法,在你的應用中播放聲音。主UI界面會照常工作。這時,即使屏幕被鎖定或者設備爲靜音模式,音頻回放都會繼續。
  • AVAudioSessionCategoryRecord:錄音時使用。 這會停止其他應用的聲音並讓你的應用也不能初始化音頻回放(比如AVAudioPlaye)。在這種模式下,你只能進行錄音。使用這個類別,調用AVAudioPlaye 的 prepareToPlay 會返回 true,但是調用 play 方法將返回 false。主UI界面會照常工作。這時,即使你的設備屏幕被用戶鎖定了,應用的錄音仍會繼續。

  • AVAudioSessionCategoryPlayAndRecord:錄音並需要播放時使用。這個類別允許你的應用中同時進行聲音的播放和錄製。當你的聲音錄製或播放開始後,其他應用的聲音播放將會停止。主UI界面會照常工作。這時,即使屏幕被鎖定或者設備爲靜音模式,音頻回放和錄製都會繼續。

  • AVAudioSessionCategoryAudioProcessing:這個類別用於音頻處理,比如編碼解碼時/不播放音頻/未錄音時使用。設置了這種模式,你在應用中就不能播放和錄製任何聲音。調用AVAudioPlaye的prepareToPlay 和play方法都將返回false。其他應用的音頻也會在此模式下停止。
  • AVAudioSessionCategoryMultiRoute:這個類別可以實現同時可以有多種輸出,例如:usb和耳機同時輸出,但並非所有輸入輸出方式均支持. 輸入方式僅包括:
    AVAudioSessionPortUSBAudio
    AVAudioSessionPortHeadsetMic
    AVAudioSessionPortBuiltInMic
    輸出僅包括:
    AVAudioSessionPortUSBAudio
    AVAudioSessionPortLineOut
    AVAudioSessionPortHeadphones
    AVAudioSessionPortHDMI
    AVAudioSessionPortBuiltInSpeaker

根據前面對音頻會話的理解,我們就可以使用音頻會話來實現音頻文件的後臺播放了,如果要支持後臺播放需要做下面幾件事情:

  1. 設置後臺運行模式:在plist文件中添加Required background modes,並且設置item 0=App plays audio or streams audio/video using AirPlay(其實可以直接通過Xcode在Project Targets-Capabilities-Background Modes中設置)。如果應用程序要允許運行到後臺必須設置,正常情況下應用如果進入後臺會被掛起,通過該設置可以上應用程序繼續在後臺運行。

  2. 設置AVAudioSession的類型爲AVAudioSessionCategoryPlayback並且調用setActive:error:方法啓動會話。

  3. 爲了能夠讓應用退到後臺之後支持耳機控制,建議添加遠程控制事件(這一步不是後臺播放必須的)

    前兩步是後臺播放所必須設置的,第三步主要用於接收遠程事件,方便我們在後臺對音頻播放的操作。如果這一步不設置雖讓也能夠在後臺播放,但是無法獲得音頻控制權(如果在使用當前應用之前使用其他播放器播放音樂的話,此時如果按耳機播放鍵或者控制中心的播放按鈕則會播放前一個應用的音頻),並且不能使用耳機進行音頻控制。

示例代碼

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>

@interfaceViewController ()
{
    AVAudioPlayer *_avAudioPlayer;
}
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    /*通過音頻會話實現後臺播放
     1. 在info.plist文件中添加Required background modes,並且設置item 0=App plays audio or streams audio/video using AirPlay
     2. 設置AVAudioSession的類型爲AVAudioSessionCategoryPlayback並且調用setActive:error:方法啓動會話。
     */

    // 播放音樂
    NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"東京不太熱" ofType:@"mp3"]];
    _avAudioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
    [_avAudioPlayer prepareToPlay];
    [_avAudioPlayer play];

    // 1.後臺播放必要的修改
    // info.plist文件添加設置:Required background modes
    // item:App plays audio or streams audio/video using AirPlay

    // 2.創建後臺會話並啓動
    AVAudioSession *session = [AVAudioSession sharedInstance];
    [session setCategory:AVAudioSessionCategoryPlayback error:nil];
    [session setActive:YES error:nil];

    // 3.接受遠程事件
    // 接受耳機的線性控制

    // 4.拔出耳機停止播放音樂
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseWithRouteChange:) name:AVAudioSessionRouteChangeNotification object:nil];
}

// 監聽拔出耳機的線控狀態
- (void)pauseWithRouteChange:(NSNotification *)notification {

    // 獲取通知詳情
    NSDictionary *dic=notification.userInfo;
    // 獲取輸出狀態
    int changeReason= [dic[AVAudioSessionRouteChangeReasonKey] intValue];

    if (changeReason==AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
        // 等於AVAudioSessionRouteChangeReasonOldDeviceUnavailable表示舊輸出不可用

        AVAudioSessionRouteDescription *routeDescription=dic[AVAudioSessionRouteChangePreviousRouteKey];
        AVAudioSessionPortDescription *portDescription= [routeDescription.outputs firstObject];
        //原設備爲耳機則暫停
        if ([portDescription.portType isEqualToString:@"Headphones"]) {
            if (_avAudioPlayer.playing) {
                [_avAudioPlayer pause];
            }
        }
    }
    [dic enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        NSLog(@"%@:%@",key,obj);
    }];
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    // 接受遠程事件
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];

    // 結束接受遠程事件
    [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
}

// 遠程操作觸發事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {

    NSLog(@"event type:%ld",event.subtype);
    switch (event.subtype) {
        case UIEventSubtypeRemoteControlTogglePlayPause:
            //播放或暫停切換【操作:播放或暫停狀態下,按耳機線控中間按鈕一下】
            if (_avAudioPlayer.playing) {
                [_avAudioPlayer pause];
            }else {
                [_avAudioPlayer play];
            }
            break;
        case UIEventSubtypeRemoteControlPause:
            //播放或暫停切換
            if (_avAudioPlayer.playing) {
                [_avAudioPlayer pause];
            }else {
                [_avAudioPlayer play];
            }
            break;
        case UIEventSubtypeRemoteControlNextTrack:
            NSLog(@"下一曲");
            break;
        case UIEventSubtypeRemoteControlPreviousTrack:
            NSLog(@"上一曲");
            break;
        default:
            break;
    }
}

#pragma mark - 播放器代理方法
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{
    NSLog(@"音樂播放完成...");
    //根據實際情況播放完成可以將會話關閉,其他音頻應用繼續播放
    [[AVAudioSession sharedInstance]setActive:NO error:nil];
}

 @end

在上面的代碼中還實現了拔出耳機暫停音樂播放的功能,這也是一個比較常見的功能。在iOS7及以後的版本中可以通過通知獲得輸出改變的通知,然後拿到通知對象後根據userInfo獲得是何種改變類型,進而根據情況對音樂進行暫停操作。

下面是在後臺實現遠程控制的事件類型及具體的操作:

  • UIEventSubtypeNone // 不包含任何子事件類型
  • UIEventSubtypeMotionShake // 搖晃事件(從iOS3.0開始支持此事件)
  • //遠程控制子事件類型(從iOS4.0開始支持遠程控制事件)
  • UIEventSubtypeRemoteControlPlay //播放事件【操作:停止狀態下,按耳機線控中間按鈕一下】
  • UIEventSubtypeRemoteControlPause //暫停事件
  • UIEventSubtypeRemoteControlStop //停止事件、
  • UIEventSubtypeRemoteControlTogglePlayPause //播放或暫停切換【操作:播放或暫停狀態下,按耳機線控中間按鈕一下】
  • UIEventSubtypeRemoteControlNextTrack //下一曲【操作:按耳機線控中間按鈕兩下】
  • UIEventSubtypeRemoteControlPreviousTrack //上一曲【操作:按耳機線控中間按鈕三下】
  • UIEventSubtypeRemoteControlBeginSeekingBackward //快退開始【操作:按耳機線控中間按鈕三下不要鬆開】
  • UIEventSubtypeRemoteControlEndSeekingBackward //快退停止【操作:按耳機線控中間按鈕三下到了快退的位置鬆開】
  • UIEventSubtypeRemoteControlBeginSeekingForward //快進開始【操作:按耳機線控中間按鈕兩下不要鬆開】
  • UIEventSubtypeRemoteControlEndSeekingForward //快進停止【操作:按耳機線控中間按鈕兩下到了快進的位置鬆開】

2. 音頻文件信息

通常情況下,設置後臺播放後,當屏幕處在鎖屏狀態時,我們可以在屏幕上看到正在播放歌曲的一些信息,例如歌曲海報,歌曲名稱及歌手等等,這時候就需要我們獲取音頻文件的信息並將這些信息在鎖屏狀態下完成顯示。AVFoundation框架有一個 AVURLAsset 類可以幫助我們獲取音頻文件中的音頻信息,而在MediaPlayer.framework框架中有一個 MPNowPlayingInfoCenter 類幫助我們實現鎖屏狀態下顯示音頻信息的功能。

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>

@interface ViewController ()
{
    AVAudioPlayer *_avAudioPlayer;
    //             歌曲名       歌手              專輯名
    NSString *_artName,*_artistName,*_album;
    UIImage *_artwork;//歌曲海報
}
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    /*
     1. 獲取MP3文件中包含的信息:封面、演奏者、專輯名等
     2. 實現後臺播放,並在鎖屏狀態下顯示音樂的封面和其他信息
     3. 注意,鎖屏狀態先顯示信息需要實現遠程控制事件
     */

    //播放音樂
    NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"東京不太熱" ofType:@"mp3"]];
    _avAudioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
    [_avAudioPlayer prepareToPlay];
    [_avAudioPlayer play];

    //1. 後臺播放必要的修改
    //info.plist文件添加設置:Required background modes
    //item:App plays audio or streams audio/video using AirPlay

    //2. 創建後臺會話並啓動
    AVAudioSession *session = [AVAudioSession sharedInstance];
    [session setCategory:AVAudioSessionCategoryPlayback error:nil];
    [session setActive:YES error:nil];


    //3. 獲取音樂文件信息
    // 獲取媒體
    AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:nil];
    // 獲取可用的媒體數據類型
    NSArray *formats = [asset availableMetadataFormats];
    NSLog(@"%ld",formats.count);

    for (NSString *format in formats) {

        //org.id3 :表示MP3文件
        if ([format isEqualToString:@"org.id3"]) {

            // 獲取到asset裏的所有數據:AVMetadataItem
            NSArray *items = [asset metadata];
            /*
             封面:artwork:image
             專輯名稱:albumName:東京不太熱
             歌曲名:title:東京不太熱
             表演者:artist:洛天依
             */
            // item的commonKey代表着儲存的信息類型;value代表對應的信息的值
            for (AVMetadataItem *item in items) {
                NSLog(@"%@:%@",item.commonKey,item.value);
                //歌曲名
                if ([item.commonKey isEqualToString:@"title"]) {
                    _artName = [NSString stringWithFormat:@"%@",item.value];
                }

                //表演者
                if ([item.commonKey isEqualToString:@"artist"]) {
                    _artistName = [NSString stringWithFormat:@"%@",item.value];
                }

                //專輯名稱
                if ([item.commonKey isEqualToString:@"albumName"]) {
                    _album = [NSString stringWithFormat:@"%@",item.value];
                }

                //歌曲海報
                if ([item.commonKey isEqualToString:@"artwork"]) {

                    _artwork = [UIImage imageWithData:(NSData *)item.value];
                }
            }
        }
    }

    // 加載歌曲海報
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
    imageView.image = _artwork;
    [self.view addSubview:imageView];

    // 4.後臺執行的代碼
    // 導入MediaPlayer.framework 庫文件
    // 設置鎖屏信息
    // 轉換UIImage爲 Artworky作爲鎖屏視圖
    MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:_artwork];
    [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:@{
                                                                MPMediaItemPropertyArtwork:artwork,
                                                                MPMediaItemPropertyTitle:_artName,
                                                                MPMediaItemPropertyArtist:_artistName
                                                                }];
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    // 接受遠程事件
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];

    // 結束接受遠程事件
    [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
}

// 遠程操作觸發事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {

    NSLog(@"event type:%ld",event.subtype);
    switch (event.subtype) {
        case UIEventSubtypeRemoteControlTogglePlayPause:
            //播放或暫停切換【操作:播放或暫停狀態下,按耳機線控中間按鈕一下】
            if (_avAudioPlayer.playing) {
                [_avAudioPlayer pause];
            }else {
                [_avAudioPlayer play];
            }
            break;
        case UIEventSubtypeRemoteControlPause:
            //播放或暫停切換
            if (_avAudioPlayer.playing) {
                [_avAudioPlayer pause];
            }else {
                [_avAudioPlayer play];
            }
            break;
        case UIEventSubtypeRemoteControlNextTrack:
            NSLog(@"下一曲");
            break;
        case UIEventSubtypeRemoteControlPreviousTrack:
            NSLog(@"上一曲");
            break;
        default:
            break;
    }
}

@end

注意:

在鎖屏狀態下顯示音頻信息並對音頻文件做相關操作,需要實現遠程控制事件,因爲鎖屏狀態操作音頻文件被系統認爲是遠程控制事件,會觸發遠程控制的方法。

3. 自定義後臺任務

iOS系統可以支持三種標準的後臺任務:音頻播放,後臺定位,IP電話,需要在plist文件設置。你的應用程序就支持對應的模式除去這三種標準的後臺模式,我們也可以添加自定義的後臺任務。比如,視頻、音頻的下載等等。但通過情況下,你的應用程序只能活躍600s,爲此我們需要對程序做一些額外的設置。

#import "AppDelegate.h"

@interface AppDelegate ()
{

    UIBackgroundTaskIdentifier bgTask;
    int count;
}
@end

@implementation AppDelegate
- (void)applicationDidEnterBackground:(UIApplication *)application {

    NSLog(@"+++++");
    [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];

}

- (void)timerAction {

    count ++;
    NSLog(@"%d",count);
    //因爲一個任務只能保持600秒,所以當500秒的時,新開一個新的任務
    if (count %500 == 0) {

        count = 0;

        bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{

            [[UIApplication sharedApplication] endBackgroundTask:bgTask];

            bgTask = UIBackgroundTaskInvalid;
        }];
    }
}

@end
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章