iOS 音視頻學(xué)習(xí)筆記

音頻會(huì)話

//
//  ViewController.m
//  KCAVAudioPlayer
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//  AVAudioSession 音頻會(huì)話

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
#define kMusicFile @"劉若英 - 原來(lái)你也在這里.mp3"
#define kMusicSinger @"劉若英"
#define kMusicTitle @"原來(lái)你也在這里"

@interface ViewController ()<AVAudioPlayerDelegate>

@property (nonatomic,strong) AVAudioPlayer *audioPlayer;//播放器
@property (weak, nonatomic) IBOutlet UILabel *controlPanel; //控制面板
@property (weak, nonatomic) IBOutlet UIProgressView *playProgress;//播放進(jìn)度
@property (weak, nonatomic) IBOutlet UILabel *musicSinger; //演唱者
@property (weak, nonatomic) IBOutlet UIButton *playOrPause; //播放/暫停按鈕(如果tag為0認(rèn)為是暫停狀態(tài)纫普,1是播放狀態(tài))

@property (weak ,nonatomic) NSTimer *timer;//進(jìn)度更新定時(shí)器

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self setupUI];
    
}

/**
 *  顯示當(dāng)面視圖控制器時(shí)注冊(cè)遠(yuǎn)程事件
 *
 *  @param animated 是否以動(dòng)畫(huà)的形式顯示
 */
-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    // 開(kāi)啟遠(yuǎn)程控制 chang
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
    // 作為第一響應(yīng)者
    //[self becomeFirstResponder];
}
/**
 *  當(dāng)前控制器視圖不顯示時(shí)取消遠(yuǎn)程控制
 *
 *  @param animated 是否以動(dòng)畫(huà)的形式消失
 */
-(void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
    // chang
    [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
    //[self resignFirstResponder];
}

/**
 *  初始化UI
 */
-(void)setupUI{
    self.title=kMusicTitle;
    self.musicSinger.text=kMusicSinger;
}

-(NSTimer *)timer{
    if (!_timer) {
    
    // chang 這里更新頻率為0.5s而不是1s
        _timer=[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateProgress) userInfo:nil repeats:true];
    }
    return _timer;
}

/**
 *  創(chuàng)建播放器
 *
 *  @return 音頻播放器
 */
-(AVAudioPlayer *)audioPlayer{
    if (!_audioPlayer) {
        NSString *urlStr=[[NSBundle mainBundle]pathForResource:kMusicFile ofType:nil];
        NSURL *url=[NSURL fileURLWithPath:urlStr];
        NSError *error=nil;
        //初始化播放器匿辩,注意這里的Url參數(shù)只能時(shí)文件路徑响禽,不支持HTTP Url
        _audioPlayer=[[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error];
        //設(shè)置播放器屬性
        _audioPlayer.numberOfLoops=0;//設(shè)置為0不循環(huán)
        _audioPlayer.delegate=self;
        
        // 注:重要 chang
        [_audioPlayer prepareToPlay];//加載音頻文件到緩存
        if(error){
            NSLog(@"初始化播放器過(guò)程發(fā)生錯(cuò)誤,錯(cuò)誤信息:%@",error.localizedDescription);
            return nil;
        }
        //設(shè)置后臺(tái)播放模式
        AVAudioSession *audioSession=[AVAudioSession sharedInstance];
        [audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
//        [audioSession setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:nil];
        [audioSession setActive:YES error:nil];
        // 添加通知胖笛,拔出耳機(jī)后暫停播放
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChange:) name:AVAudioSessionRouteChangeNotification object:nil];
    }
    return _audioPlayer;
}

/**
 *  播放音頻
 */
-(void)play{
    if (![self.audioPlayer isPlaying]) {
        [self.audioPlayer play];
        self.timer.fireDate=[NSDate distantPast];// 恢復(fù)定時(shí)器
    }
}

/**
 *  暫停播放
 */
-(void)pause{
    if ([self.audioPlayer isPlaying]) {
        [self.audioPlayer pause];
        self.timer.fireDate=[NSDate distantFuture];// 暫停定時(shí)器苍柏,注意不能調(diào)用invalidate方法肪康,此方法會(huì)取消荚恶,之后無(wú)法恢復(fù)
        
    }
}

/**
 *  點(diǎn)擊播放/暫停按鈕
 *
 *  @param sender 播放/暫停按鈕
 */
- (IBAction)playClick:(UIButton *)sender {
    if(sender.tag){
        sender.tag=0;
        [sender setImage:[UIImage imageNamed:@"playing_btn_play_n"] forState:UIControlStateNormal];
        [sender setImage:[UIImage imageNamed:@"playing_btn_play_h"] forState:UIControlStateHighlighted];
        [self pause];
    }else{
        sender.tag=1;
        [sender setImage:[UIImage imageNamed:@"playing_btn_pause_n"] forState:UIControlStateNormal];
        [sender setImage:[UIImage imageNamed:@"playing_btn_pause_h"] forState:UIControlStateHighlighted];
        [self play];
    }
}

/**
 *  更新播放進(jìn)度
 */
-(void)updateProgress{
    float progress= self.audioPlayer.currentTime /self.audioPlayer.duration;
    [self.playProgress setProgress:progress animated:true];
}

/**
 *  一旦輸出改變則執(zhí)行此方法
 *
 *  @param notification 輸出改變通知對(duì)象
 */
-(void)routeChange:(NSNotification *)notification{
    NSDictionary *dic=notification.userInfo;
    int changeReason= [dic[AVAudioSessionRouteChangeReasonKey] intValue];
    //等于AVAudioSessionRouteChangeReasonOldDeviceUnavailable表示舊輸出不可用
    if (changeReason==AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
        AVAudioSessionRouteDescription *routeDescription=dic[AVAudioSessionRouteChangePreviousRouteKey];
        AVAudioSessionPortDescription *portDescription= [routeDescription.outputs firstObject];
        //原設(shè)備為耳機(jī)則暫停
        if ([portDescription.portType isEqualToString:@"Headphones"]) {
            [self pause];
        }
    }
    
//    [dic enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
//        NSLog(@"%@:%@",key,obj);
//    }];
}

-(void)dealloc{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:AVAudioSessionRouteChangeNotification object:nil];
}

#pragma mark - 播放器代理方法
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{
    NSLog(@"音樂(lè)播放完成...");
    // 根據(jù)實(shí)際情況播放完成可以將會(huì)話關(guān)閉,其他音頻應(yīng)用繼續(xù)播放 chang
    [[AVAudioSession sharedInstance]setActive:NO error:nil];
}

@end

在上面的代碼中還實(shí)現(xiàn)了拔出耳機(jī)暫停音樂(lè)播放的功能磷支,這也是一個(gè)比較常見(jiàn)的功能谒撼。在iOS7及以后的版本中可以通過(guò)通知獲得輸出改變的通知,然后拿到通知對(duì)象后根據(jù)userInfo獲得是何種改變類型雾狈,進(jìn)而根據(jù)情況對(duì)音樂(lè)進(jìn)行暫停操作廓潜。

AVAudioRecorder 錄音

錄音機(jī)必須知道錄音文件的格式、采樣率、通道數(shù)辩蛋、每個(gè)采樣點(diǎn)的位數(shù)等信息呻畸,但是也并不是所有的信息都必須設(shè)置,通常只需要幾個(gè)常用設(shè)置悼院。關(guān)于錄音設(shè)置詳見(jiàn)幫助文檔中的“AV Foundation Audio Settings Constants”伤为。

//
//  ViewController.m
//  AVAudioRecorder
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
#define kRecordAudioFile @"myRecord.caf"

@interface ViewController ()<AVAudioRecorderDelegate>

@property (nonatomic,strong) AVAudioRecorder *audioRecorder;//音頻錄音機(jī)
@property (nonatomic,strong) AVAudioPlayer *audioPlayer;//音頻播放器,用于播放錄音文件
@property (nonatomic,strong) NSTimer *timer;//錄音聲波監(jiān)控(注意這里暫時(shí)不對(duì)播放進(jìn)行監(jiān)控)

@property (weak, nonatomic) IBOutlet UIButton *record;//開(kāi)始錄音
@property (weak, nonatomic) IBOutlet UIButton *pause;//暫停錄音
@property (weak, nonatomic) IBOutlet UIButton *resume;//恢復(fù)錄音
@property (weak, nonatomic) IBOutlet UIButton *stop;//停止錄音
@property (weak, nonatomic) IBOutlet UIProgressView *audioPower;//音頻波動(dòng)

@end

@implementation ViewController

#pragma mark - 控制器視圖方法
- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self setAudioSession];
}

#pragma mark - 私有方法
/**
 *  設(shè)置音頻會(huì)話
 */
-(void)setAudioSession{
    AVAudioSession *audioSession=[AVAudioSession sharedInstance];
    //設(shè)置為播放和錄音狀態(tài)据途,以便可以在錄制完之后播放錄音
    [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
    [audioSession setActive:YES error:nil];
}

/**
 *  取得錄音文件保存路徑
 *
 *  @return 錄音文件路徑
 */
-(NSURL *)getSavePath{
    NSString *urlStr=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    urlStr=[urlStr stringByAppendingPathComponent:kRecordAudioFile];
    NSLog(@"file path:%@",urlStr);
    NSURL *url=[NSURL fileURLWithPath:urlStr];
    return url;
}

/**
 *  取得錄音文件設(shè)置
 *
 *  @return 錄音設(shè)置
 */
-(NSDictionary *)getAudioSetting{
    NSMutableDictionary *dicM=[NSMutableDictionary dictionary];
    //設(shè)置錄音格式
    [dicM setObject:@(kAudioFormatLinearPCM) forKey:AVFormatIDKey];
    //設(shè)置錄音采樣率绞愚,8000是電話采樣率,對(duì)于一般錄音已經(jīng)夠了
    [dicM setObject:@(8000) forKey:AVSampleRateKey];
    //設(shè)置通道,這里采用單聲道
    [dicM setObject:@(1) forKey:AVNumberOfChannelsKey];
    //每個(gè)采樣點(diǎn)位數(shù),分為8昨凡、16爽醋、24、32
    [dicM setObject:@(8) forKey:AVLinearPCMBitDepthKey];
    //是否使用浮點(diǎn)數(shù)采樣
    [dicM setObject:@(YES) forKey:AVLinearPCMIsFloatKey];
    //....其他設(shè)置等
    return dicM;
}

/**
 *  獲得錄音機(jī)對(duì)象
 *
 *  @return 錄音機(jī)對(duì)象
 */
-(AVAudioRecorder *)audioRecorder{
    if (!_audioRecorder) {
        //創(chuàng)建錄音文件保存路徑
        NSURL *url=[self getSavePath];
        //創(chuàng)建錄音格式設(shè)置
        NSDictionary *setting=[self getAudioSetting];
        //創(chuàng)建錄音機(jī)
        NSError *error=nil;
        _audioRecorder=[[AVAudioRecorder alloc]initWithURL:url settings:setting error:&error];
        _audioRecorder.delegate=self;
        _audioRecorder.meteringEnabled=YES;//如果要監(jiān)控聲波則必須設(shè)置為YES
        if (error) {
            NSLog(@"創(chuàng)建錄音機(jī)對(duì)象時(shí)發(fā)生錯(cuò)誤便脊,錯(cuò)誤信息:%@",error.localizedDescription);
            return nil;
        }
    }
    return _audioRecorder;
}

/**
 *  創(chuàng)建播放器
 *
 *  @return 播放器
 */
-(AVAudioPlayer *)audioPlayer{
    if (!_audioPlayer) {
        NSURL *url=[self getSavePath];
        NSError *error=nil;
        _audioPlayer=[[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error];
        _audioPlayer.numberOfLoops=0;
        [_audioPlayer prepareToPlay];
        if (error) {
            NSLog(@"創(chuàng)建播放器過(guò)程中發(fā)生錯(cuò)誤蚂四,錯(cuò)誤信息:%@",error.localizedDescription);
            return nil;
        }
    }
    return _audioPlayer;
}

/**
 *  錄音聲波監(jiān)控定制器
 *
 *  @return 定時(shí)器
 */
-(NSTimer *)timer{
    if (!_timer) {
        _timer=[NSTimer scheduledTimerWithTimeInterval:0.1f target:self selector:@selector(audioPowerChange) userInfo:nil repeats:YES];
    }
    return _timer;
}

/**
 *  錄音聲波狀態(tài)設(shè)置
 */
-(void)audioPowerChange{
    [self.audioRecorder updateMeters];//更新測(cè)量值
    float power= [self.audioRecorder averagePowerForChannel:0];//取得第一個(gè)通道的音頻,注意音頻強(qiáng)度范圍時(shí)-160到0
    CGFloat progress=(1.0/160.0)*(power+160.0);
    [self.audioPower setProgress:progress];
}
#pragma mark - UI事件
/**
 *  點(diǎn)擊錄音按鈕
 *
 *  @param sender 錄音按鈕
 */
- (IBAction)recordClick:(UIButton *)sender {
    if (![self.audioRecorder isRecording]) {
        [self.audioRecorder record];//首次使用應(yīng)用時(shí)如果調(diào)用record方法會(huì)詢問(wèn)用戶是否允許使用麥克風(fēng)
        self.timer.fireDate=[NSDate distantPast];
    }
}

/**
 *  點(diǎn)擊暫定按鈕
 *
 *  @param sender 暫停按鈕
 */
- (IBAction)pauseClick:(UIButton *)sender {
    if ([self.audioRecorder isRecording]) {
        [self.audioRecorder pause];
        self.timer.fireDate=[NSDate distantFuture];
    }
}

/**
 *  點(diǎn)擊恢復(fù)按鈕
 *  恢復(fù)錄音只需要再次調(diào)用record哪痰,AVAudioSession會(huì)幫助你記錄上次錄音位置并追加錄音
 *
 *  @param sender 恢復(fù)按鈕
 */
- (IBAction)resumeClick:(UIButton *)sender {
    [self recordClick:sender];
}

/**
 *  點(diǎn)擊停止按鈕
 *
 *  @param sender 停止按鈕
 */
- (IBAction)stopClick:(UIButton *)sender {
    [self.audioRecorder stop];
    self.timer.fireDate=[NSDate distantFuture];
    self.audioPower.progress=0.0;
}

#pragma mark - 錄音機(jī)代理方法
/**
 *  錄音完成遂赠,錄音完成后播放錄音
 *
 *  @param recorder 錄音機(jī)對(duì)象
 *  @param flag     是否成功
 */
-(void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag{
    if (![self.audioPlayer isPlaying]) {
        [self.audioPlayer play];
    }
    NSLog(@"錄音完成!");
}

@end

音頻隊(duì)列服務(wù)

AVAudioPlayer只能播放本地文件,并且是一次性加載所以音頻數(shù)據(jù)晌杰,初始化AVAudioPlayer時(shí)指定的URL也只能是File URL而不能是HTTP URL跷睦。當(dāng)然,將音頻文件下載到本地然后再調(diào)用AVAudioPlayer來(lái)播放也是一種播放網(wǎng)絡(luò)音頻的辦法肋演,但是這種方式最大的弊端就是必須等到整個(gè)音頻播放完成才能播放抑诸,而不能使用流式播放,這往往在實(shí)際開(kāi)發(fā)中是不切實(shí)際的爹殊。那么在iOS中如何播放網(wǎng)絡(luò)流媒體呢蜕乡?就是使用AudioToolbox框架中的音頻隊(duì)列服務(wù)Audio Queue Services。

播放網(wǎng)絡(luò)音頻方法:

//
//  ViewController.m
//  AudioQueueServices
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//  使用FreeStreamer實(shí)現(xiàn)網(wǎng)絡(luò)音頻播放

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

@interface ViewController ()

@property (nonatomic,strong) FSAudioStream *audioStream;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self.audioStream play];
}

/**
 *  取得本地文件路徑
 *
 *  @return 文件路徑
 */
-(NSURL *)getFileUrl{
    NSString *urlStr=[[NSBundle mainBundle]pathForResource:@"劉若英 - 原來(lái)你也在這里.mp3" ofType:nil];
    NSURL *url=[NSURL fileURLWithPath:urlStr];
    return url;
}
-(NSURL *)getNetworkUrl{
    NSString *urlStr=@"http://192.168.1.102/liu.mp3";
    NSURL *url=[NSURL URLWithString:urlStr];
    return url;
}

/**
 *  創(chuàng)建FSAudioStream對(duì)象
 *
 *  @return FSAudioStream對(duì)象
 */
-(FSAudioStream *)audioStream{
    if (!_audioStream) {
        NSURL *url=[self getNetworkUrl];
        //創(chuàng)建FSAudioStream對(duì)象
        _audioStream=[[FSAudioStream alloc]initWithUrl:url];
        _audioStream.onFailure=^(FSAudioStreamError error,NSString *description){
            NSLog(@"播放過(guò)程中發(fā)生錯(cuò)誤梗夸,錯(cuò)誤信息:%@",description);
        };
        _audioStream.onCompletion=^(){
            NSLog(@"播放完成!");
        };
        [_audioStream setVolume:0.5];//設(shè)置聲音
    }
    return _audioStream;
}

@end

擴(kuò)展--使用AVFoundation生成縮略圖

使用MPMoviePlayerController來(lái)生成縮略圖足夠簡(jiǎn)單层玲,但是如果僅僅是是為了生成縮略圖而不進(jìn)行視頻播放的話,此刻使用MPMoviePlayerController就有點(diǎn)大材小用了反症。其實(shí)使用AVFundation框架中的AVAssetImageGenerator就可以獲取視頻縮略圖辛块。使用AVAssetImageGenerator獲取縮略圖大致分為三個(gè)步驟:

創(chuàng)建AVURLAsset對(duì)象(此類主要用于獲取媒體信息,包括視頻铅碍、聲音等)润绵。
根據(jù)AVURLAsset創(chuàng)建AVAssetImageGenerator對(duì)象。
使用AVAssetImageGenerator的copyCGImageAtTime::方法獲得指定時(shí)間點(diǎn)的截圖胞谈。

//
//  ViewController.m
//  AVAssetImageGenerator
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

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

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //獲取第13.0s的縮略圖
    [self thumbnailImageRequest:13.0];
}

#pragma mark - 私有方法
/**
 *  取得本地文件路徑
 *
 *  @return 文件路徑
 */
-(NSURL *)getFileUrl{
    NSString *urlStr=[[NSBundle mainBundle] pathForResource:@"The New Look of OS X Yosemite.mp4" ofType:nil];
    NSURL *url=[NSURL fileURLWithPath:urlStr];
    return url;
}

/**
 *  取得網(wǎng)絡(luò)文件路徑
 *
 *  @return 文件路徑
 */
-(NSURL *)getNetworkUrl{
    NSString *urlStr=@"http://192.168.1.161/The New Look of OS X Yosemite.mp4";
    urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSURL *url=[NSURL URLWithString:urlStr];
    return url;
}

/**
 *  截取指定時(shí)間的視頻縮略圖
 *
 *  @param timeBySecond 時(shí)間點(diǎn)
 */
-(void)thumbnailImageRequest:(CGFloat )timeBySecond{
    //創(chuàng)建URL
    NSURL *url=[self getNetworkUrl];
    //根據(jù)url創(chuàng)建AVURLAsset
    AVURLAsset *urlAsset=[AVURLAsset assetWithURL:url];
    //根據(jù)AVURLAsset創(chuàng)建AVAssetImageGenerator
    AVAssetImageGenerator *imageGenerator=[AVAssetImageGenerator assetImageGeneratorWithAsset:urlAsset];
    /*截圖
     * requestTime:縮略圖創(chuàng)建時(shí)間
     * actualTime:縮略圖實(shí)際生成的時(shí)間
     */
    NSError *error=nil;
    CMTime time=CMTimeMakeWithSeconds(timeBySecond, 10);//CMTime是表示電影時(shí)間信息的結(jié)構(gòu)體授药,第一個(gè)參數(shù)表示是視頻第幾秒士嚎,第二個(gè)參數(shù)表示每秒幀數(shù).(如果要活的某一秒的第幾幀可以使用CMTimeMake方法)
    CMTime actualTime;
    CGImageRef cgImage= [imageGenerator copyCGImageAtTime:time actualTime:&actualTime error:&error];
    if(error){
        NSLog(@"截取視頻縮略圖時(shí)發(fā)生錯(cuò)誤,錯(cuò)誤信息:%@",error.localizedDescription);
        return;
    }
    CMTimeShow(actualTime);
    UIImage *image=[UIImage imageWithCGImage:cgImage];//轉(zhuǎn)化為UIImage
    //保存到相冊(cè)
    UIImageWriteToSavedPhotosAlbum(image,nil, nil, nil);
    CGImageRelease(cgImage);
}

@end

AVPlayer

MPMoviePlayerController足夠強(qiáng)大悔叽,幾乎不用寫(xiě)幾行代碼就能完成一個(gè)播放器莱衩,但是正是由于它的高度封裝使得要自定義這個(gè)播放器變得很復(fù)雜,甚至是不可能完成娇澎。例如有些時(shí)候需要自定義播放器的樣式笨蚁,那么如果要使用MPMoviePlayerController就不合適了,如果要對(duì)視頻有自由的控制則可以使用AVPlayer趟庄。AVPlayer存在于AVFoundation中括细,它更加接近于底層,所以靈活性也更強(qiáng):

AVPlayer本身并不能顯示視頻戚啥,而且它也不像MPMoviePlayerController有一個(gè)view屬性奋单。如果AVPlayer要顯示必須創(chuàng)建一個(gè)播放器層AVPlayerLayer用于展示,播放器層繼承于CALayer猫十,有了AVPlayerLayer之后添加到控制器視圖的layer中即可览濒。要使用 AVPlayer 首先了解一下幾個(gè)常用的類:

AVAsset:主要用于獲取多媒體信息,是一個(gè)抽象類拖云,不能直接使用贷笛。

AVURLAsset:AVAsset的子類,可以根據(jù)一個(gè)URL路徑創(chuàng)建一個(gè)包含媒體信息的AVURLAsset對(duì)象宙项。

AVPlayerItem:一個(gè)媒體資源管理對(duì)象乏苦,管理者視頻的一些基本信息和狀態(tài),一個(gè)AVPlayerItem對(duì)應(yīng)著一個(gè)視頻資源尤筐。

AVPlayer 沒(méi)有播放狀態(tài)屬性汇荐,通常情況下可以通過(guò)判斷播放器的播放速度來(lái)獲得播放狀態(tài)。如果rate為0說(shuō)明是停止?fàn)顟B(tài)盆繁,1是則是正常播放狀態(tài)掀淘。

播放視頻時(shí),特別是播放網(wǎng)絡(luò)視頻往往需要知道視頻加載情況改基、緩沖情況繁疤、播放情況咖为,這些信息可以通過(guò)KVO監(jiān)控AVPlayerItem的status秕狰、loadedTimeRanges屬性來(lái)獲得。當(dāng) AVPlayerItem 的 status 屬性為AVPlayerStatusReadyToPlay是說(shuō)明正在播放躁染,只有處于這個(gè)狀態(tài)時(shí)才能獲得視頻時(shí)長(zhǎng)等信息鸣哀;當(dāng)loadedTimeRanges的改變時(shí)(每緩沖一部分?jǐn)?shù)據(jù)就會(huì)更新此屬性)可以獲得本次緩沖加載的視頻范圍(包含起始時(shí)間、本次加載時(shí)長(zhǎng))吞彤,這樣一來(lái)就可以實(shí)時(shí)獲得緩沖情況我衬。然后就是依靠AVPlayer的- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block方法獲得播放進(jìn)度叹放,這個(gè)方法會(huì)在設(shè)定的時(shí)間間隔內(nèi)定時(shí)更新播放進(jìn)度,通過(guò)time參數(shù)通知客戶端挠羔。

存在問(wèn)題
無(wú)論是MPMoviePlayerController還是AVPlayer來(lái)播放視頻都相當(dāng)強(qiáng)大井仰,但是它也存在著一些不可回避的問(wèn)題,那就是支持的視頻編碼格式很有限:H.264破加、MPEG-4,擴(kuò)展名(壓縮格式):.mp4、.mov冠跷、.m4v衫樊、.m2v、.3gp锭环、.3g2等聪全。但是無(wú)論是MPMoviePlayerController還是AVPlayer它們都支持絕大多數(shù)音頻編碼,所以大家如果純粹是為了播放音樂(lè)的話也可以考慮使用這兩個(gè)播放器辅辩。那么如何支持更多視頻編碼格式呢难礼?目前來(lái)說(shuō)主要還是依靠第三方框架,在iOS上常用的視頻編碼汽久、解碼框架有:VLC鹤竭、ffmpeg

基礎(chǔ)

目前我們?cè)谟?jì)算機(jī)上進(jìn)行音頻播放都需要依賴于音頻文件,音頻文件的生成過(guò)程是將聲音信息采樣景醇、量化和編碼產(chǎn)生的數(shù)字信號(hào)的過(guò)程臀稚,人耳所能聽(tīng)到的聲音,最低的頻率是從20Hz起一直到最高頻率20KHZ三痰,因此音頻文件格式的最大帶寬是20KHZ吧寺。根據(jù)奈奎斯特的理論,只有采樣頻率高于聲音信號(hào)最高頻率的兩倍時(shí)散劫,才能把數(shù)字信號(hào)表示的聲音還原成為原來(lái)的聲音稚机,所以音頻文件的采樣率一般在40~50KHZ,比如最常見(jiàn)的CD音質(zhì)采樣率44.1KHZ获搏。

對(duì)聲音進(jìn)行采樣赖条、量化過(guò)程被稱為脈沖編碼調(diào)制(Pulse Code Modulation),簡(jiǎn)稱PCM常熙。PCM數(shù)據(jù)是最原始的音頻數(shù)據(jù)完全無(wú)損纬乍,所以PCM數(shù)據(jù)雖然音質(zhì)優(yōu)秀但體積龐大,為了解決這個(gè)問(wèn)題先后誕生了一系列的音頻格式裸卫,這些音頻格式運(yùn)用不同的方法對(duì)音頻數(shù)據(jù)進(jìn)行壓縮仿贬,其中有無(wú)損壓縮(ALAC、APE墓贿、FLAC)和有損壓縮(MP3茧泪、AAC蜓氨、OGG、WMA)兩種队伟。

MP3格式中的碼率(BitRate)代表了MP3數(shù)據(jù)的壓縮質(zhì)量穴吹,現(xiàn)在常用的碼率有128kbit/s、160kbit/s嗜侮、320kbit/s等等刀荒,這個(gè)值越高聲音質(zhì)量也就越高。MP3編碼方式常用的有兩種固定碼率(Constant bitrate棘钞,CBR)和可變碼率(Variable bitrate缠借,VBR)。
MP3格式中的數(shù)據(jù)通常由兩部分組成宜猜,一部分為ID3用來(lái)存儲(chǔ)歌名泼返、演唱者、專輯姨拥、音軌數(shù)等信息,另一部分為音頻數(shù)據(jù)叫乌。音頻數(shù)據(jù)部分以幀(frame)為單位存儲(chǔ),每個(gè)音頻都有自己的幀頭革屠,如圖所示就是一個(gè)MP3文件幀結(jié)構(gòu)圖(圖片同樣來(lái)自互聯(lián)網(wǎng))排宰。MP3中的每一個(gè)幀都有自己的幀頭,其中存儲(chǔ)了采樣率等解碼必須的信息板甘,所以每一個(gè)幀都可以獨(dú)立于文件存在和播放,這個(gè)特性加上高壓縮比使得MP3文件成為了音頻流播放的主流格式盐类。幀頭之后存儲(chǔ)著音頻數(shù)據(jù),這些音頻數(shù)據(jù)是若干個(gè)PCM數(shù)據(jù)幀經(jīng)過(guò)壓縮算法壓縮得到的枪萄,對(duì)CBR的MP3數(shù)據(jù)來(lái)說(shuō)每個(gè)幀中包含的PCM數(shù)據(jù)幀是固定的,而VBR是可變的硬毕。

iOS音頻播放概述

了解了基礎(chǔ)概念之后我們就可以列出一個(gè)經(jīng)典的音頻播放流程(以MP3為例):

  1. 讀取MP3文件
  2. 解析采樣率呻引、碼率礼仗、時(shí)長(zhǎng)等信息吐咳,分離MP3中的音頻幀
  3. 對(duì)分離出來(lái)的音頻幀解碼得到PCM數(shù)據(jù)
  4. 對(duì)PCM數(shù)據(jù)進(jìn)行音效處理(均衡器逻悠、混響器等,非必須)
  5. 把PCM數(shù)據(jù)解碼成音頻信號(hào)
  6. 把音頻信號(hào)交給硬件播放
  7. 重復(fù)1-6步直到播放完成

CoreAudio的接口層次

CoreAudio的接口層次

下面對(duì)其中的中高層接口進(jìn)行功能說(shuō)明:

  • Audio File Services:讀寫(xiě)音頻數(shù)據(jù)韭脊,可以完成播放流程中的第2步童谒;
  • Audio File Stream Services:對(duì)音頻進(jìn)行解碼,可以完成播放流程中的第2步沪羔;
  • Audio Converter services:音頻數(shù)據(jù)轉(zhuǎn)換饥伊,可以完成播放流程中的第3步;
  • Audio Processing Graph Services:音效處理模塊蔫饰,可以完成播放流程中的第4步琅豆;
  • Audio Unit Services:播放音頻數(shù)據(jù):可以完成播放流程中的第5步、第6步篓吁;
  • Extended Audio File Services:Audio File Services和Audio Converter services的結(jié)合體茫因;
  • AVAudioPlayer/AVPlayer(AVFoundation):高級(jí)接口,可以完成整個(gè)音頻播放的過(guò)程(包括本地文件和網(wǎng)絡(luò)流播放杖剪,第4步除外)冻押;
  • Audio Queue Services:高級(jí)接口,可以進(jìn)行錄音和播放盛嘿,可以完成播放流程中的第3洛巢、5、6步次兆;
  • OpenAL:用于游戲音頻播放

使用場(chǎng)景

  • 如果你只是想實(shí)現(xiàn)音頻的播放稿茉,沒(méi)有其他需求AVFoundation會(huì)很好的滿足你的需求。它的接口使用簡(jiǎn)單芥炭、不用關(guān)心其中的細(xì)節(jié)狈邑;

  • 如果你的app需要對(duì)音頻進(jìn)行流播放并且同時(shí)存儲(chǔ),那么AudioFileStreamer加AudioQueue能夠幫到你砰琢,你可以先把音頻數(shù)據(jù)下載到本地陪汽,一邊下載一邊用NSFileHandler等接口讀取本地音頻文件并交給AudioFileStreamer或者AudioFile解析分離音頻幀况增,分離出來(lái)的音頻幀可以送給AudioQueue進(jìn)行解碼和播放澳骤。如果是本地文件直接讀取文件解析即可。(這兩個(gè)都是比較直接的做法摊册,這類需求也可以用AVFoundation+本地server的方式實(shí)現(xiàn)茅特,AVAudioPlayer會(huì)把請(qǐng)求發(fā)送給本地server白修,由本地server轉(zhuǎn)發(fā)出去熬荆,獲取數(shù)據(jù)后在本地server中存儲(chǔ)并轉(zhuǎn)送給AVAudioPlayer卤恳。另一個(gè)比較trick的做法是先把音頻下載到文件中突琳,在下載到一定量的數(shù)據(jù)后把文件路徑給AVAudioPlayer播放,當(dāng)然這種做法在音頻seek后就回有問(wèn)題了啊终。)蓝牲;

  • 如果你正在開(kāi)發(fā)一個(gè)專業(yè)的音樂(lè)播放軟件昔期,需要對(duì)音頻施加音效(均衡器硼一、混響器)般贼,那么除了數(shù)據(jù)的讀取和解析以外還需要用到AudioConverter來(lái)把音頻數(shù)據(jù)轉(zhuǎn)換成PCM數(shù)據(jù)哼蛆,再由AudioUnit+AUGraph來(lái)進(jìn)行音效處理和播放(但目前多數(shù)帶音效的app都是自己開(kāi)發(fā)音效模塊來(lái)坐PCM數(shù)據(jù)的處理人芽,這部分功能自行開(kāi)發(fā)在自定義性和擴(kuò)展性上會(huì)比較強(qiáng)一些萤厅。PCM數(shù)據(jù)通過(guò)音效器處理完成后就可以使用AudioUnit播放了惕味,當(dāng)然AudioQueue也支持直接使對(duì)PCM數(shù)據(jù)進(jìn)行播放。)

AudioFile + AudioConverter + AudioUnit進(jìn)行音頻播放的流程.png

音頻播放的實(shí)現(xiàn)級(jí)別:

(1) 離線播放:這里并不是指應(yīng)用不聯(lián)網(wǎng)禀倔,而是指播放本地音頻文件救湖,包括先下完完成音頻文件再進(jìn)行播放的情況鞋既,這種使用AVFoundation里的AVAudioPlayer可以滿足
(2) 在線播放:使用AVFoundation的AVPlayer可以滿足
(3) 在線播放同時(shí)存儲(chǔ)文件:使用
AudioFileStreamer + AudioQueue 可以滿足
(4) 在線播放且?guī)в幸粜幚恚菏褂?br> AudioFileStreamer + AudioQueue + 音效模塊(系統(tǒng)自帶或者
自行開(kāi)發(fā))來(lái)滿足

功能需求

通常音樂(lè)播放并展示到界面上需要我們實(shí)現(xiàn)的功能如下:
1邑闺、(核心)播放器通過(guò)一個(gè)網(wǎng)絡(luò)鏈接播放音樂(lè)
2陡舅、(基本)播放器的常用操作:暫停伴挚、播放章鲤、上一首败徊、下一首等等
3、(基本)監(jiān)聽(tīng)該音樂(lè)的播放進(jìn)度眷蜈、獲取音樂(lè)的總時(shí)間酌儒、當(dāng)前播放時(shí)間
4忌怎、(基本)監(jiān)聽(tīng)改播放器狀態(tài):
?????(1)媒體加載狀態(tài)
?????(2)數(shù)據(jù)緩沖狀態(tài)
?????(3)播放完畢狀態(tài)
5榴啸、(可選)Remote Control控制音樂(lè)的播放
6鸥印、(可選)Now Playing Center展示正在播放的音樂(lè)

監(jiān)聽(tīng)改播放器狀態(tài)

[songItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];

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

    if ([keyPath isEqualToString:@"status"]) {
        switch (self.player.status) {            
            case AVPlayerStatusUnknown:                
                BASE_INFO_FUN(@"KVO:未知狀態(tài)库说,此時(shí)不能播放");                
                break;            
            case AVPlayerStatusReadyToPlay:                
                self.status = SUPlayStatusReadyToPlay;                    
                BASE_INFO_FUN(@"KVO:準(zhǔn)備完畢,可以播放");                
                break;
            case AVPlayerStatusFailed:
                BASE_INFO_FUN(@"KVO:加載失敗构回,網(wǎng)絡(luò)或者服務(wù)器出現(xiàn)問(wèn)題");
                break;            
            default:                
                break;        
        }
    }
}

播放完后移除觀察者:[songItem removeObserver:self forKeyPath:@"status"];

Remote Control控制音樂(lè)的播放

Remote Control可以讓你在不打開(kāi)APP的情況下控制其播放纤掸,最常見(jiàn)的出現(xiàn)于鎖屏界面政己、從屏幕底部上拉和耳機(jī)線控三種掏愁,可以達(dá)到增強(qiáng)用戶體驗(yàn)的作用果港。

我們?cè)贏ppDelegate里去設(shè)置Remote Control:
(1)聲明接收Remote Control事件

[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];

(2)重寫(xiě)方法谢谦,成為第一響應(yīng)者

- (BOOL)canBecomeFirstResponder {    
    return YES;
}

(3)對(duì)事件進(jìn)行處理

- (void)remoteControlReceivedWithEvent:(UIEvent *)event {       
    switch (event.subtype)    {        
        case UIEventSubtypeRemoteControlPlay:
            [self.player startPlay];
            BASE_INFO_FUN(@“remote_播放");
            break;        
        case UIEventSubtypeRemoteControlPause:            
            [self.player pausePlay];
            BASE_INFO_FUN(@"remote_暫停");
            break;        
        case UIEventSubtypeRemoteControlNextTrack:
            [self.player playNextSong];
            BASE_INFO_FUN(@"remote_下一首");
            break;        
        case UIEventSubtypeRemoteControlTogglePlayPause:            
            self.player.isPlaying ? [self.player pausePlay] : [self.player startPlay];           
            BASE_INFO_FUN(@“remote_耳機(jī)的播放/暫停");
            break;        
        default:            
            break;    }
}

Now Playing Center

Now Playing Center可以在鎖屏界面展示音樂(lè)的信息回挽,也達(dá)到增強(qiáng)用戶體驗(yàn)的作用千劈。

- (void)configNowPlayingCenter {    
    BASE_INFO_FUN(@"配置NowPlayingCenter");
    NSMutableDictionary * info = [NSMutableDictionary dictionary];
    // 音樂(lè)的標(biāo)題
    [info setObject:_player.currentSong.title forKey:MPMediaItemPropertyTitle];
     // 音樂(lè)的藝術(shù)家
    [info setObject:_player.currentSong.artist forKey:MPMediaItemPropertyArtist];
     // 音樂(lè)的播放時(shí)間
    [info setObject:@(self.player.playTime.intValue) forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
     // 音樂(lè)的播放速度
    [info setObject:@(1) forKey:MPNowPlayingInfoPropertyPlaybackRate];
     // 音樂(lè)的總時(shí)間
    [info setObject:@(self.player.playDuration.intValue) forKey:MPMediaItemPropertyPlaybackDuration];
     // 音樂(lè)的封面
    MPMediaItemArtwork * artwork = [[MPMediaItemArtwork alloc] initWithImage:_player.coverImg];
    [info setObject:artwork forKey:MPMediaItemPropertyArtwork];
     // 完成設(shè)置
    [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:info];
}

Now Playing Center并不需要每一秒都去刷新(設(shè)置)涡驮,它是根據(jù)你設(shè)置的PlaybackRate來(lái)計(jì)算進(jìn)度條展示的進(jìn)度遮怜,比如你PlaybackRate傳1,那就是1秒刷新一次進(jìn)度顯示即碗,當(dāng)然暫停播放的時(shí)候它也會(huì)自動(dòng)暫停剥懒。

那什么時(shí)候設(shè)置Now Playing Center比較合適呢初橘?對(duì)于播放網(wǎng)絡(luò)音樂(lè)來(lái)說(shuō),需要刷新的有幾個(gè)時(shí)間點(diǎn):當(dāng)前播放的歌曲變化時(shí)(如切換到下一首)耕蝉、當(dāng)前歌曲信息變化時(shí)(如從Unknown到ReadyToPlay)垒在、當(dāng)前歌曲拖動(dòng)進(jìn)度時(shí)场躯。

如果有讀者是使用百度音樂(lè)聽(tīng)歌的話踢关,會(huì)發(fā)現(xiàn)其帶有鎖屏歌詞耘成,其實(shí)它是采用“將歌詞和封面合成新的圖片設(shè)置為Now Playing Center的封面 + 歌詞躍進(jìn)時(shí)刷新Now Playing Center”來(lái)實(shí)現(xiàn)的撒会。

幾種技術(shù)優(yōu)缺點(diǎn)對(duì)比

參考:

  1. iOS開(kāi)發(fā)系列--音頻播放诵肛、錄音、視頻播放薛训、拍照仑氛、視頻錄制
  2. http://msching.github.io/blog/2014/07/07/audio-in-ios/
  3. iOS音頻篇:使用AVPlayer播放網(wǎng)絡(luò)音樂(lè)
  4. iOS音頻篇:使用AVPlayer播放網(wǎng)絡(luò)音樂(lè)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市遇伞,隨后出現(xiàn)的幾起案子捶牢,更是在濱河造成了極大的恐慌渐排,老刑警劉巖飞盆,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吓歇,死亡現(xiàn)場(chǎng)離奇詭異城看,居然都是意外死亡测柠,警方通過(guò)查閱死者的電腦和手機(jī)谒主,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門霎肯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人驮俗,你說(shuō)我怎么就攤上這事王凑』绯纾” “怎么了术荤?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)子库。 經(jīng)常有香客問(wèn)我,道長(zhǎng)仓技,這世上最難降的妖魔是什么脖捻? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任地沮,我火速辦了婚禮嗜浮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘摩疑。我一直安慰自己危融,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布雷袋。 她就那樣靜靜地躺著专挪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪片排。 梳的紋絲不亂的頭發(fā)上寨腔,一...
    開(kāi)封第一講書(shū)人閱讀 51,624評(píng)論 1 305
  • 那天捅僵,我揣著相機(jī)與錄音上荡,去河邊找鬼逛薇。 笑死卧秘,一個(gè)胖子當(dāng)著我的面吹牛哼御,可吹牛的內(nèi)容都是我干的赶促。 我是一名探鬼主播婿滓,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起坊罢,我...
    開(kāi)封第一講書(shū)人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤询兴,失蹤者是張志新(化名)和其女友劉穎眶根,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體怒竿,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年领舰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了锉桑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡因苹,死狀恐怖蘸秘,靈堂內(nèi)的尸體忽然破棺而出毛秘,到底是詐尸還是另有隱情抹恳,我是刑警寧澤窃这,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布畔裕,位于F島的核電站,受9級(jí)特大地震影響铜邮,放射性物質(zhì)發(fā)生泄漏评姨。R本人自食惡果不足惜文虏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望膳算。 院中可真熱鬧,春花似錦诸尽、人聲如沸您机。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至录淡,卻和暖如春刨裆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背彬檀。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工帆啃, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人窍帝。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓努潘,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親坤学。 傳聞我的和親對(duì)象是個(gè)殘疾皇子疯坤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容