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