分享

流媒体开发(一)音频播放

 horkss 2021-11-29

                       
                   
                   

序言


随着人们学习、娱乐和工作的需要,人们对如何在网络上传输海量的视频、音频等多媒体信息有了进一步的要求。在这种背景下,iOS流媒体技术应运而生。通俗的讲,所谓的iOS流媒体技术,就是将视音频文件经过压缩处理后,放在网络服务器上进行分段的传输,客户端计算机不用将整个的视音频文件下载到本地,便可以即时收听和收看。


iOS流媒体格式的文件是经过了特殊的编码,才能够实现在网络上边边下边播,而不是将整个的影音文件全部下载后才能播放。通常,编码由专门的压缩编码软件来完成,使用者收听和收看网络影音文件则是一个解压缩的过程,这是由专门的播放器来完成的。所以,iOS流媒体文件具有它特殊的格式。网络上的影音服务,不同公司提供的格式、类型、以及传输方式都存在着差异。目前,在iOS流媒体领域中,参与竞争的公司主要有三个:微软、Real Networks和苹果公司,相应的iOS流媒体解决方案分别是:Windows Media、Real System和QuickTime.


在实际的开发工作中,流媒体开发是程序员必须掌握的开发技术。iOS SDK中提供了很多方便的方法来播放流媒体。iOS对于流媒体支持相当灵活和完善,既有高度封装的类帮助我们实现流媒体的开发工作,也开放了大量的底层接口供我们使用,我们可以自行封装实现我们需要的功能,开发的自由度非常高。


流媒体开发主要是对音频文件和视频文件的处理,下面我们先来看一下iOS中如何实现音频播放。




音频播放


在iOS中音频播放从形式上可以分为音效播放和音乐播放。前者主要指的是一些短音频播放,通常作为点缀音频,对于这类音频不需要进行进度、循环等控制;后者指的是一些较长的音频,通常是主音频,对于这些音频的播放通常需要进行精确的控制。


在iOS中播放两类音频分别使用AudioToolbox.framework和AVFoundation.framework来完成音效和音乐播放。



1. 音效播放


在iOS开发中,有时候需要我们频繁的播放某种提示声音,比如微博的刷新提示声音,QQ消息的提示声音等,对于这些短小且频繁的音频,我们最好将其加入到系统声音里。


音频数据可分为压缩和非压缩的存储类型。压缩的音频文件虽然文件体积较小(相对于非压缩的),但需要耗费处理器的性能进行解压和解码。如果音频文件体积较小,压缩后的音频文件,也不会节省较大的磁盘空间。像这一类小型非压缩的文件可以注册成为系统声音。


通过AudioToolbox.framework来完成系统声音的注册和播放:


  AudioToolbox.framework是一套基于C语言的框架,使用它来播放音效其本质是将短音频注册到系统声音服务(System Sound Service)。
  System Sound Service是一种简单、底层的声音播放服务,但是它本身也存在着一些限制:
  1. 音频播放时间不能超过30s
  2. 数据必须是PCM或者IMA4格式
  3. 音频文件必须打包成.caf、.aif、.wav中的一种(注意这是官方文档的说法,实际测试发现一些.mp3也可以播放)

使用System Sound Service 播放音效的步骤如下:


  1. 调用AudioServicesCreateSystemSoundID(CFURLRef inFileURL, SystemSoundID* outSystemSoundID) 函数获得系统声音ID。
  2. 如果需要监听播放完成操作,则使用AudioServicesAddSystemSoundCompletion( SystemSoundID inSystemSoundID,CFRunLoopRef inRunLoop, CFStringRef inRunLoopMode, AudioServicesSystemSoundCompletionProc inCompletionRoutine, void* inClientData)方法注册回调函数。
  3. 调用AudioServicesPlaySystemSound(SystemSoundID inSystemSoundID) 或者AudioServicesPlayAlertSound(SystemSoundID inSystemSoundID) 方法播放音效(后者带有震动效果)。

示例代码:


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

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 1.获取音频文件
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"msgcome" ofType:@"wav"];
    NSURL *fileURL = [NSURL fileURLWithPath:filePath];

    // 2.注册声音到系统 -> 获取系统声音ID
    SystemSoundID soundID = 0;
    AudioServicesCreateSystemSoundID((__bridge CFURLRef)(fileURL), &soundID);
    unsigned long sID = soundID;//SystemSoundID不能直接打印,需要一次中转
    NSLog(@"soundID:%lu",sID);
    /* 通过该函数将音频文件注册为系统声音
     * 该函数调用完成后,系统自动分配soundID
     * 参数一:音频文件路径
     <#CFURLRef  _Nonnull inFileURL#>
     * 参数二:注册的系统声音ID
     <#SystemSoundID * _Nonnull outSystemSoundID#>
     */

    // 3.播放系统声音
    AudioServicesPlaySystemSound(soundID);// 播放音效
//    AudioServicesPlayAlertSound(soundID);// 播放系统声音并震动

    // 4.监听播放完成的操作
    AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, soundCompletionCallback, (__bridge void *)self);
    /*
     *参数说明:
     * 1、刚刚播放完成自定义系统声音的ID
     * 2、回调函数(playFinished)执行的run Loop,NULL表示main run loop
     * 3、回调函数执行所在run loop的模式,NULL表示默认的run loop mode
     * 4、需要回调的函数,按照要求的函数格式自定义回调函数
     * 5、传入的参数, 此参数会被传入回调函数里
     */

}

// 音效播放结束的回调函数
void soundCompletionCallback (SystemSoundID mySSID, void* __nullable data) {

    NSLog(@"completion Callback");
    unsigned long sID = mySSID;//SystemSoundID不能直接打印,需要一次中转
    NSLog(@"soundID:%lu",sID);

    /*ARC中C语言函数需要自己管理内存,所以这里,我们需要回调完成后移除刚刚添加的操   作 ,并且确认当你以后不再需要播放该ID的音频时,清理该ID音频对应的所有资源
     */
    // 根据ID移除播放完成后回调执行的函数
    AudioServicesRemoveSystemSoundCompletion(mySSID);

    // 根据ID释放自定义的系统声音
    AudioServicesDisposeSystemSoundID(mySSID);
}

@end

注意:


  • 必须等到播放完成才能清理该ID对应的所有资源,也就是说,如果我们打算不再使用该音频,可以在完成后调用的函数里清理资源,如上例所示。 不过一般情况下,既然加到系统声音里的多是频繁使用的,可以在程序结束或者某个控制器销毁的时候再清理。
  • 如果在AudioServicesPlaySystemSound(ID)之后马上调用 AudioServicesDisposeSystemSoundID(ID),你将听不到任何声音,并且也不会调用播放完成后的函数,这是因为系统音频播放的所有操作都是放到主线程之外执行的,当主线程执行到清理的时候,该音频如果试图播放ID对应的音频,将无法找到。



2. 音乐播放


如果播放较大的音频或者要对音频有精确的控制则System Sound Service可能就很难满足实际需求了,通常这种情况会选择使用AVFoundation.framework中的AVAudioPlayer来实现。AVAudioPlayer可以看成一个播放器,它支持多种音频格式,而且能够进行进度、音量、播放速度等控制。


首先简单看一下AVAudioPlayer常用的属性和方法:


属性说明
@property(readonly, getter=isPlaying) BOOL playing是否正在播放,只读
@property(readonly) NSUInteger numberOfChannels音频声道数,只读
@property(readonly) NSTimeInterval duration音频时长
@property(readonly) NSURL *url音频文件路径,只读
@property(readonly) NSData *data音频数据,只读
@property float pan立体声平衡,如果为-1.0则完全左声道,如果0.0则左右声道平衡,如果为1.0则完全为右声道
@property float volume音量大小,范围0-1.0
@property BOOL enableRate是否允许改变播放速率
@property float rate播放速率,范围0.5-2.0,如果为1.0则正常播放,如果要修改播放速率则必须设置enableRate为YES
@property NSTimeInterval currentTime当前播放时长
@property(readonly) NSTimeInterval deviceCurrentTime输出设备播放音频的时间,注意如果播放中被暂停此时间也会继续累加
@property NSInteger numberOfLoops循环播放次数,如果为0则不循环,如果小于0则无限循环,大于0则表示循环次数
@property(readonly) NSDictionary *settings音频播放设置信息,只读
@property(getter=isMeteringEnabled) BOOL meteringEnabled是否启用音频测量,默认为NO,一旦启用音频测量可以通过updateMeters方法更新测量值
@property(nonatomic, copy) NSArray *channelAssignments获得或设置播放声道
对象方法说明
-(instancetype)initWithContentsOfURL:(NSURL )url error:(NSError *)outError使用文件URL初始化播放器,注意这个URL不能是HTTP URL,AVAudioPlayer不支持加载网络媒体流,只能播放本地文件
-(instancetype)initWithData:(NSData )data error:(NSError *)outError使用NSData初始化播放器,注意使用此方法时必须文件格式和文件后缀一致,否则出错,所以相比此方法更推荐使用上述方法或- (instancetype)initWithData:(NSData )data fileTypeHint:(NSString )utiString error:(NSError **)outError方法进行初始化
-(BOOL)prepareToPlay;加载音频文件到缓冲区,注意即使在播放之前音频文件没有加载到缓冲区程序也会隐式调用此方法。
-(BOOL)play;播放音频文件
-(BOOL)playAtTime:(NSTimeInterval)time在指定的时间开始播放音频
-(void)pause;暂停播放
-(void)stop;停止播放
-(void)updateMeters更新音频测量值,注意如果要更新音频测量值必须设置meteringEnabled为YES,通过音频测量值可以即时获得音频分贝等信息
-(float)peakPowerForChannel:(NSUInteger)channelNumber;获得指定声道的分贝峰值,注意如果要获得分贝峰值必须在此之前调用updateMeters方法
-(float)averagePowerForChannel:(NSUInteger)channelNumber获得指定声道的分贝平均值,注意如果要获得分贝平均值必须在此之前调用updateMeters方法
代理方法说明
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag音频播放完成
-(void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer )player error:(NSError )error音频解码发生错误

下面是AVAudioPlayer的使用步骤:


  1. 初始化AVAudioPlayer对象,此时通常指定本地文件路径。
    注意:需要设置为全局变量,系统没有任何的逻辑对其进行强引用,像视图添加在父视图上,会抑制存在,如果设置为局部变量,音乐还没有播放,就被自动释放掉了。
  2. 设置播放器属性,例如重复次数、音量大小等。
  3. 调用play方法播放。

示例代码:


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

@interface ViewController ()<AVAudioPlayerDelegate>
{
    AVAudioPlayer *_audioPlayer;//音频播放器

    __weak IBOutlet UISlider *_progress;
    __weak IBOutlet UISlider *_volume;

}
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 1.加载音频文件
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"东京不太热" ofType:@"mp3"];
    NSURL *fileURL = [NSURL fileURLWithPath:filePath];

    // 2.初始化音频播放器
    //使用文件URL初始化播放器,注意这个URL不能是HTTP URL,AVAudioPlayer不支持加载网络媒体流,只能播放本地文件
    NSError *error = nil;
    _audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&error];
    if (error) {

        NSLog(@"初始化音频播放器出错,error:%@",error);
        return;
    }
    // 3.设置播放器属性
    {
        _audioPlayer.numberOfLoops = -1;// 循环次数
        _audioPlayer.delegate = self;// 代理对象

        CGFloat duration = _audioPlayer.duration;// 播放时长
        NSLog(@"音乐总时长:%.2f",duration);
        _audioPlayer.currentTime = 20;// 当前的播放时间
        _audioPlayer.volume = 0.5;// 音量 0.0~1.0

        _audioPlayer.enableRate = YES;// 允许改变播放速率
        _audioPlayer.rate = 1.0;// 正常速率

        [_audioPlayer prepareToPlay];// 加载音频文件到播放队列,准备播放
        [_audioPlayer play];// 开始播放
    }

    // UI设置
    [_volume setValue:_audioPlayer.volume];
    [_progress setValue:_audioPlayer.currentTime/_audioPlayer.duration];
    //添加计时器方法,刷新进度条
    CADisplayLink *displayLink  = [CADisplayLink displayLinkWithTarget:self selector:@selector(updataProgress)];
    //CADisplayLink需要将显示链接添加到一个运行循环中,即用于处理系统事件的一个Cocoa Touch结构。
    [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

}

// 即时调用刷新进度条的value值
- (void)updataProgress {

    // 计算播放进度 = 当前播放时间/播放总时长
    CGFloat progress = _audioPlayer.currentTime/_audioPlayer.duration;
    [_progress setValue:progress];// 设置进度条的播放进度

}

// 拖动进度条,改变音频的播放音量
- (IBAction)volumeAction:(UISlider *)sender {

    // 设置音量
    _audioPlayer.volume = sender.value;
}

// 拖动进度条,改变音频的播放进度
- (IBAction)progress:(UISlider *)sender {

    // 进度条的拖动进度
    CGFloat progress = sender.value;

    // 设置当前的播放时间 = 总时长 * 播放进度
    _audioPlayer.currentTime = _audioPlayer.duration * progress;

}
- (IBAction)stop:(UIButton *)sender {

    // 判断播放器是否处于播放状态
    BOOL playing = _audioPlayer.playing;
    if (playing) {

        [_audioPlayer stop];//停止播放
    }
}

// 点击按钮,切换音频的播放状态
- (IBAction)playOrPause:(UIButton *)sender {

    //判断当前的播放状态
    BOOL isplay = _audioPlayer.playing;
    if (isplay) {

        [_audioPlayer pause];// 暂停

        [sender setTitle:@"play" forState:UIControlStateNormal];
    }else {

        //播放
        [_audioPlayer play];

        [sender setTitle:@"pause" forState:UIControlStateNormal];
    }
}

#pragma mark - AVAudioPlayerDelegate
// 播放结束
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {

    NSLog(@"player finished");
    NSLog(@"%f",_progress.value);

    _progress.value = 0.0f;
}

// 音频解码出错
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError * __nullable)error {

    NSLog(@"player decode error");
}

@end

由于AVAudioPlayer一次只能播放一个音频文件,所有上一曲、下一曲其实可以通过创建多个播放器对象来完成,这里暂不实现。播放进度的实现主要依靠一个定时器实时计算当前播放时长和音频总时长的比例,另外为了演示委托方法,上面的代码中也实现了播放完成委托方法,通常如果有下一曲功能的话播放完可以触发下一曲音乐播放。


注意:


  • AVAudioPlayer必须设置为全局变量,否则音频无法播放。
  • 播放音频是不能添加全局断点,否则会导致程序崩溃。
  • AVAudioPlayer的委托方法,通常如果有下一曲功能的话播放完可以触发下一曲音乐播放。当然由于AVAudioPlayer一次只能播放一个音频文件,所有上一曲、下一曲其实可以通过创建多个播放器对象来完成。



3. 网络音频播放


大家应该已经注意到了,使用AVAudioPlayer播放音频并不支持网络流媒体播放,但是对于音频播放来说网络流媒体播放有时候是很有必要的。AVAudioPlayer只能播放本地文件,并且是一次性加载所以音频数据,初始化AVAudioPlayer时指定的URL也只能是File URL而不能是HTTP URL。当然,将音频文件下载到本地然后再调用AVAudioPlayer来播放也是一种播放网络音频的办法,但是这种方式最大的弊端就是必须等到整个音频播放完成才能播放,而不能使用流式播放,这往往在实际开发中是不切实际的。


那么在iOS中如何播放网络流媒体呢?苹果公司提供了功能强大的AVPlayer用于播放网络音频(既流媒体),效果和可控性都比较好。


AVPlayer是一个全功能影音播放器,可以播放任何格式的音频或视频,支持当前流行的几乎所有的多媒体格式:
 支持视频格式: WMV,AVI,MKV,RMVB,RM,XVID,MP4,3GP,MPG …
 支持音频格式: MP3,WMA,RM,ACC,OGG,APE,FLAC,FLV…
 支持外部字幕: smi,srt,ass,sub,txt…

下面我们先来重点介绍下AVPlayer中网络音频的播放。


示例代码:


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

@interface ViewController ()
{
    NSArray *_playList;
    AVPlayer *_avPlayer;// 音频播放器
}
@property (weak, nonatomic) IBOutlet UISlider *volume;
@property (weak, nonatomic) IBOutlet UISlider *progress;
@property (weak, nonatomic) IBOutlet UIButton *play;
@property (strong, nonatomic) IBOutlet UIProgressView *progressView;

- (IBAction)volume:(UISlider *)sender;
- (IBAction)progress:(UISlider *)sender;
- (IBAction)playOrPause:(UIButton *)sender;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"%@",[NSHomeDirectory() stringByAppendingString:@"/Documents"]);

    // 1.加载网络音频网址
    NSString *str1 = @"http://www./filestores/2016/02/20/692de68936d75eea6da4a75f87f3ab2f.mp3";
    NSString *str2 = @"http://www./filestores/2014/08/20/93fde37c2aeae22c61a9c7b70b247f92.mp3";
    NSURL *url1 = [NSURL URLWithString:str1];
    NSURL *url2 = [NSURL URLWithString:str2];
    _playList = @[url1,url2];

    //使用AVPlayer同样可以播放本地的音频文件
//    NSURL *fileURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"PartOfMe" ofType:@"mp3"]];

    // 2.初始化AVPlayer音频播放器
    AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:_playList[0]];
    _avPlayer = [[AVPlayer alloc] initWithPlayerItem:item];

    // 3.播放音频
    [_avPlayer play];

    {// 常用属性设置

        _avPlayer.rate = 1;// 播放速度
        _avPlayer.volume = 0.8f;// 播放音量
        _avPlayer.muted = NO;// 静音

        // 播放完成的操作
        _avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndPause;
        /* AVPlayerActionAtItemEnd
         AVPlayerActionAtItemEndAdvance = 0,// 不要使用
         AVPlayerActionAtItemEndPause = 1,
         AVPlayerActionAtItemEndNone    = 2,
         */
    }

    {// 常用方法
        // 指定播放时间
        [_avPlayer seekToTime:CMTimeMakeWithSeconds(20, 24)];// 从指定时间的前一秒开始播放
    }

    // 监测播放状态,
    [_avPlayer addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];

    // 注册通知 AVPlayerItemDidPlayToEndTimeNotification:音频播放完成
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playDidEnd) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
    /* 音频播放通知属性名
     AVF_EXPORT NSString *const AVPlayerItemTimeJumpedNotification NS_AVAILABLE(10_7, 5_0); // 项目的当前时间间断地发生了变化
     AVF_EXPORT NSString *const AVPlayerItemDidPlayToEndTimeNotification      NS_AVAILABLE(10_7, 4_0);   // 项目中结束时间
     AVF_EXPORT NSString *const AVPlayerItemFailedToPlayToEndTimeNotification NS_AVAILABLE(10_7, 4_3);   // 项目未能发挥它的结束时间
     AVF_EXPORT NSString *const AVPlayerItemPlaybackStalledNotification       NSvh_AVAILABLE(10_9, 6_0);    // 媒体没有及时赶到继续播放
     AVF_EXPORT NSString *const AVPlayerItemNewAccessLogEntryNotification NS_AVAILABLE(10_9, 6_0); // 一个新的访问日志条目已被添加
     AVF_EXPORT NSString *const AVPlayerItemNewErrorLogEntryNotification NS_AVAILABLE(10_9, 6_0); // 添加了一个新的错误日志条目
     AVF_EXPORT NSString *const AVPlayerItemFailedToPlayToEndTimeErrorKey     NS_AVAILABLE(10_7, 4_3);   // 项目中结束时间发生错误
     */

    // 实时监听音频文件的播放进度
    __block ViewController *blockSelf = self;
    __block AVPlayer *blockPlayer = _avPlayer;
    [_avPlayer addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {

        // 周期性回调该block的代码块
        NSLog(@"播放状态:%ld",item.status);
        CGFloat seconds = time.value/time.timescale;
        NSLog(@"%f",seconds);

        /* CMTime : value/timescale = seconds.
         time指的就是時間(不是秒),
         而時間要換算成秒就要看第二個參數timeScale了.
         timeScale指的是1秒需要由幾個frame構成(可以視為fps,帧数),
         因此真正要表達的時間就會是 time / timeScale 才會是秒.
         */

        // 统计时长
        CMTime duration = blockPlayer.currentItem.duration;
        CGFloat durationSec = CMTimeGetSeconds(duration);
        NSLog(@"总时长:%f",durationSec);

        CMTime current = blockPlayer.currentItem.currentTime;
        CGFloat currentSec = CMTimeGetSeconds(current);
        NSLog(@"当前时间:%f",currentSec);

        //计算缓存时间
        NSTimeInterval timeInterval = [blockSelf availabelDuration];
        NSLog(@"time interval :%f",timeInterval);

        // 刷新播放进度
        [blockSelf.progress setValue:currentSec/durationSec animated:YES];

        // 刷新下载进度
        [blockSelf.progressView setProgress:timeInterval/durationSec animated:YES];

    }];

    // 限定时间监听,可以设置具体的回调时间
//    NSValue *value = [NSValue valueWithCMTime:CMTimeMake(10, 2)];
    NSValue *value = [NSValue valueWithCMTime:CMTimeMakeWithSeconds(5, 24)];
    [_avPlayer addBoundaryTimeObserverForTimes:@[value] queue:dispatch_get_main_queue() usingBlock:^{
        NSLog(@“第5秒。。。。。");

    }];

    {// UI设置
        self.volume.value = _avPlayer.volume;

        self.progress.value = 0;
        self.progressView.progress = 0;
        [self.play setTitle:@"pause" forState:UIControlStateNormal];
    }
}

// 播放结束后执行该方法
- (void)playDidEnd {

    NSLog(@"play finished");

    // 播放下一首
    AVPlayerItem *currentItem = [[AVPlayerItem alloc] initWithURL:_playList[1]];
    [_avPlayer replaceCurrentItemWithPlayerItem:currentItem];
    [_avPlayer play];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {

    if ([keyPath isEqualToString:@"status"]) {

        /* AVPlayerItemStatus
         AVPlayerItemStatusUnknown,
         AVPlayerItemStatusReadyToPlay,
         AVPlayerItemStatusFailed
         */
        if (_avPlayer.status == AVPlayerStatusReadyToPlay) {

            [self.play setTitle:@"pause" forState:UIControlStateNormal];
        }
    }
}
- (IBAction)volume:(UISlider *)sender {

    _avPlayer.volume = sender.value;
}
- (IBAction)progress:(UISlider *)sender {

    // 总时长
    float duration = _avPlayer.currentItem.duration.value/_avPlayer.currentItem.duration.timescale;
    float currentTime = duration * sender.value;
    [_avPlayer seekToTime:CMTimeMake(currentTime, 1)];

}
static bool playing = YES;
- (IBAction)playOrPause:(UIButton *)sender {

    if (playing) {
        [_avPlayer pause]; //暂停播放
        playing = NO;
        [sender setTitle:@"play" forState:UIControlStateNormal];
    }else {

        [_avPlayer play]; //暂停播放
        playing = YES;
        [sender setTitle:@"pause" forState:UIControlStateNormal];
    }
}

#pragma mark 计算网络音频文件缓冲
- (NSTimeInterval)availabelDuration {

    NSArray *loadedTimeRanges = [[_avPlayer currentItem] loadedTimeRanges];
    CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue];// 获取缓存区域
    /* typedef struct
      {
         CMTime start;
         CMTime duration;
      } CMTimeRange;
     */
    float startSeconds = CMTimeGetSeconds(timeRange.start);
    float durationSeconds = CMTimeGetSeconds(timeRange.duration);

    // 计算缓存事件

    NSTimeInterval result = startSeconds + durationSeconds;
    return result;
}

@end

事实上,AVPlayer一般用来完成视频播放,上面的代码稍作修改后可以用来完成视频的播放,具体的操作我们在后面讲到视频播放时再具体说明。




4. 音频队列


上面我们使用了AVPlayer完成网络音频的播放,除此之外,我们也可以使用AudioToolbox框架中的音频队列服务Audio Queue Services。
使用音频队列服务完全可以做到音频播放以及音频的录制,首先看一下录音音频服务队列:
这里写图片描述


 音频录制的音频服务队列Audio Queue有三部分组成:
 1. 三个缓冲器Buffers:每个缓冲器都是一个存储音频数据的临时仓库。
 2. 一个缓冲队列Buffer Queue:一个包含音频缓冲器的有序队列。
 3. 一个回调Callback:一个自定义的队列回调函数。

声音通过输入设备进入缓冲队列中,首先填充第一个缓冲器;当第一个缓冲器填充满之后自动填充下一个缓冲器,同时会调用回调函数;在回调函数中需要将缓冲器中的音频数据写入磁盘,同时将缓冲器放回到缓冲队列中以便重用。下面是Apple官方关于音频队列服务的流程示意图:
这里写图片描述


类似的,看一下音频播放缓冲队列,其组成部分和录音缓冲队列类似。
这里写图片描述


但是在音频播放缓冲队列中,回调函数调用的时机不同于音频录制缓冲队列,流程刚好相反。将音频读取到缓冲器中,一旦一个缓冲器填充满之后就放到缓冲队列中,然后继续填充其他缓冲器;当开始播放时,则从第一个缓冲器中读取音频进行播放;一旦播放完之后就会触发回调函数,开始播放下一个缓冲器中的音频,同时填充第一个缓冲器放;填充满之后再次放回到缓冲队列。下面是详细的流程:
这里写图片描述


当然,要明白音频队列服务的原理并不难,问题是如何实现这个自定义的回调函数,这其中我们有大量的工作要做,控制播放状态、处理异常中断、进行音频编码等等。由于牵扯内容过多,本文不对音频队列的使用做更多的介绍,目前有很多第三方优秀框架可以直接使用,例如AudioStreamer、FreeStreamer。由于前者当前只有非ARC版本,所以下面使用FreeStreamer来简单演示在线音频播放的过程,当然在使用之前要做如下准备工作:


  1. 拷贝FreeStreamer中的Reachability.h、Reachability.m和Common、astreamer两个文件夹中的内容到项目中。
  2. 添加FreeStreamer使用的类库:CFNetwork.framework、AudioToolbox.framework、AVFoundation.framework、libxml2.dylib、MediaPlayer.framework。
  3. 如果引用libxml2.dylib编译不通过,需要在Xcode的Targets-Build Settings-Header Search Path中添加$(SDKROOT)/usr/include/libxml2。
  4. 导入头文件 FSAudioStream.h

示例代码:


#import "ViewController.h"
#import "FSAudioStream.h"

@interface ViewController ()

@property (nonatomic,strong)FSAudioStream *audioStream;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];


    [self.audioStream play];
}
//加载本地音频
- (NSURL *)getFileURL {

    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"东京不太热" ofType:@"mp3"];

    NSURL *fileURL = [NSURL fileURLWithPath:filePath];

    return fileURL;
}
// 加载网络音频
- (NSURL *)getHttpURL {
//    NSString *str = @"http://www./filestores/2016/02/20/692de68936d75eea6da4a75f87f3ab2f.mp3";

    NSString *str = @"http://www./filestores/2014/08/20/93fde37c2aeae22c61a9c7b70b247f92.mp3";
    NSURL *url = [NSURL URLWithString:str];

    return url;
}

- (FSAudioStream *)audioStream{

    if (!_audioStream) {

//        NSURL *URL = [self getFileURL];
        NSURL *URL = [self getHttpURL];

        _audioStream = [[FSAudioStream alloc] initWithUrl:URL];

        //播放失败block
        _audioStream.onFailure = ^(FSAudioStreamError error, NSString *errorDescription) {

            NSLog(@"播放失败:error:%d \n str:%@",error,errorDescription);
        };

        //播放完成的回调block
        _audioStream.onCompletion = ^(){

            NSLog(@"已经播放完成");
        };
        [_audioStream setVolume:0.8];// 设置声音
    }
    return _audioStream;
}

@end

其实FreeStreamer的功能很强大,不仅仅是播放本地、网络音频那么简单,它还支持播放列表、检查包内容、RSS订阅、播放中断等很多强大的功能,甚至还包含了一个音频分析器,有兴趣的朋友可以访问官网查看详细用法




拓展:播放音乐库中的音乐


众所周知音乐是iOS的重要组成播放,无论是iPod、iTouch、iPhone还是iPad都可以在iTunes购买音乐或添加本地音乐到音乐库中同步到你的iOS设备。在MediaPlayer.frameowork中有一个MPMusicPlayerController用于播放音乐库中的音乐。


下面先来看一下MPMusicPlayerController的常用属性和方法:


属性说明
@property (nonatomic, readonly) MPMusicPlaybackState playbackState播放器状态,枚举类型: MPMusicPlaybackStateStopped:停止播放 MPMusicPlaybackStatePlaying:正在播放MPMusicPlaybackStatePaused:暂停播放MPMusicPlaybackStateInterrupted:播放中断MPMusicPlaybackStateSeekingForward:向前查找MPMusicPlaybackStateSeekingBackward:向后查找
@property (nonatomic) MPMusicRepeatMode repeatMode重复模式,枚举类型:MPMusicRepeatModeDefault:默认模式,使用用户的首选项(系统音乐程序设置)MPMusicRepeatModeNone:不重复MPMusicRepeatModeOne:单曲循环MPMusicRepeatModeAll:在当前列表内循环
@property (nonatomic) MPMusicShuffleMode shuffleMode随机播放模式,枚举类型:MPMusicShuffleModeDefault:默认模式,使用用户首选项(系统音乐程序设置)MPMusicShuffleModeOff:不随机播放MPMusicShuffleModeSongs:按歌曲随机播放MPMusicShuffleModeAlbums:按专辑随机播放
@property (nonatomic, copy) MPMediaItem *nowPlayingItem正在播放的音乐项
@property (nonatomic, readonly) NSUInteger indexOfNowPlayingItem当前正在播放的音乐在播放队列中的索引
@property(nonatomic, readonly) BOOL isPreparedToPlay是否准好播放准备
@property(nonatomic) NSTimeInterval currentPlaybackTime当前已播放时间,单位:秒
@property(nonatomic) float currentPlaybackRate当前播放速度,是一个播放速度倍率,0表示暂停播放,1代表正常速度
类方法说明
+(MPMusicPlayerController *)applicationMusicPlayer;获取应用播放器,注意此类播放器无法在后台播放
+(MPMusicPlayerController *)systemMusicPlayer获取系统播放器,支持后台播放
对象方法说明
-(void)setQueueWithQuery:(MPMediaQuery *)query使用媒体队列设置播放源媒体队列
-(void)setQueueWithItemCollection:(MPMediaItemCollection *)itemCollection使用媒体项集合设置播放源媒体队列
-(void)skipToNextItem下一曲
-(void)skipToBeginning从起始位置播放
-(void)skipToPreviousItem上一曲
-(void)beginGeneratingPlaybackNotifications开启播放通知,注意不同于其他播放器,MPMusicPlayerController要想获得通知必须首先开启,默认情况无法获得通知
-(void)endGeneratingPlaybackNotifications关闭播放通知
-(void)prepareToPlay做好播放准备(加载音频到缓冲区),在使用play方法播放时如果没有做好准备回自动调用该方法
-(void)play开始播放
-(void)pause暂停播放
-(void)stop停止播放
-(void)beginSeekingForward开始向前查找(快进)
-(void)beginSeekingBackward开始向后查找(快退)
-(void)endSeeking结束查找
通知说明(注意:要想获得MPMusicPlayerController通知必须首先调用beginGeneratingPlaybackNotifications开启通知)
MPMusicPlayerControllerPlaybackStateDidChangeNotification播放状态改变
MPMusicPlayerControllerNowPlayingItemDidChangeNotification当前播放音频改变
MPMusicPlayerControllerVolumeDidChangeNotification}声音大小改变
MPMediaPlaybackIsPreparedToPlayDidChangeNotification准备好播放

- MPMusicPlayerController有两种播放器:applicationMusicPlayer和systemMusicPlayer,前者在应用退出后音乐播放会自动停止,后者在应用停止后不会退出播放状态。
- MPMusicPlayerController加载音乐不同于前面的AVAudioPlayer是通过一个文件路径来加载,而是需要一个播放队列。在MPMusicPlayerController中提供了两个方法来加载播放队列:- (void)setQueueWithQuery:(MPMediaQuery )query和- (void)setQueueWithItemCollection:(MPMediaItemCollection )itemCollection,正是由于它的播放音频来源是一个队列,因此MPMusicPlayerController支持上一曲、下一曲等操作。


那么接下来的问题就是如何获取MPMediaQueue或者MPMediaItemCollection?MPMediaQueue对象有一系列的类方法来获得媒体队列:
+ (MPMediaQuery *)albumsQuery;
+ (MPMediaQuery *)artistsQuery;
+ (MPMediaQuery *)songsQuery;
+ (MPMediaQuery *)playlistsQuery;
+ (MPMediaQuery *)podcastsQuery;
+ (MPMediaQuery *)audiobooksQuery;
+ (MPMediaQuery *)compilationsQuery;
+ (MPMediaQuery *)composersQuery;
+ (MPMediaQuery *)genresQuery;


有了这些方法,就可以很容易获到歌曲、播放列表、专辑媒体等媒体队列了,这样就可以通过setQueueWithQuery:query方法设置音乐来源了。又或者得到MPMediaQueue之后创建MPMediaItemCollection,使用setQueueWithItemCollection:itemCollection设置音乐来源。


有时候可能希望用户自己来选择要播放的音乐,这时可以使用MPMediaPickerController,它是一个视图控制器,类似于UIImagePickerController,选择完播放来源后可以在其代理方法中获得MPMediaItemCollection对象。


无论是通过哪种方式获得MPMusicPlayerController的媒体源,可能都希望将每个媒体的信息显示出来,这时候可以通过MPMediaItem对象获得。一个MPMediaItem代表一个媒体文件,通过它可以访问媒体标题、专辑名称、专辑封面、音乐时长等等。无论是MPMediaQueue还是MPMediaItemCollection都有一个items属性,它是MPMediaItem数组,通过这个属性可以获得MPMediaItem对象。


下面就简单看一下MPMusicPlayerController的使用,在下面的例子中简单演示了音乐的选择、播放、暂停、通知、下一曲、上一曲功能,相信有了上面的概念,代码读起来并不复杂(示例中是直接通过MPMeidaPicker进行音乐选择的,但是仍然提供了两个方法getLocalMediaQuery和getLocalMediaItemCollection来演示如何直接通过MPMediaQueue获得媒体队列或媒体集合):


示例代码:


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

@interface ViewController ()<MPMediaPickerControllerDelegate>

@property (nonatomic,strong) MPMediaPickerController *mediaPicker; //媒体选择控制器
@property (nonatomic,strong) MPMusicPlayerController *musicPlayer; //音乐播放器

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    /*
     1. 集成 <MediaPlayer/MediaPlayer.h>的头文件
     2. 初始化媒体选择控制器和音乐播放器

     */

}

// 懒加载媒体选择器
- (MPMediaPickerController *)mediaPicker {

    //初始化媒体选择器,这里需要设置媒体类型为音乐,也可以选择视频、广播等
    if (!_mediaPicker) {

        _mediaPicker=[[MPMediaPickerController alloc]initWithMediaTypes:MPMediaTypeMusic];

//        _mediaPicker=[[MPMediaPickerController alloc]initWithMediaTypes:MPMediaTypeAny];
        _mediaPicker.allowsPickingMultipleItems=YES;//允许多选
//        _mediaPicker.showsCloudItems=YES;
        // 显示icloud选项
        _mediaPicker.prompt=@"请选择要播放的音乐";
        _mediaPicker.delegate=self;// 设置选择器代理

    }

    return _mediaPicker;
}

// 懒加载音乐播放器
- (MPMusicPlayerController *)musicPlayer {

    if (!_musicPlayer) {

        _musicPlayer = [MPMusicPlayerController systemMusicPlayer];// 在应用停止后不会退出播放状态。
        _musicPlayer = [MPMusicPlayerController applicationMusicPlayer];// 在应用退出后音乐播放会自动停止

        //开启通知,否则监控不到MPMusicPlayerController的通知
        [_musicPlayer beginGeneratingPlaybackNotifications];

        [self addNotification];//添加通知

        //如果不使用MPMediaPickerController可以使用如下方法获得音乐库媒体队列
        //[_musicPlayer setQueueWithItemCollection:[self getLocalMediaItemCollection]];
    }

    return _musicPlayer;

}

#pragma mark - 通知
// 添加通知
- (void)addNotification {

    // 播放装填改变的通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackStateChange:) name:MPMusicPlayerControllerPlaybackStateDidChangeNotification object:self.musicPlayer];
}

// 播放状态改变时调用该方法
- (void)playbackStateChange:(NSNotification *)notification {

    switch (self.musicPlayer.playbackState) {
        case MPMusicPlaybackStatePlaying:

            NSLog(@"正在播放...");
        break;
        case MPMusicPlaybackStatePaused:

            NSLog(@"播放暂停.");
        break;
        case MPMusicPlaybackStateStopped:

            NSLog(@"播放停止.");
        break;
        default:
        break;
    }
}
-(void)dealloc{

    [self.musicPlayer endGeneratingPlaybackNotifications];
}

// 取得媒体队列
- (MPMediaQuery *)getLocalMediaQuery {

    MPMediaQuery *mediaQueue=[MPMediaQuery songsQuery];
    for (MPMediaItem *item in mediaQueue.items) {
        NSLog(@"标题:%@,%@",item.title,item.albumTitle);
    }
    return mediaQueue;
}

// 获取媒体集合
- (MPMediaItemCollection *)getLocalMediaItemCollection {

    MPMediaQuery *mediaQueue = [MPMediaQuery songsQuery];
    NSMutableArray *items = [NSMutableArray array];

    for (MPMediaItem *item in mediaQueue.items) {

        [items addObject:item];
        NSLog(@"标题:%@,%@",item.title,item.albumTitle);
    }

    MPMediaItemCollection *mediaItemCollection=[[MPMediaItemCollection alloc]initWithItems:items];

    return mediaItemCollection;
}

#pragma mark - MPMediaPickerControllerDelegate
//选择完成
- (void)mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection {
    MPMediaItem *mediaItem=[mediaItemCollection.items firstObject];//第一个播放音乐
    //注意很多音乐信息如标题、专辑、表演者、封面、时长等信息都可以通过MPMediaItem的valueForKey:方法得到,但是从iOS7开始都有对应的属性可以直接访问
//    NSString *title= [mediaItem valueForKey:MPMediaItemPropertyAlbumTitle];
//    NSString *artist= [mediaItem valueForKey:MPMediaItemPropertyAlbumArtist];
//    MPMediaItemArtwork *artwork= [mediaItem valueForKey:MPMediaItemPropertyArtwork];
//    UIImage *image=[artwork imageWithSize:CGSizeMake(100, 100)];
    //专辑图片
    NSLog(@"标题:%@,表演者:%@,专辑:%@",mediaItem.title ,mediaItem.artist,mediaItem.albumTitle); [self.musicPlayer setQueueWithItemCollection:mediaItemCollection];

    [self dismissViewControllerAnimated:YES completion:nil];
}

//取消选择
- (void)mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker {
    [self dismissViewControllerAnimated:YES completion:nil];
}

#pragma mark - UI事件
- (IBAction)selectClick:(UIButton *)sender {

    [self presentViewController:self.mediaPicker animated:YES completion:nil];
}

- (IBAction)playClick:(UIButton *)sender {

    [self.musicPlayer play];
}

- (IBAction)puaseClick:(UIButton *)sender {

    [self.musicPlayer pause];
}

- (IBAction)stopClick:(UIButton *)sender {

    [self.musicPlayer stop];
}

- (IBAction)nextClick:(UIButton *)sender {

    [self.musicPlayer skipToNextItem];
}

- (IBAction)prevClick:(UIButton *)sender {

    [self.musicPlayer skipToPreviousItem];
}

@end



小结


系统提供的音频播放形式主要有:AudioToolbox、AVAudioPlayer、AVPlayer、音频队列对比他们的优缺点:


  • AudioToolbox:主要播放比较小且需要频繁播放的系统声音。
  • AVAudioPlayer:使用简单方便,但只能播放本地音频,不支持流媒体播放
  • AVPlayer:iOS4.0以后,可以使用AVPlayer播放本地音频和支持流媒体播放,但提供接口较少,处理音频不够灵活,当然AVPlayer主要是用来播放视频的。
  • 音频队列:主要处理流媒体播放,提供了强大且灵活的API接口(C函数的接口),但处理起来也较为复杂,需要自己去调用接口封装出需要的播放服务,我们介绍了几个开源框架来实现音频队列的服务。

               

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多