流媒体开发(二)后台音频播放

简介

我们发现现在我们所做的音频播放只支持前台播放,当我们退出后台时,音频播放就会停止。但是通常我们看到的播放器即使退出到后台也是可以播放的,我们可以通过音频会话来实现音频的后台播放。

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章