隨著移動網際網路的發展,如今的手機早已不是打電話、發簡訊那麼簡單了,播放音樂、視訊、錄音、拍照等都是很常用的功能。在iOS中對於多媒體的支援是非常強大的,無論是音視訊播放、錄製,還是對麥克風、攝像頭的操作都提供了多套API。在今天的文章中將會對這些內容進行一一介紹:
音訊
在iOS中音訊播放從形式上可以分為音效播放和音樂播放。前者主要指的是一些短音訊播放,通常作為點綴音訊,對於這類音訊不需要進行進度、迴圈等控制。後者指的是一些較長的音訊,通常是主音訊,對於這些音訊的播放通常需要進行精確的控制。在iOS中播放兩類音訊分別使用AudioToolbox.framework和AVFoundation.framework來完成音效和音樂播放。
音效
AudioToolbox.framework是一套基於C語言的框架,使用它來播放音效其本質是將短音訊註冊到系統聲音服務(System Sound Service)。System Sound Service是一種簡單、底層的聲音播放服務,但是它本身也存在著一些限制:
- 音訊播放時間不能超過30s
- 資料必須是PCM或者IMA4格式
- 音訊檔案必須打包成.caf、.aif、.wav中的一種(注意這是官方文件的說法,實際測試發現一些.mp3也可以播放)
使用System Sound Service 播放音效的步驟如下:
- 呼叫AudioServicesCreateSystemSoundID( CFURLRef inFileURL, SystemSoundID* outSystemSoundID)函式獲得系統聲音ID。
- 如果需要監聽播放完成操作,則使用AudioServicesAddSystemSoundCompletion( SystemSoundID inSystemSoundID,
CFRunLoopRef inRunLoop, CFStringRef inRunLoopMode, AudioServicesSystemSoundCompletionProc inCompletionRoutine, void* inClientData)方法註冊回撥函式。 - 呼叫AudioServicesPlaySystemSound(SystemSoundID inSystemSoundID) 或者AudioServicesPlayAlertSound(SystemSoundID inSystemSoundID) 方法播放音效(後者帶有震動效果)。
下面是一個簡單的示例程式:
// // KCMainViewController.m // Audio // // Created by Kenshin Cui on 14/03/30. // Copyright (c) 2014年 cmjstudio. All rights reserved. // 音效播放 #import "KCMainViewController.h" #import <AudioToolbox/AudioToolbox.h> @interface KCMainViewController () @end @implementation KCMainViewController - (void)viewDidLoad { [super viewDidLoad]; [self playSoundEffect:@"videoRing.caf"]; } /** * 播放完成回撥函式 * * @param soundID 系統聲音ID * @param clientData 回撥時傳遞的資料 */ void soundCompleteCallback(SystemSoundID soundID,void * clientData){ NSLog(@"播放完成..."); } /** * 播放音效檔案 * * @param name 音訊檔名稱 */ -(void)playSoundEffect:(NSString *)name{ NSString *audioFile=[[NSBundle mainBundle] pathForResource:name ofType:nil]; NSURL *fileUrl=[NSURL fileURLWithPath:audioFile]; //1.獲得系統聲音ID SystemSoundID soundID=0; /** * inFileUrl:音訊檔案url * outSystemSoundID:聲音id(此函式會將音效檔案加入到系統音訊服務中並返回一個長整形ID) */ AudioServicesCreateSystemSoundID((__bridge CFURLRef)(fileUrl), &soundID); //如果需要在播放完之後執行某些操作,可以呼叫如下方法註冊一個播放完成回撥函式 AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, soundCompleteCallback, NULL); //2.播放音訊 AudioServicesPlaySystemSound(soundID);//播放音效 // AudioServicesPlayAlertSound(soundID);//播放音效並震動 } @end
音樂
如果播放較大的音訊或者要對音訊有精確的控制則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方法更新測量值 |
物件方法 | 說明 |
- (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方法 |
@property(nonatomic, copy) NSArray *channelAssignments | 獲得或設定播放聲道 |
代理方法 | 說明 |
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag | 音訊播放完成 |
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error | 音訊解碼發生錯誤 |
AVAudioPlayer的使用比較簡單:
- 初始化AVAudioPlayer物件,此時通常指定本地檔案路徑。
- 設定播放器屬性,例如重複次數、音量大小等。
- 呼叫play方法播放。
下面就使用AVAudioPlayer實現一個簡單播放器,在這個播放器中實現了播放、暫停、顯示播放進度功能,當然例如調節音量、設定迴圈模式、甚至是聲波影像(通過分析音訊分貝值)等功能都可以實現,這裡就不再一一演示。介面效果如下:
當然由於AVAudioPlayer一次只能播放一個音訊檔案,所有上一曲、下一曲其實可以通過建立多個播放器物件來完成,這裡暫不實現。播放進度的實現主要依靠一個定時器實時計算當前播放時長和音訊總時長的比例,另外為了演示委託方法,下面的程式碼中也實現了播放完成委託方法,通常如果有下一曲功能的話播放完可以觸發下一曲音樂播放。下面是主要程式碼:
// // ViewController.m // KCAVAudioPlayer // // Created by Kenshin Cui on 14/03/30. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "ViewController.h" #import <AVFoundation/AVFoundation.h> #define kMusicFile @"劉若英 - 原來你也在這裡.mp3" #define kMusicSinger @"劉若英" #define kMusicTitle @"原來你也在這裡" @interface ViewController ()<AVAudioPlayerDelegate> @property (nonatomic,strong) AVAudioPlayer *audioPlayer;//播放器 @property (weak, nonatomic) IBOutlet UILabel *controlPanel; //控制皮膚 @property (weak, nonatomic) IBOutlet UIProgressView *playProgress;//播放進度 @property (weak, nonatomic) IBOutlet UILabel *musicSinger; //演唱者 @property (weak, nonatomic) IBOutlet UIButton *playOrPause; //播放/暫停按鈕(如果tag為0認為是暫停狀態,1是播放狀態) @property (weak ,nonatomic) NSTimer *timer;//進度更新定時器 @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self setupUI]; } /** * 初始化UI */ -(void)setupUI{ self.title=kMusicTitle; self.musicSinger.text=kMusicSinger; } -(NSTimer *)timer{ if (!_timer) { _timer=[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateProgress) userInfo:nil repeats:true]; } return _timer; } /** * 建立播放器 * * @return 音訊播放器 */ -(AVAudioPlayer *)audioPlayer{ if (!_audioPlayer) { NSString *urlStr=[[NSBundle mainBundle]pathForResource:kMusicFile ofType:nil]; NSURL *url=[NSURL fileURLWithPath:urlStr]; NSError *error=nil; //初始化播放器,注意這裡的Url引數只能時檔案路徑,不支援HTTP Url _audioPlayer=[[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error]; //設定播放器屬性 _audioPlayer.numberOfLoops=0;//設定為0不迴圈 _audioPlayer.delegate=self; [_audioPlayer prepareToPlay];//載入音訊檔案到快取 if(error){ NSLog(@"初始化播放器過程發生錯誤,錯誤資訊:%@",error.localizedDescription); return nil; } } return _audioPlayer; } /** * 播放音訊 */ -(void)play{ if (![self.audioPlayer isPlaying]) { [self.audioPlayer play]; self.timer.fireDate=[NSDate distantPast];//恢復定時器 } } /** * 暫停播放 */ -(void)pause{ if ([self.audioPlayer isPlaying]) { [self.audioPlayer pause]; self.timer.fireDate=[NSDate distantFuture];//暫停定時器,注意不能呼叫invalidate方法,此方法會取消,之後無法恢復 } } /** * 點選播放/暫停按鈕 * * @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]; } } /** * 更新播放進度 */ -(void)updateProgress{ float progress= self.audioPlayer.currentTime /self.audioPlayer.duration; [self.playProgress setProgress:progress animated:true]; } #pragma mark - 播放器代理方法 -(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{ NSLog(@"音樂播放完成..."); } @end執行效果:
音訊會話
事實上上面的播放器還存在一些問題,例如通常我們看到的播放器即使退出到後臺也是可以播放的,而這個播放器如果退出到後臺它會自動暫停。如果要支援後臺播放需要做下面幾件事情:
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::方法啟動會話。
AVAudioSession *audioSession=[AVAudioSession sharedInstance]; [audioSession setCategory:AVAudioSessionCategoryPlayback error:nil]; [audioSession setActive:YES error:nil];
3.為了能夠讓應用退到後臺之後支援耳機控制,建議新增遠端控制事件(這一步不是後臺播放必須的)
前兩步是後臺播放所必須設定的,第三步主要用於接收遠端事件,這部分內容之前的文章中有詳細介紹,如果這一步不設定雖讓也能夠在後臺播放,但是無法獲得音訊控制權(如果在使用當前應用之前使用其他播放器播放音樂的話,此時如果按耳機播放鍵或者控制中心的播放按鈕則會播放前一個應用的音訊),並且不能使用耳機進行音訊控制。第一步操作相信大家都很容易理解,如果應用程式要允許執行到後臺必須設定,正常情況下應用如果進入後臺會被掛起,通過該設定可以上應用程式繼續在後臺執行。但是第二步使用的AVAudioSession有必要進行一下詳細的說明。
在iOS中每個應用都有一個音訊會話,這個會話就通過AVAudioSession來表示。AVAudioSession同樣存在於AVFoundation框架中,它是單例模式設計,通過sharedInstance進行訪問。在使用Apple裝置時大家會發現有些應用只要開啟其他音訊播放就會終止,而有些應用卻可以和其他應用同時播放,在多種音訊環境中如何去控制播放的方式就是通過音訊會話來完成的。下面是音訊會話的幾種會話模式:
會話型別 | 說明 | 是否要求輸入 | 是否要求輸出 | 是否遵從靜音鍵 |
AVAudioSessionCategoryAmbient | 混音播放,可以與其他音訊應用同時播放 | 否 | 是 | 是 |
AVAudioSessionCategorySoloAmbient | 獨佔播放 | 否 | 是 | 是 |
AVAudioSessionCategoryPlayback | 後臺播放,也是獨佔的 | 否 | 是 | 否 |
AVAudioSessionCategoryRecord | 錄音模式,用於錄音時使用 | 是 | 否 | 否 |
AVAudioSessionCategoryPlayAndRecord | 播放和錄音,此時可以錄音也可以播放 | 是 | 是 | 否 |
AVAudioSessionCategoryAudioProcessing | 硬體解碼音訊,此時不能播放和錄製 | 否 | 否 | 否 |
AVAudioSessionCategoryMultiRoute | 多種輸入輸出,例如可以耳機、USB裝置同時播放 | 是 | 是 | 否 |
注意:是否遵循靜音鍵表示在播放過程中如果使用者通過硬體設定為靜音是否能關閉聲音。
根據前面對音訊會話的理解,相信大家開發出能夠在後臺播放的音訊播放器並不難,但是注意一下,在前面的程式碼中也提到設定完音訊會話型別之後需要呼叫setActive::方法將會話啟用才能起作用。類似的,如果一個應用已經在播放音訊,開啟我們的應用之後設定了在後臺播放的會話型別,此時其他應用的音訊會停止而播放我們的音訊,如果希望我們的程式音訊播放完之後(關閉或退出到後臺之後)能夠繼續播放其他應用的音訊的話則可以呼叫setActive::方法關閉會話。程式碼如下:
// // ViewController.m // KCAVAudioPlayer // // Created by Kenshin Cui on 14/03/30. // Copyright (c) 2014年 cmjstudio. All rights reserved. // AVAudioSession 音訊會話 #import "ViewController.h" #import <AVFoundation/AVFoundation.h> #define kMusicFile @"劉若英 - 原來你也在這裡.mp3" #define kMusicSinger @"劉若英" #define kMusicTitle @"原來你也在這裡" @interface ViewController ()<AVAudioPlayerDelegate> @property (nonatomic,strong) AVAudioPlayer *audioPlayer;//播放器 @property (weak, nonatomic) IBOutlet UILabel *controlPanel; //控制皮膚 @property (weak, nonatomic) IBOutlet UIProgressView *playProgress;//播放進度 @property (weak, nonatomic) IBOutlet UILabel *musicSinger; //演唱者 @property (weak, nonatomic) IBOutlet UIButton *playOrPause; //播放/暫停按鈕(如果tag為0認為是暫停狀態,1是播放狀態) @property (weak ,nonatomic) NSTimer *timer;//進度更新定時器 @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self setupUI]; } /** * 顯示當面檢視控制器時註冊遠端事件 * * @param animated 是否以動畫的形式顯示 */ -(void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; //開啟遠端控制 [[UIApplication sharedApplication] beginReceivingRemoteControlEvents]; //作為第一響應者 //[self becomeFirstResponder]; } /** * 當前控制器檢視不顯示時取消遠端控制 * * @param animated 是否以動畫的形式消失 */ -(void)viewWillDisappear:(BOOL)animated{ [super viewWillDisappear:animated]; [[UIApplication sharedApplication] endReceivingRemoteControlEvents]; //[self resignFirstResponder]; } /** * 初始化UI */ -(void)setupUI{ self.title=kMusicTitle; self.musicSinger.text=kMusicSinger; } -(NSTimer *)timer{ if (!_timer) { _timer=[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateProgress) userInfo:nil repeats:true]; } return _timer; } /** * 建立播放器 * * @return 音訊播放器 */ -(AVAudioPlayer *)audioPlayer{ if (!_audioPlayer) { NSString *urlStr=[[NSBundle mainBundle]pathForResource:kMusicFile ofType:nil]; NSURL *url=[NSURL fileURLWithPath:urlStr]; NSError *error=nil; //初始化播放器,注意這裡的Url引數只能時檔案路徑,不支援HTTP Url _audioPlayer=[[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error]; //設定播放器屬性 _audioPlayer.numberOfLoops=0;//設定為0不迴圈 _audioPlayer.delegate=self; [_audioPlayer prepareToPlay];//載入音訊檔案到快取 if(error){ NSLog(@"初始化播放器過程發生錯誤,錯誤資訊:%@",error.localizedDescription); return nil; } //設定後臺播放模式 AVAudioSession *audioSession=[AVAudioSession sharedInstance]; [audioSession setCategory:AVAudioSessionCategoryPlayback error:nil]; // [audioSession setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:nil]; [audioSession setActive:YES error:nil]; //新增通知,拔出耳機後暫停播放 [[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];//恢復定時器 } } /** * 暫停播放 */ -(void)pause{ if ([self.audioPlayer isPlaying]) { [self.audioPlayer pause]; self.timer.fireDate=[NSDate distantFuture];//暫停定時器,注意不能呼叫invalidate方法,此方法會取消,之後無法恢復 } } /** * 點選播放/暫停按鈕 * * @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]; } } /** * 更新播放進度 */ -(void)updateProgress{ float progress= self.audioPlayer.currentTime /self.audioPlayer.duration; [self.playProgress setProgress:progress animated:true]; } /** * 一旦輸出改變則執行此方法 * * @param notification 輸出改變通知物件 */ -(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]; //原裝置為耳機則暫停 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(@"音樂播放完成..."); //根據實際情況播放完成可以將會話關閉,其他音訊應用繼續播放 [[AVAudioSession sharedInstance]setActive:NO error:nil]; } @end
在上面的程式碼中還實現了拔出耳機暫停音樂播放的功能,這也是一個比較常見的功能。在iOS7及以後的版本中可以通過通知獲得輸出改變的通知,然後拿到通知物件後根據userInfo獲得是何種改變型別,進而根據情況對音樂進行暫停操作。
擴充套件--播放音樂庫中的音樂
眾所周知音樂是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;
有了這些方法,就可以很容易獲到歌曲、播放列表、專輯媒體等媒體佇列了,這樣就可以通過:- (void)setQueueWithQuery:(MPMediaQuery *)query方法設定音樂來源了。又或者得到MPMediaQueue之後建立MPMediaItemCollection,使用- (void)setQueueWithItemCollection:(MPMediaItemCollection *)itemCollection設定音樂來源。
有時候可能希望使用者自己來選擇要播放的音樂,這時可以使用MPMediaPickerController,它是一個檢視控制器,類似於UIImagePickerController,選擇完播放來源後可以在其代理方法中獲得MPMediaItemCollection物件。
無論是通過哪種方式獲得MPMusicPlayerController的媒體源,可能都希望將每個媒體的資訊顯示出來,這時候可以通過MPMediaItem物件獲得。一個MPMediaItem代表一個媒體檔案,通過它可以訪問媒體標題、專輯名稱、專輯封面、音樂時長等等。無論是MPMediaQueue還是MPMediaItemCollection都有一個items屬性,它是MPMediaItem陣列,通過這個屬性可以獲得MPMediaItem物件。
下面就簡單看一下MPMusicPlayerController的使用,在下面的例子中簡單演示了音樂的選擇、播放、暫停、通知、下一曲、上一曲功能,相信有了上面的概念,程式碼讀起來並不複雜(示例中是直接通過MPMeidaPicker進行音樂選擇的,但是仍然提供了兩個方法getLocalMediaQuery和getLocalMediaItemCollection來演示如何直接通過MPMediaQueue獲得媒體佇列或媒體集合):
// // ViewController.m // MPMusicPlayerController // // Created by Kenshin Cui 14/03/30 // Copyright (c) 2014年 cmjstudio. All rights reserved. // #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]; } -(void)dealloc{ [self.musicPlayer endGeneratingPlaybackNotifications]; } /** * 獲得音樂播放器 * * @return 音樂播放器 */ -(MPMusicPlayerController *)musicPlayer{ if (!_musicPlayer) { _musicPlayer=[MPMusicPlayerController systemMusicPlayer]; [_musicPlayer beginGeneratingPlaybackNotifications];//開啟通知,否則監控不到MPMusicPlayerController的通知 [self addNotification];//新增通知 //如果不使用MPMediaPickerController可以使用如下方法獲得音樂庫媒體佇列 //[_musicPlayer setQueueWithItemCollection:[self getLocalMediaItemCollection]]; } return _musicPlayer; } /** * 建立媒體選擇器 * * @return 媒體選擇器 */ -(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; } /** * 取得媒體佇列 * * @return 媒體佇列 */ -(MPMediaQuery *)getLocalMediaQuery{ MPMediaQuery *mediaQueue=[MPMediaQuery songsQuery]; for (MPMediaItem *item in mediaQueue.items) { NSLog(@"標題:%@,%@",item.title,item.albumTitle); } return mediaQueue; } /** * 取得媒體集合 * * @return 媒體集合 */ -(MPMediaItemCollection *)getLocalMediaItemCollection{ MPMediaQuery *mediaQueue=[MPMediaQuery songsQuery]; NSMutableArray *array=[NSMutableArray array]; for (MPMediaItem *item in mediaQueue.items) { [array addObject:item]; NSLog(@"標題:%@,%@",item.title,item.albumTitle); } MPMediaItemCollection *mediaItemCollection=[[MPMediaItemCollection alloc]initWithItems:[array copy]]; return mediaItemCollection; } #pragma mark - MPMediaPickerController代理方法 //選擇完成 -(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 - 通知 /** * 新增通知 */ -(void)addNotification{ NSNotificationCenter *notificationCenter=[NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(playbackStateChange:) name:MPMusicPlayerControllerPlaybackStateDidChangeNotification object:self.musicPlayer]; } /** * 播放狀態改變通知 * * @param notification 通知物件 */ -(void)playbackStateChange:(NSNotification *)notification{ switch (self.musicPlayer.playbackState) { case MPMusicPlaybackStatePlaying: NSLog(@"正在播放..."); break; case MPMusicPlaybackStatePaused: NSLog(@"播放暫停."); break; case MPMusicPlaybackStateStopped: NSLog(@"播放停止."); break; default: break; } } #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
錄音
除了上面說的,在AVFoundation框架中還要一個AVAudioRecorder類專門處理錄音操作,它同樣支援多種音訊格式。與AVAudioPlayer類似,你完全可以將它看成是一個錄音機控制類,下面是常用的屬性和方法:
屬性 | 說明 |
@property(readonly, getter=isRecording) BOOL recording; | 是否正在錄音,只讀 |
@property(readonly) NSURL *url | 錄音檔案地址,只讀 |
@property(readonly) NSDictionary *settings | 錄音檔案設定,只讀 |
@property(readonly) NSTimeInterval currentTime | 錄音時長,只讀,注意僅僅在錄音狀態可用 |
@property(readonly) NSTimeInterval deviceCurrentTime | 輸入設定的時間長度,只讀,注意此屬性一直可訪問 |
@property(getter=isMeteringEnabled) BOOL meteringEnabled; | 是否啟用錄音測量,如果啟用錄音測量可以獲得錄音分貝等資料資訊 |
@property(nonatomic, copy) NSArray *channelAssignments | 當前錄音的通道 |
物件方法 | 說明 |
- (instancetype)initWithURL:(NSURL *)url settings:(NSDictionary *)settings error:(NSError **)outError | 錄音機物件初始化方法,注意其中的url必須是本地檔案url,settings是錄音格式、編碼等設定 |
- (BOOL)prepareToRecord | 準備錄音,主要用於建立緩衝區,如果不手動呼叫,在呼叫record錄音時也會自動呼叫 |
- (BOOL)record | 開始錄音 |
- (BOOL)recordAtTime:(NSTimeInterval)time | 在指定的時間開始錄音,一般用於錄音暫停再恢復錄音 |
- (BOOL)recordForDuration:(NSTimeInterval) duration | 按指定的時長開始錄音 |
- (BOOL)recordAtTime:(NSTimeInterval)time forDuration:(NSTimeInterval) duration | 在指定的時間開始錄音,並指定錄音時長 |
- (void)pause; | 暫停錄音 |
- (void)stop; | 停止錄音 |
- (BOOL)deleteRecording; | 刪除錄音,注意要刪除錄音此時錄音機必須處於停止狀態 |
- (void)updateMeters; | 更新測量資料,注意只有meteringEnabled為YES此方法才可用 |
- (float)peakPowerForChannel:(NSUInteger)channelNumber; | 指定通道的測量峰值,注意只有呼叫完updateMeters才有值 |
- (float)averagePowerForChannel:(NSUInteger)channelNumber | 指定通道的測量平均值,注意只有呼叫完updateMeters才有值 |
代理方法 | 說明 |
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag | 完成錄音 |
- (void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder *)recorder error:(NSError *)error | 錄音編碼發生錯誤 |
AVAudioRecorder很多屬性和方法跟AVAudioPlayer都是類似的,但是它的建立有所不同,在建立錄音機時除了指定路徑外還必須指定錄音設定資訊,因為錄音機必須知道錄音檔案的格式、取樣率、通道數、每個取樣點的位數等資訊,但是也並不是所有的資訊都必須設定,通常只需要幾個常用設定。關於錄音設定詳見幫助文件中的“AV Foundation Audio Settings Constants”。
下面就使用AVAudioRecorder建立一個錄音機,實現了錄音、暫停、停止、播放等功能,實現效果大致如下:
在這個示例中將實行一個完整的錄音控制,包括錄音、暫停、恢復、停止,同時還會實時展示使用者錄音的聲音波動,當使用者點選完停止按鈕還會自動播放錄音檔案。程式的構建主要分為以下幾步:
- 設定音訊會話型別為AVAudioSessionCategoryPlayAndRecord,因為程式中牽扯到錄音和播放操作。
- 建立錄音機AVAudioRecorder,指定錄音儲存的路徑並且設定錄音屬性,注意對於一般的錄音檔案要求的取樣率、位數並不高,需要適當設定以保證錄音檔案的大小和效果。
- 設定錄音機代理以便在錄音完成後播放錄音,開啟錄音測量保證能夠實時獲得錄音時的聲音強度。(注意聲音強度範圍-160到0,0代表最大輸入)
- 建立音訊播放器AVAudioPlayer,用於在錄音完成之後播放錄音。
- 建立一個定時器以便實時重新整理錄音測量值並更新錄音強度到UIProgressView中顯示。
- 新增錄音、暫停、恢復、停止操作,需要注意錄音的恢復操作其實是有音訊會話管理的,恢復時只要再次呼叫record方法即可,無需手動管理恢復時間等。
下面是主要程式碼:
// // 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;//音訊錄音機 @property (nonatomic,strong) AVAudioPlayer *audioPlayer;//音訊播放器,用於播放錄音檔案 @property (nonatomic,strong) NSTimer *timer;//錄音聲波監控(注意這裡暫時不對播放進行監控) @property (weak, nonatomic) IBOutlet UIButton *record;//開始錄音 @property (weak, nonatomic) IBOutlet UIButton *pause;//暫停錄音 @property (weak, nonatomic) IBOutlet UIButton *resume;//恢復錄音 @property (weak, nonatomic) IBOutlet UIButton *stop;//停止錄音 @property (weak, nonatomic) IBOutlet UIProgressView *audioPower;//音訊波動 @end @implementation ViewController #pragma mark - 控制器檢視方法 - (void)viewDidLoad { [super viewDidLoad]; [self setAudioSession]; } #pragma mark - 私有方法 /** * 設定音訊會話 */ -(void)setAudioSession{ AVAudioSession *audioSession=[AVAudioSession sharedInstance]; //設定為播放和錄音狀態,以便可以在錄製完之後播放錄音 [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; } /** * 取得錄音檔案設定 * * @return 錄音設定 */ -(NSDictionary *)getAudioSetting{ NSMutableDictionary *dicM=[NSMutableDictionary dictionary]; //設定錄音格式 [dicM setObject:@(kAudioFormatLinearPCM) forKey:AVFormatIDKey]; //設定錄音取樣率,8000是電話取樣率,對於一般錄音已經夠了 [dicM setObject:@(8000) forKey:AVSampleRateKey]; //設定通道,這裡採用單聲道 [dicM setObject:@(1) forKey:AVNumberOfChannelsKey]; //每個取樣點位數,分為8、16、24、32 [dicM setObject:@(8) forKey:AVLinearPCMBitDepthKey]; //是否使用浮點數取樣 [dicM setObject:@(YES) forKey:AVLinearPCMIsFloatKey]; //....其他設定等 return dicM; } /** * 獲得錄音機物件 * * @return 錄音機物件 */ -(AVAudioRecorder *)audioRecorder{ if (!_audioRecorder) { //建立錄音檔案儲存路徑 NSURL *url=[self getSavePath]; //建立錄音格式設定 NSDictionary *setting=[self getAudioSetting]; //建立錄音機 NSError *error=nil; _audioRecorder=[[AVAudioRecorder alloc]initWithURL:url settings:setting error:&error]; _audioRecorder.delegate=self; _audioRecorder.meteringEnabled=YES;//如果要監控聲波則必須設定為YES if (error) { NSLog(@"建立錄音機物件時發生錯誤,錯誤資訊:%@",error.localizedDescription); return nil; } } return _audioRecorder; } /** * 建立播放器 * * @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(@"建立播放器過程中發生錯誤,錯誤資訊:%@",error.localizedDescription); return nil; } } return _audioPlayer; } /** * 錄音聲波監控定製器 * * @return 定時器 */ -(NSTimer *)timer{ if (!_timer) { _timer=[NSTimer scheduledTimerWithTimeInterval:0.1f target:self selector:@selector(audioPowerChange) userInfo:nil repeats:YES]; } return _timer; } /** * 錄音聲波狀態設定 */ -(void)audioPowerChange{ [self.audioRecorder updateMeters];//更新測量值 float power= [self.audioRecorder averagePowerForChannel:0];//取得第一個通道的音訊,注意音訊強度範圍時-160到0 CGFloat progress=(1.0/160.0)*(power+160.0); [self.audioPower setProgress:progress]; } #pragma mark - UI事件 /** * 點選錄音按鈕 * * @param sender 錄音按鈕 */ - (IBAction)recordClick:(UIButton *)sender { if (![self.audioRecorder isRecording]) { [self.audioRecorder record];//首次使用應用時如果呼叫record方法會詢問使用者是否允許使用麥克風 self.timer.fireDate=[NSDate distantPast]; } } /** * 點選暫定按鈕 * * @param sender 暫停按鈕 */ - (IBAction)pauseClick:(UIButton *)sender { if ([self.audioRecorder isRecording]) { [self.audioRecorder pause]; self.timer.fireDate=[NSDate distantFuture]; } } /** * 點選恢復按鈕 * 恢復錄音只需要再次呼叫record,AVAudioSession會幫助你記錄上次錄音位置並追加錄音 * * @param sender 恢復按鈕 */ - (IBAction)resumeClick:(UIButton *)sender { [self recordClick:sender]; } /** * 點選停止按鈕 * * @param sender 停止按鈕 */ - (IBAction)stopClick:(UIButton *)sender { [self.audioRecorder stop]; self.timer.fireDate=[NSDate distantFuture]; self.audioPower.progress=0.0; } #pragma mark - 錄音機代理方法 /** * 錄音完成,錄音完成後播放錄音 * * @param recorder 錄音機物件 * @param flag 是否成功 */ -(void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag{ if (![self.audioPlayer isPlaying]) { [self.audioPlayer play]; } NSLog(@"錄音完成!"); } @end
執行效果:
音訊佇列服務
大家應該已經注意到了,無論是前面的錄音還是音訊播放均不支援網路流媒體播放,當然對於錄音來說這種需求可能不大,但是對於音訊播放來說有時候就很有必要了。AVAudioPlayer只能播放本地檔案,並且是一次性載入所以音訊資料,初始化AVAudioPlayer時指定的URL也只能是File URL而不能是HTTP URL。當然,將音訊檔案下載到本地然後再呼叫AVAudioPlayer來播放也是一種播放網路音訊的辦法,但是這種方式最大的弊端就是必須等到整個音訊播放完成才能播放,而不能使用流式播放,這往往在實際開發中是不切實際的。那麼在iOS中如何播放網路流媒體呢?就是使用AudioToolbox框架中的音訊佇列服務Audio Queue Services。
使用音訊佇列服務完全可以做到音訊播放和錄製,首先看一下錄音音訊服務佇列:
一個音訊服務佇列Audio Queue有三部分組成:
三個緩衝器Buffers:每個緩衝器都是一個儲存音訊資料的臨時倉庫。
一個緩衝佇列Buffer Queue:一個包含音訊緩衝器的有序佇列。
一個回撥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 Build Path中新增$(SDKROOT)/usr/include/libxml2。
4.將FreeStreamer中的FreeStreamerMobile-Prefix.pch檔案新增到專案中並將Targets-Build Settings-Precompile Prefix Header設定為YES,在Targets-Build Settings-Prefix Header設定為$(SRCROOT)/專案名稱/FreeStreamerMobile-Prefix.pch(因為Xcode6預設沒有pch檔案)
然後就可以編寫程式碼播放網路音訊了:
// // ViewController.m // AudioQueueServices // // Created by Kenshin Cui on 14/03/30. // Copyright (c) 2014年 cmjstudio. All rights reserved. // 使用FreeStreamer實現網路音訊播放 #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:@"劉若英 - 原來你也在這裡.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; } /** * 建立FSAudioStream物件 * * @return FSAudioStream物件 */ -(FSAudioStream *)audioStream{ if (!_audioStream) { NSURL *url=[self getNetworkUrl]; //建立FSAudioStream物件 _audioStream=[[FSAudioStream alloc]initWithUrl:url]; _audioStream.onFailure=^(FSAudioStreamError error,NSString *description){ NSLog(@"播放過程中發生錯誤,錯誤資訊:%@",description); }; _audioStream.onCompletion=^(){ NSLog(@"播放完成!"); }; [_audioStream setVolume:0.5];//設定聲音 } return _audioStream; } @end其實FreeStreamer的功能很強大,不僅僅是播放本地、網路音訊那麼簡單,它還支援播放列表、檢查包內容、RSS訂閱、播放中斷等很多強大的功能,甚至還包含了一個音訊分析器,有興趣的朋友可以訪問官網檢視詳細用法
視訊
MPMoviePlayerController
在iOS中播放視訊可以使用MediaPlayer.framework種的MPMoviePlayerController類來完成,它支援本地視訊和網路視訊播放。這個類實現了MPMediaPlayback協議,因此具備一般的播放器控制功能,例如播放、暫停、停止等。但是MPMediaPlayerController自身並不是一個完整的檢視控制器,如果要在UI中展示視訊需要將view屬性新增到介面中。下面列出了MPMoviePlayerController的常用屬性和方法:
屬性 | 說明 |
@property (nonatomic, copy) NSURL *contentURL | 播放媒體URL,這個URL可以是本地路徑,也可以是網路路徑 |
@property (nonatomic, readonly) UIView *view | 播放器檢視,如果要顯示視訊必須將此檢視新增到控制器檢視中 |
@property (nonatomic, readonly) UIView *backgroundView | 播放器背景檢視 |
@property (nonatomic, readonly) MPMoviePlaybackState playbackState | 媒體播放狀態,列舉型別: MPMoviePlaybackStateStopped:停止播放 MPMoviePlaybackStatePlaying:正在播放 MPMoviePlaybackStatePaused:暫停 MPMoviePlaybackStateInterrupted:中斷 MPMoviePlaybackStateSeekingForward:向前定位 MPMoviePlaybackStateSeekingBackward:向後定位 |
@property (nonatomic, readonly) MPMovieLoadState loadState | 網路媒體載入狀態,列舉型別: MPMovieLoadStateUnknown:位置型別 MPMovieLoadStatePlayable: MPMovieLoadStatePlaythroughOK:這種狀態如果shouldAutoPlay為YES將自動播放 MPMovieLoadStateStalled:停滯狀態 |
@property (nonatomic) MPMovieControlStyle controlStyle | 控制皮膚風格,列舉型別: MPMovieControlStyleNone:無控制皮膚 MPMovieControlStyleEmbedded:嵌入視訊風格 MPMovieControlStyleFullscreen:全屏 MPMovieControlStyleDefault:預設風格 |
@property (nonatomic) MPMovieRepeatMode repeatMode; | 重複播放模式,列舉型別: MPMovieRepeatModeNone:不重複,預設值 MPMovieRepeatModeOne:重複播放 |
@property (nonatomic) BOOL shouldAutoplay | 當網路媒體快取到一定資料時是否自動播放,預設為YES |
@property (nonatomic, getter=isFullscreen) BOOL fullscreen | 是否全屏展示,預設為NO,注意如果要通過此屬性設定全屏必須在檢視顯示完成後設定,否則無效 |
@property (nonatomic) MPMovieScalingMode scalingMode | 視訊縮放填充模式,列舉型別: MPMovieScalingModeNone:不進行任何縮放 MPMovieScalingModeAspectFit:固定縮放比例並且儘量全部展示視訊,不會裁切視訊 MPMovieScalingModeAspectFill:固定縮放比例並填充滿整個檢視展示,可能會裁切視訊 MPMovieScalingModeFill:不固定縮放比例壓縮填充整個檢視,視訊不會被裁切但是比例失衡 |
@property (nonatomic, readonly) BOOL readyForDisplay | 是否有相關媒體被播放 |
@property (nonatomic, readonly) MPMovieMediaTypeMask movieMediaTypes | 媒體類別,列舉型別: MPMovieMediaTypeMaskNone:未知型別 MPMovieMediaTypeMaskVideo:視訊 MPMovieMediaTypeMaskAudio:音訊 |
@property (nonatomic) MPMovieSourceType movieSourceType | 媒體源,列舉型別: MPMovieSourceTypeUnknown:未知來源 MPMovieSourceTypeFile:本地檔案 MPMovieSourceTypeStreaming:流媒體(直播或點播) |
@property (nonatomic, readonly) NSTimeInterval duration | 媒體時長,如果未知則返回0 |
@property (nonatomic, readonly) NSTimeInterval playableDuration | 媒體可播放時長,主要用於表示網路媒體已下載視訊時長 |
@property (nonatomic, readonly) CGSize naturalSize | 視訊實際尺寸,如果未知則返回CGSizeZero |
@property (nonatomic) NSTimeInterval initialPlaybackTime | 起始播放時間 |
@property (nonatomic) NSTimeInterval endPlaybackTime | 終止播放時間 |
@property (nonatomic) BOOL allowsAirPlay | 是否允許無線播放,預設為YES |
@property (nonatomic, readonly, getter=isAirPlayVideoActive) BOOL airPlayVideoActive | 當前媒體是否正在通過AirPlay播放 |
@property(nonatomic, readonly) BOOL isPreparedToPlay | 是否準備好播放 |
@property(nonatomic) NSTimeInterval currentPlaybackTime | 當前播放時間,單位:秒 |
@property(nonatomic) float currentPlaybackRate | 當前播放速度,如果暫停則為0,正常速度為1.0,非0資料表示倍率 |
物件方法 | 說明 |
- (instancetype)initWithContentURL:(NSURL *)url | 使用指定的URL初始化媒體播放控制器物件 |
- (void)setFullscreen:(BOOL)fullscreen animated:(BOOL)animated | 設定視訊全屏,注意如果要通過此方法設定全屏則必須在其檢視顯示之後設定,否則無效 |
- (void)requestThumbnailImagesAtTimes:(NSArray *)playbackTimes timeOption:(MPMovieTimeOption)option | 獲取在指定播放時間的視訊縮圖,第一個引數是獲取縮圖的時間點陣列;第二個引數代表時間點精度,列舉型別: MPMovieTimeOptionNearestKeyFrame:時間點附近 MPMovieTimeOptionExact:準確時間 |
- (void)cancelAllThumbnailImageRequests | 取消所有縮圖獲取請求 |
- (void)prepareToPlay | 準備播放,載入視訊資料到快取,當呼叫play方法時如果沒有準備好會自動呼叫此方法 |
- (void)play | 開始播放 |
- (void)pause | 暫停播放 |
- (void)stop | 停止播放 |
- (void)beginSeekingForward | 向前定位 |
- (void)beginSeekingBackward | 向後定位 |
- (void)endSeeking | 停止快進/快退 |
通知 | 說明 |
MPMoviePlayerScalingModeDidChangeNotification | 視訊縮放填充模式發生改變 |
MPMoviePlayerPlaybackDidFinishNotification | 媒體播放完成或使用者手動退出,具體完成原因可以通過通知userInfo中的key為MPMoviePlayerPlaybackDidFinishReasonUserInfoKey的物件獲取 |
MPMoviePlayerPlaybackStateDidChangeNotification | 播放狀態改變,可配合playbakcState屬性獲取具體狀態 |
MPMoviePlayerLoadStateDidChangeNotification | 媒體網路載入狀態改變 |
MPMoviePlayerNowPlayingMovieDidChangeNotification | 當前播放的媒體內容發生改變 |
MPMoviePlayerWillEnterFullscreenNotification | 將要進入全屏 |
MPMoviePlayerDidEnterFullscreenNotification | 進入全屏後 |
MPMoviePlayerWillExitFullscreenNotification | 將要退出全屏 |
MPMoviePlayerDidExitFullscreenNotification | 退出全屏後 |
MPMoviePlayerIsAirPlayVideoActiveDidChangeNotification | 當媒體開始通過AirPlay播放或者結束AirPlay播放 |
MPMoviePlayerReadyForDisplayDidChangeNotification | 視訊顯示狀態改變 |
MPMovieMediaTypesAvailableNotification | 確定了媒體可用型別後 |
MPMovieSourceTypeAvailableNotification | 確定了媒體來源後 |
MPMovieDurationAvailableNotification | 確定了媒體播放時長後 |
MPMovieNaturalSizeAvailableNotification | 確定了媒體的實際尺寸後 |
MPMoviePlayerThumbnailImageRequestDidFinishNotification | 縮圖請求完成之後 |
MPMediaPlaybackIsPreparedToPlayDidChangeNotification | 做好播放準備後 |
注意MPMediaPlayerController的狀態等資訊並不是通過代理來和外界互動的,而是通過通知中心,因此從上面的列表中可以看到常用的一些通知。由於MPMoviePlayerController本身對於媒體播放做了深度的封裝,使用起來就相當簡單:建立MPMoviePlayerController物件,設定frame屬性,將MPMoviePlayerController的view新增到控制器檢視中。下面的示例中將建立一個播放控制器並新增播放狀態改變及播放完成的通知:
// // ViewController.m // MPMoviePlayerController // // Created by Kenshin Cui on 14/03/30. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "ViewController.h" #import <MediaPlayer/MediaPlayer.h> @interface ViewController () @property (nonatomic,strong) MPMoviePlayerController *moviePlayer;//視訊播放控制器 @end @implementation ViewController #pragma mark - 控制器檢視方法 - (void)viewDidLoad { [super viewDidLoad]; //播放 [self.moviePlayer play]; //新增通知 [self addNotification]; } -(void)dealloc{ //移除所有通知監控 [[NSNotificationCenter defaultCenter] removeObserver:self]; } #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; } /** * 取得網路檔案路徑 * * @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; } /** * 建立媒體播放控制器 * * @return 媒體播放控制器 */ -(MPMoviePlayerController *)moviePlayer{ if (!_moviePlayer) { NSURL *url=[self getNetworkUrl]; _moviePlayer=[[MPMoviePlayerController alloc]initWithContentURL:url]; _moviePlayer.view.frame=self.view.bounds; _moviePlayer.view.autoresizingMask=UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; [self.view addSubview:_moviePlayer.view]; } return _moviePlayer; } /** * 新增通知監控媒體播放控制器狀態 */ -(void)addNotification{ NSNotificationCenter *notificationCenter=[NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(mediaPlayerPlaybackStateChange:) name:MPMoviePlayerPlaybackStateDidChangeNotification object:self.moviePlayer]; [notificationCenter addObserver:self selector:@selector(mediaPlayerPlaybackFinished:) name:MPMoviePlayerPlaybackDidFinishNotification object:self.moviePlayer]; } /** * 播放狀態改變,注意播放完成時的狀態是暫停 * * @param notification 通知物件 */ -(void)mediaPlayerPlaybackStateChange:(NSNotification *)notification{ switch (self.moviePlayer.playbackState) { case MPMoviePlaybackStatePlaying: NSLog(@"正在播放..."); break; case MPMoviePlaybackStatePaused: NSLog(@"暫停播放."); break; case MPMoviePlaybackStateStopped: NSLog(@"停止播放."); break; default: NSLog(@"播放狀態:%li",self.moviePlayer.playbackState); break; } } /** * 播放完成 * * @param notification 通知物件 */ -(void)mediaPlayerPlaybackFinished:(NSNotification *)notification{ NSLog(@"播放完成.%li",self.moviePlayer.playbackState); } @end
執行效果:
從上面的API大家也不難看出其實MPMoviePlayerController功能相當強大,日常開發中作為一般的媒體播放器也完全沒有問題。MPMoviePlayerController除了一般的視訊播放和控制外還有一些強大的功能,例如擷取視訊縮圖。請求視訊縮圖時只要呼叫- (void)requestThumbnailImagesAtTimes:(NSArray *)playbackTimes timeOption:(MPMovieTimeOption)option方法指定獲得縮圖的時間點,然後監控MPMoviePlayerThumbnailImageRequestDidFinishNotification通知,每個時間點的縮圖請求完成就會呼叫通知,在通知呼叫方法中可以通過MPMoviePlayerThumbnailImageKey獲得UIImage物件處理即可。例如下面的程式演示了在程式啟動後獲得兩個時間點的縮圖的過程,截圖成功後儲存到相簿:
// // ViewController.m // MPMoviePlayerController // // Created by Kenshin Cui on 14/03/30. // Copyright (c) 2014年 cmjstudio. All rights reserved. // 視訊截圖 #import "ViewController.h" #import <MediaPlayer/MediaPlayer.h> @interface ViewController () @property (nonatomic,strong) MPMoviePlayerController *moviePlayer;//視訊播放控制器 @end @implementation ViewController #pragma mark - 控制器檢視方法 - (void)viewDidLoad { [super viewDidLoad]; //播放 [self.moviePlayer play]; //新增通知 [self addNotification]; //獲取縮圖 [self thumbnailImageRequest]; } -(void)dealloc{ //移除所有通知監控 [[NSNotificationCenter defaultCenter] removeObserver:self]; } #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; } /** * 取得網路檔案路徑 * * @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; } /** * 建立媒體播放控制器 * * @return 媒體播放控制器 */ -(MPMoviePlayerController *)moviePlayer{ if (!_moviePlayer) { NSURL *url=[self getNetworkUrl]; _moviePlayer=[[MPMoviePlayerController alloc]initWithContentURL:url]; _moviePlayer.view.frame=self.view.bounds; _moviePlayer.view.autoresizingMask=UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; [self.view addSubview:_moviePlayer.view]; } return _moviePlayer; } /** * 獲取視訊縮圖 */ -(void)thumbnailImageRequest{ //獲取13.0s、21.5s的縮圖 [self.moviePlayer requestThumbnailImagesAtTimes:@[@13.0,@21.5] timeOption:MPMovieTimeOptionNearestKeyFrame]; } #pragma mark - 控制器通知 /** * 新增通知監控媒體播放控制器狀態 */ -(void)addNotification{ NSNotificationCenter *notificationCenter=[NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(mediaPlayerPlaybackStateChange:) name:MPMoviePlayerPlaybackStateDidChangeNotification object:self.moviePlayer]; [notificationCenter addObserver:self selector:@selector(mediaPlayerPlaybackFinished:) name:MPMoviePlayerPlaybackDidFinishNotification object:self.moviePlayer]; [notificationCenter addObserver:self selector:@selector(mediaPlayerThumbnailRequestFinished:) name:MPMoviePlayerThumbnailImageRequestDidFinishNotification object:self.moviePlayer]; } /** * 播放狀態改變,注意播放完成時的狀態是暫停 * * @param notification 通知物件 */ -(void)mediaPlayerPlaybackStateChange:(NSNotification *)notification{ switch (self.moviePlayer.playbackState) { case MPMoviePlaybackStatePlaying: NSLog(@"正在播放..."); break; case MPMoviePlaybackStatePaused: NSLog(@"暫停播放."); break; case MPMoviePlaybackStateStopped: NSLog(@"停止播放."); break; default: NSLog(@"播放狀態:%li",self.moviePlayer.playbackState); break; } } /** * 播放完成 * * @param notification 通知物件 */ -(void)mediaPlayerPlaybackFinished:(NSNotification *)notification{ NSLog(@"播放完成.%li",self.moviePlayer.playbackState); } /** * 縮圖請求完成,此方法每次截圖成功都會呼叫一次 * * @param notification 通知物件 */ -(void)mediaPlayerThumbnailRequestFinished:(NSNotification *)notification{ NSLog(@"視訊截圖完成."); UIImage *image=notification.userInfo[MPMoviePlayerThumbnailImageKey]; //儲存圖片到相簿(首次呼叫會請求使用者獲得訪問相簿許可權) UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); } @end
截圖效果:
擴充套件--使用AVFoundation生成縮圖
通過前面的方法大家應該已經看到,使用MPMoviePlayerController來生成縮圖足夠簡單,但是如果僅僅是是為了生成縮圖而不進行視訊播放的話,此刻使用MPMoviePlayerController就有點大材小用了。其實使用AVFundation框架中的AVAssetImageGenerator就可以獲取視訊縮圖。使用AVAssetImageGenerator獲取縮圖大致分為三個步驟:
- 建立AVURLAsset物件(此類主要用於獲取媒體資訊,包括視訊、聲音等)。
- 根據AVURLAsset建立AVAssetImageGenerator物件。
- 使用AVAssetImageGenerator的copyCGImageAtTime::方法獲得指定時間點的截圖。
// // 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; } /** * 取得網路檔案路徑 * * @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; } /** * 擷取指定時間的視訊縮圖 * * @param timeBySecond 時間點 */ -(void)thumbnailImageRequest:(CGFloat )timeBySecond{ //建立URL NSURL *url=[self getNetworkUrl]; //根據url建立AVURLAsset AVURLAsset *urlAsset=[AVURLAsset assetWithURL:url]; //根據AVURLAsset建立AVAssetImageGenerator AVAssetImageGenerator *imageGenerator=[AVAssetImageGenerator assetImageGeneratorWithAsset:urlAsset]; /*截圖 * requestTime:縮圖建立時間 * actualTime:縮圖實際生成的時間 */ NSError *error=nil; CMTime time=CMTimeMakeWithSeconds(timeBySecond, 10);//CMTime是表示電影時間資訊的結構體,第一個參數列示是視訊第幾秒,第二個參數列示每秒幀數.(如果要活的某一秒的第幾幀可以使用CMTimeMake方法) CMTime actualTime; CGImageRef cgImage= [imageGenerator copyCGImageAtTime:time actualTime:&actualTime error:&error]; if(error){ NSLog(@"擷取視訊縮圖時發生錯誤,錯誤資訊:%@",error.localizedDescription); return; } CMTimeShow(actualTime); UIImage *image=[UIImage imageWithCGImage:cgImage];//轉化為UIImage //儲存到相簿 UIImageWriteToSavedPhotosAlbum(image,nil, nil, nil); CGImageRelease(cgImage); } @end
生成的縮圖效果:
MPMoviePlayerViewController
其實MPMoviePlayerController如果不作為嵌入視訊來播放(例如在新聞中嵌入一個視訊),通常在播放時都是佔滿一個螢幕的,特別是在iPhone、iTouch上。因此從iOS3.2以後蘋果也在思考既然MPMoviePlayerController在使用時通常都是將其檢視view新增到另外一個檢視控制器中作為子檢視,那麼何不直接建立一個控制器檢視內部建立一個MPMoviePlayerController屬性並且預設全屏播放,開發者在開發的時候直接使用這個檢視控制器。這個內部有一個MPMoviePlayerController的檢視控制器就是MPMoviePlayerViewController,它繼承於UIViewController。MPMoviePlayerViewController內部多了一個moviePlayer屬性和一個帶有url的初始化方法,同時它內部實現了一些作為模態檢視展示所特有的功能,例如預設是全屏模式展示、彈出後自動播放、作為模態視窗展示時如果點選“Done”按鈕會自動退出模態視窗等。在下面的示例中就不直接將播放器放到主檢視控制器,而是放到一個模態檢視控制器中,簡單演示MPMoviePlayerViewController的使用。
// // ViewController.m // MPMoviePlayerViewController // // Created by Kenshin Cui on 14/03/30. // Copyright (c) 2014年 cmjstudio. All rights reserved. // MPMoviePlayerViewController使用 #import "ViewController.h" #import <MediaPlayer/MediaPlayer.h> @interface ViewController () //播放器檢視控制器 @property (nonatomic,strong) MPMoviePlayerViewController *moviePlayerViewController; @end @implementation ViewController #pragma mark - 控制器檢視方法 - (void)viewDidLoad { [super viewDidLoad]; } -(void)dealloc{ //移除所有通知監控 [[NSNotificationCenter defaultCenter] removeObserver:self]; } #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; } /** * 取得網路檔案路徑 * * @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; } -(MPMoviePlayerViewController *)moviePlayerViewController{ if (!_moviePlayerViewController) { NSURL *url=[self getNetworkUrl]; _moviePlayerViewController=[[MPMoviePlayerViewController alloc]initWithContentURL:url]; [self addNotification]; } return _moviePlayerViewController; } #pragma mark - UI事件 - (IBAction)playClick:(UIButton *)sender { self.moviePlayerViewController=nil;//保證每次點選都重新建立視訊播放控制器檢視,避免再次點選時由於不播放的問題 // [self presentViewController:self.moviePlayerViewController animated:YES completion:nil]; //注意,在MPMoviePlayerViewController.h中對UIViewController擴充套件兩個用於模態展示和關閉MPMoviePlayerViewController的方法,增加了一種下拉展示動畫效果 [self presentMoviePlayerViewControllerAnimated:self.moviePlayerViewController]; } #pragma mark - 控制器通知 /** * 新增通知監控媒體播放控制器狀態 */ -(void)addNotification{ NSNotificationCenter *notificationCenter=[NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(mediaPlayerPlaybackStateChange:) name:MPMoviePlayerPlaybackStateDidChangeNotification object:self.moviePlayerViewController.moviePlayer]; [notificationCenter addObserver:self selector:@selector(mediaPlayerPlaybackFinished:) name:MPMoviePlayerPlaybackDidFinishNotification object:self.moviePlayerViewController.moviePlayer]; } /** * 播放狀態改變,注意播放完成時的狀態是暫停 * * @param notification 通知物件 */ -(void)mediaPlayerPlaybackStateChange:(NSNotification *)notification{ switch (self.moviePlayerViewController.moviePlayer.playbackState) { case MPMoviePlaybackStatePlaying: NSLog(@"正在播放..."); break; case MPMoviePlaybackStatePaused: NSLog(@"暫停播放."); break; case MPMoviePlaybackStateStopped: NSLog(@"停止播放."); break; default: NSLog(@"播放狀態:%li",self.moviePlayerViewController.moviePlayer.playbackState); break; } } /** * 播放完成 * * @param notification 通知物件 */ -(void)mediaPlayerPlaybackFinished:(NSNotification *)notification{ NSLog(@"播放完成.%li",self.moviePlayerViewController.moviePlayer.playbackState); } @end
執行效果:
這裡需要強調一下,由於MPMoviePlayerViewController的初始化方法做了大量工作(例如設定URL、自動播放、新增點選Done完成的監控等),所以當再次點選播放彈出新的模態視窗的時如果不銷燬之前的MPMoviePlayerViewController,那麼新的物件就無法完成初始化,這樣也就不能再次進行播放。
AVPlayer
MPMoviePlayerController足夠強大,幾乎不用寫幾行程式碼就能完成一個播放器,但是正是由於它的高度封裝使得要自定義這個播放器變得很複雜,甚至是不可能完成。例如有些時候需要自定義播放器的樣式,那麼如果要使用MPMoviePlayerController就不合適了,如果要對視訊有自由的控制則可以使用AVPlayer。AVPlayer存在於AVFoundation中,它更加接近於底層,所以靈活性也更強:
AVPlayer本身並不能顯示視訊,而且它也不像MPMoviePlayerController有一個view屬性。如果AVPlayer要顯示必須建立一個播放器層AVPlayerLayer用於展示,播放器層繼承於CALayer,有了AVPlayerLayer之新增到控制器檢視的layer中即可。要使用AVPlayer首先了解一下幾個常用的類:
AVAsset:主要用於獲取多媒體資訊,是一個抽象類,不能直接使用。
AVURLAsset:AVAsset的子類,可以根據一個URL路徑建立一個包含媒體資訊的AVURLAsset物件。
AVPlayerItem:一個媒體資源管理物件,管理者視訊的一些基本資訊和狀態,一個AVPlayerItem對應著一個視訊資源。
下面簡單通過一個播放器來演示AVPlayer的使用,播放器的效果如下:
在這個自定義的播放器中實現了視訊播放、暫停、進度展示和視訊列表功能,下面將對這些功能一一介紹。
首先說一下視訊的播放、暫停功能,這也是最基本的功能,AVPlayer對應著兩個方法play、pause來實現。但是關鍵問題是如何判斷當前視訊是否在播放,在前面的內容中無論是音訊播放器還是視訊播放器都有對應的狀態來判斷,但是AVPlayer卻沒有這樣的狀態屬性,通常情況下可以通過判斷播放器的播放速度來獲得播放狀態。如果rate為0說明是停止狀態,1是則是正常播放狀態。
其次要展示播放進度就沒有其他播放器那麼簡單了。在前面的播放器中通常是使用通知來獲得播放器的狀態,媒體載入狀態等,但是無論是AVPlayer還是AVPlayerItem(AVPlayer有一個屬性currentItem是AVPlayerItem型別,表示當前播放的視訊物件)都無法獲得這些資訊。當然AVPlayerItem是有通知的,但是對於獲得播放狀態和載入狀態有用的通知只有一個:播放完成通知AVPlayerItemDidPlayToEndTimeNotification。在播放視訊時,特別是播放網路視訊往往需要知道視訊載入情況、緩衝情況、播放情況,這些資訊可以通過KVO監控AVPlayerItem的status、loadedTimeRanges屬性來獲得。當AVPlayerItem的status屬性為AVPlayerStatusReadyToPlay是說明正在播放,只有處於這個狀態時才能獲得視訊時長等資訊;當loadedTimeRanges的改變時(每緩衝一部分資料就會更新此屬性)可以獲得本次緩衝載入的視訊範圍(包含起始時間、本次載入時長),這樣一來就可以實時獲得緩衝情況。然後就是依靠AVPlayer的- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block方法獲得播放進度,這個方法會在設定的時間間隔內定時更新播放進度,通過time引數通知客戶端。相信有了這些視訊資訊播放進度就不成問題了,事實上通過這些資訊就算是平時看到的其他播放器的緩衝進度顯示以及拖動播放的功能也可以順利的實現。
最後就是視訊切換的功能,在前面介紹的所有播放器中每個播放器物件一次只能播放一個視訊,如果要切換視訊只能重新建立一個物件,但是AVPlayer卻提供了- (void)replaceCurrentItemWithPlayerItem:(AVPlayerItem *)item方法用於在不同的視訊之間切換(事實上在AVFoundation內部還有一個AVQueuePlayer專門處理播放列表切換,有興趣的朋友可以自行研究,這裡不再贅述)。
下面附上程式碼:
// // ViewController.m // AVPlayer // // Created by Kenshin Cui on 14/03/30. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "ViewController.h" #import <AVFoundation/AVFoundation.h> @interface ViewController () @property (nonatomic,strong) AVPlayer *player;//播放器物件 @property (weak, nonatomic) IBOutlet UIView *container; //播放器容器 @property (weak, nonatomic) IBOutlet UIButton *playOrPause; //播放/暫停按鈕 @property (weak, nonatomic) IBOutlet UIProgressView *progress;//播放進度 @end @implementation ViewController #pragma mark - 控制器檢視方法 - (void)viewDidLoad { [super viewDidLoad]; [self setupUI]; [self.player play]; } -(void)dealloc{ [self removeObserverFromPlayerItem:self.player.currentItem]; [self removeNotification]; } #pragma mark - 私有方法 -(void)setupUI{ //建立播放器層 AVPlayerLayer *playerLayer=[AVPlayerLayer playerLayerWithPlayer:self.player]; playerLayer.frame=self.container.frame; //playerLayer.videoGravity=AVLayerVideoGravityResizeAspect;//視訊填充模式 [self.container.layer addSublayer:playerLayer]; } /** * 擷取指定時間的視訊縮圖 * * @param timeBySecond 時間點 */ /** * 初始化播放器 * * @return 播放器物件 */ -(AVPlayer *)player{ if (!_player) { AVPlayerItem *playerItem=[self getPlayItem:0]; _player=[AVPlayer playerWithPlayerItem:playerItem]; [self addProgressObserver]; [self addObserverToPlayerItem:playerItem]; } return _player; } /** * 根據視訊索引取得AVPlayerItem物件 * * @param videoIndex 視訊順序索引 * * @return AVPlayerItem物件 */ -(AVPlayerItem *)getPlayItem:(int)videoIndex{ NSString *urlStr=[NSString stringWithFormat:@"http://192.168.1.161/%i.mp4",videoIndex]; urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL *url=[NSURL URLWithString:urlStr]; AVPlayerItem *playerItem=[AVPlayerItem playerItemWithURL:url]; return playerItem; } #pragma mark - 通知 /** * 新增播放器通知 */ -(void)addNotification{ //給AVPlayerItem新增播放完成通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem]; } -(void)removeNotification{ [[NSNotificationCenter defaultCenter] removeObserver:self]; } /** * 播放完成通知 * * @param notification 通知物件 */ -(void)playbackFinished:(NSNotification *)notification{ NSLog(@"視訊播放完成."); } #pragma mark - 監控 /** * 給播放器新增進度更新 */ -(void)addProgressObserver{ AVPlayerItem *playerItem=self.player.currentItem; UIProgressView *progress=self.progress; //這裡設定每秒執行一次 [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { float current=CMTimeGetSeconds(time); float total=CMTimeGetSeconds([playerItem duration]); NSLog(@"當前已經播放%.2fs.",current); if (current) { [progress setProgress:(current/total) animated:YES]; } }]; } /** * 給AVPlayerItem新增監控 * * @param playerItem AVPlayerItem物件 */ -(void)addObserverToPlayerItem:(AVPlayerItem *)playerItem{ //監控狀態屬性,注意AVPlayer也有一個status屬性,通過監控它的status也可以獲得播放狀態 [playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil]; //監控網路載入情況屬性 [playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil]; } -(void)removeObserverFromPlayerItem:(AVPlayerItem *)playerItem{ [playerItem removeObserver:self forKeyPath:@"status"]; [playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"]; } /** * 通過KVO監控播放器狀態 * * @param keyPath 監控屬性 * @param object 監視器 * @param change 狀態改變 * @param context 上下文 */ -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ AVPlayerItem *playerItem=object; if ([keyPath isEqualToString:@"status"]) { AVPlayerStatus status= [[change objectForKey:@"new"] intValue]; if(status==AVPlayerStatusReadyToPlay){ NSLog(@"正在播放...,視訊總長度:%.2f",CMTimeGetSeconds(playerItem.duration)); } }else if([keyPath isEqualToString:@"loadedTimeRanges"]){ NSArray *array=playerItem.loadedTimeRanges; CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];//本次緩衝時間範圍 float startSeconds = CMTimeGetSeconds(timeRange.start); float durationSeconds = CMTimeGetSeconds(timeRange.duration); NSTimeInterval totalBuffer = startSeconds + durationSeconds;//緩衝總長度 NSLog(@"共緩衝:%.2f",totalBuffer); // } } #pragma mark - UI事件 /** * 點選播放/暫停按鈕 * * @param sender 播放/暫停按鈕 */ - (IBAction)playClick:(UIButton *)sender { // AVPlayerItemDidPlayToEndTimeNotification //AVPlayerItem *playerItem= self.player.currentItem; if(self.player.rate==0){ //說明時暫停 [sender setImage:[UIImage imageNamed:@"player_pause"] forState:UIControlStateNormal]; [self.player play]; }else if(self.player.rate==1){//正在播放 [self.player pause]; [sender setImage:[UIImage imageNamed:@"player_play"] forState:UIControlStateNormal]; } } /** * 切換選集,這裡使用按鈕的tag代表視訊名稱 * * @param sender 點選按鈕物件 */ - (IBAction)navigationButtonClick:(UIButton *)sender { [self removeNotification]; [self removeObserverFromPlayerItem:self.player.currentItem]; AVPlayerItem *playerItem=[self getPlayItem:sender.tag]; [self addObserverToPlayerItem:playerItem]; //切換視訊 [self.player replaceCurrentItemWithPlayerItem:playerItem]; [self addNotification]; } @end
執行效果:
到目前為止無論是MPMoviePlayerController還是AVPlayer來播放視訊都相當強大,但是它也存在著一些不可迴避的問題,那就是支援的視訊編碼格式很有限:H.264、MPEG-4,副檔名(壓縮格式):.mp4、.mov、.m4v、.m2v、.3gp、.3g2等。但是無論是MPMoviePlayerController還是AVPlayer它們都支援絕大多數音訊編碼,所以大家如果純粹是為了播放音樂的話也可以考慮使用這兩個播放器。那麼如何支援更多視訊編碼格式呢?目前來說主要還是依靠第三方框架,在iOS上常用的視訊編碼、解碼框架有:VLC、ffmpeg, 具體使用方式今天就不再做詳細介紹。
攝像頭
UIImagePickerController拍照和視訊錄製
下面看一下在iOS如何拍照和錄製視訊。在iOS中要拍照和錄製視訊最簡單的方法就是使用UIImagePickerController。UIImagePickerController繼承於UINavigationController,前面的文章中主要使用它來選取照片,其實UIImagePickerController的功能不僅如此,它還可以用來拍照和錄製視訊。首先看一下這個類常用的屬性和方法:
屬性 | 說明 |
@property(nonatomic) UIImagePickerControllerSourceType sourceType | 拾取源型別,sourceType是列舉型別: UIImagePickerControllerSourceTypePhotoLibrary:照片庫 ,預設值 UIImagePickerControllerSourceTypeCamera:攝像頭 UIImagePickerControllerSourceTypeSavedPhotosAlbum:相簿 |
@property(nonatomic,copy) NSArray *mediaTypes | 媒體型別,預設情況下此陣列包含kUTTypeImage,所以拍照時可以不用設定;但是當要錄影的時候必須設定,可以設定為kUTTypeVideo(視訊,但不帶聲音)或者kUTTypeMovie(視訊並帶有聲音) |
@property(nonatomic) NSTimeInterval videoMaximumDuration | 視訊最大錄製時長,預設為10 s |
@property(nonatomic) UIImagePickerControllerQualityType videoQuality | 視訊質量,列舉型別: UIImagePickerControllerQualityTypeHigh:高清質量 UIImagePickerControllerQualityTypeMedium:中等質量,適合WiFi傳輸 UIImagePickerControllerQualityTypeLow:低質量,適合蜂窩網傳輸 UIImagePickerControllerQualityType640x480:640*480 UIImagePickerControllerQualityTypeIFrame1280x720:1280*720 UIImagePickerControllerQualityTypeIFrame960x540:960*540 |
@property(nonatomic) BOOL showsCameraControls | 是否顯示攝像頭控制皮膚,預設為YES |
@property(nonatomic,retain) UIView *cameraOverlayView | 攝像頭上覆蓋的檢視,可用通過這個視訊來自定義拍照或錄影介面 |
@property(nonatomic) CGAffineTransform cameraViewTransform | 攝像頭形變 |
@property(nonatomic) UIImagePickerControllerCameraCaptureMode cameraCaptureMode | 攝像頭捕獲模式,捕獲模式是列舉型別: UIImagePickerControllerCameraCaptureModePhoto:拍照模式 UIImagePickerControllerCameraCaptureModeVideo:視訊錄製模式 |
@property(nonatomic) UIImagePickerControllerCameraDevice cameraDevice | 攝像頭裝置,cameraDevice是列舉型別: UIImagePickerControllerCameraDeviceRear:前置攝像頭 UIImagePickerControllerCameraDeviceFront:後置攝像頭 |
@property(nonatomic) UIImagePickerControllerCameraFlashMode cameraFlashMode | 閃光燈模式,列舉型別: UIImagePickerControllerCameraFlashModeOff:關閉閃光燈 UIImagePickerControllerCameraFlashModeAuto:閃光燈自動 UIImagePickerControllerCameraFlashModeOn:開啟閃光燈 |
類方法 | 說明 |
+ (BOOL)isSourceTypeAvailable:(UIImagePickerControllerSourceType)sourceType | 指定的源型別是否可用,sourceType是列舉型別: UIImagePickerControllerSourceTypePhotoLibrary:照片庫 UIImagePickerControllerSourceTypeCamera:攝像頭 UIImagePickerControllerSourceTypeSavedPhotosAlbum:相簿 |
+ (NSArray *)availableMediaTypesForSourceType:(UIImagePickerControllerSourceType)sourceType | 指定的源裝置上可用的媒體型別,一般就是圖片和視訊 |
+ (BOOL)isCameraDeviceAvailable:(UIImagePickerControllerCameraDevice)cameraDevice | 指定的攝像頭是否可用,cameraDevice是列舉型別: UIImagePickerControllerCameraDeviceRear:前置攝像頭 UIImagePickerControllerCameraDeviceFront:後置攝像頭 |
+ (BOOL)isFlashAvailableForCameraDevice:(UIImagePickerControllerCameraDevice)cameraDevice | 指定攝像頭的閃光燈是否可用 |
+ (NSArray *)availableCaptureModesForCameraDevice:(UIImagePickerControllerCameraDevice)cameraDevice | 獲得指定攝像頭上的可用捕獲模式,捕獲模式是列舉型別: UIImagePickerControllerCameraCaptureModePhoto:拍照模式 UIImagePickerControllerCameraCaptureModeVideo:視訊錄製模式 |
物件方法 | 說明 |
- (void)takePicture | 程式設計方式拍照 |
- (BOOL)startVideoCapture | 程式設計方式錄製視訊 |
- (void)stopVideoCapture | 程式設計方式停止錄製視訊 |
代理方法 | 說明 |
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info | 媒體拾取完成 |
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker | 取消拾取 |
擴充套件方法(主要用於儲存照片、視訊到相簿) | 說明 |
UIImageWriteToSavedPhotosAlbum(UIImage *image, id completionTarget, SEL completionSelector, void *contextInfo) | 儲存照片到相簿 |
UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(NSString *videoPath) | 能否將視訊儲存到相簿 |
void UISaveVideoAtPathToSavedPhotosAlbum(NSString *videoPath, id completionTarget, SEL completionSelector, void *contextInfo) | 儲存視訊到相簿 |
要用UIImagePickerController來拍照或者錄製視訊通常可以分為如下步驟:
- 建立UIImagePickerController物件。
- 指定拾取源,平時選擇照片時使用的拾取源是照片庫或者相簿,此刻需要指定為攝像頭型別。
- 指定攝像頭,前置攝像頭或者後置攝像頭。
- 設定媒體型別mediaType,注意如果是錄影必須設定,如果是拍照此步驟可以省略,因為mediaType預設包含kUTTypeImage(注意媒體型別定義在MobileCoreServices.framework中)
- 指定捕獲模式,拍照或者錄製視訊。(視訊錄製時必須先設定媒體型別再設定捕獲模式
- )
- 展示UIImagePickerController(通常以模態視窗形式開啟)。
- 拍照和錄製視訊結束後在代理方法中展示/儲存照片或視訊。
當然這個過程中有很多細節可以設定,例如是否顯示拍照控制皮膚,拍照後是否允許編輯等等,通過上面的屬性/方法列表相信並不難理解。下面就以一個示例展示如何使用UIImagePickerController來拍照和錄製視訊,下面的程式中只要將_isVideo設定為YES就是視訊錄製模式,錄製完後在主檢視控制器中自動播放;如果將_isVideo設定為NO則為拍照模式,拍照完成之後在主檢視控制器中顯示拍攝的照片:
// // ViewController.m // UIImagePickerController // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "ViewController.h" #import <MobileCoreServices/MobileCoreServices.h> #import <AVFoundation/AVFoundation.h> @interface ViewController ()<UIImagePickerControllerDelegate,UINavigationControllerDelegate> @property (assign,nonatomic) int isVideo;//是否錄製視訊,如果為1表示錄製視訊,0代表拍照 @property (strong,nonatomic) UIImagePickerController *imagePicker; @property (weak, nonatomic) IBOutlet UIImageView *photo;//照片展示檢視 @property (strong ,nonatomic) AVPlayer *player;//播放器,用於錄製完視訊後播放視訊 @end @implementation ViewController #pragma mark - 控制器檢視事件 - (void)viewDidLoad { [super viewDidLoad]; //通過這裡設定當前程式是拍照還是錄製視訊 _isVideo=YES; } #pragma mark - UI事件 //點選拍照按鈕 - (IBAction)takeClick:(UIButton *)sender { [self presentViewController:self.imagePicker animated:YES completion:nil]; } #pragma mark - UIImagePickerController代理方法 //完成 -(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{ NSString *mediaType=[info objectForKey:UIImagePickerControllerMediaType]; if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) {//如果是拍照 UIImage *image; //如果允許編輯則獲得編輯後的照片,否則獲取原始照片 if (self.imagePicker.allowsEditing) { image=[info objectForKey:UIImagePickerControllerEditedImage];//獲取編輯後的照片 }else{ image=[info objectForKey:UIImagePickerControllerOriginalImage];//獲取原始照片 } [self.photo setImage:image];//顯示照片 UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);//儲存到相簿 }else if([mediaType isEqualToString:(NSString *)kUTTypeMovie]){//如果是錄製視訊 NSLog(@"video..."); NSURL *url=[info objectForKey:UIImagePickerControllerMediaURL];//視訊路徑 NSString *urlStr=[url path]; if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(urlStr)) { //儲存視訊到相簿,注意也可以使用ALAssetsLibrary來儲存 UISaveVideoAtPathToSavedPhotosAlbum(urlStr, self, @selector(video:didFinishSavingWithError:contextInfo:), nil);//儲存視訊到相簿 } } [self dismissViewControllerAnimated:YES completion:nil]; } -(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{ NSLog(@"取消"); } #pragma mark - 私有方法 -(UIImagePickerController *)imagePicker{ if (!_imagePicker) { _imagePicker=[[UIImagePickerController alloc]init]; _imagePicker.sourceType=UIImagePickerControllerSourceTypeCamera;//設定image picker的來源,這裡設定為攝像頭 _imagePicker.cameraDevice=UIImagePickerControllerCameraDeviceRear;//設定使用哪個攝像頭,這裡設定為後置攝像頭 if (self.isVideo) { _imagePicker.mediaTypes=@[(NSString *)kUTTypeMovie]; _imagePicker.videoQuality=UIImagePickerControllerQualityTypeIFrame1280x720; _imagePicker.cameraCaptureMode=UIImagePickerControllerCameraCaptureModeVideo;//設定攝像頭模式(拍照,錄製視訊) }else{ _imagePicker.cameraCaptureMode=UIImagePickerControllerCameraCaptureModePhoto; } _imagePicker.allowsEditing=YES;//允許編輯 _imagePicker.delegate=self;//設定代理,檢測操作 } return _imagePicker; } //視訊儲存後的回撥 - (void)video:(NSString *)videoPath didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo{ if (error) { NSLog(@"儲存視訊過程中發生錯誤,錯誤資訊:%@",error.localizedDescription); }else{ NSLog(@"視訊儲存成功."); //錄製完之後自動播放 NSURL *url=[NSURL fileURLWithPath:videoPath]; _player=[AVPlayer playerWithURL:url]; AVPlayerLayer *playerLayer=[AVPlayerLayer playerLayerWithPlayer:_player]; playerLayer.frame=self.photo.frame; [self.photo.layer addSublayer:playerLayer]; [_player play]; } } @end
執行效果(視訊錄製):
AVFoundation拍照和錄製視訊
不得不說UIImagePickerController確實強大,但是與MPMoviePlayerController類似,由於它的高度封裝性,要進行某些自定義工作就比較複雜了。例如要做出一款類似於美顏相機的拍照介面就比較難以實現了,此時就可以考慮使用AVFoundation來實現。AVFoundation中提供了很多現成的播放器和錄音機,但是事實上它還有更加底層的內容可以供開發者使用。因為AVFoundation中抽了很多和底層輸入、輸出裝置打交道的類,依靠這些類開發人員面對的不再是封裝好的音訊播放器AVAudioPlayer、錄音機(AVAudioRecorder)、視訊(包括音訊)播放器AVPlayer,而是輸入裝置(例如麥克風、攝像頭)、輸出裝置(圖片、視訊)等。首先了解一下使用AVFoundation做拍照和視訊錄製開發用到的相關類:
AVCaptureSession:媒體(音、視訊)捕獲會話,負責把捕獲的音視訊資料輸出到輸出裝置中。一個AVCaptureSession可以有多個輸入輸出:
AVCaptureDevice:輸入裝置,包括麥克風、攝像頭,通過該物件可以設定物理裝置的一些屬性(例如相機聚焦、白平衡等)。
AVCaptureDeviceInput:裝置輸入資料管理物件,可以根據AVCaptureDevice建立對應的AVCaptureDeviceInput物件,該物件將會被新增到AVCaptureSession中管理。
AVCaptureOutput:輸出資料管理物件,用於接收各類輸出資料,通常使用對應的子類AVCaptureAudioDataOutput、AVCaptureStillImageOutput、AVCaptureVideoDataOutput、AVCaptureFileOutput,該物件將會被新增到AVCaptureSession中管理。注意:前面幾個物件的輸出資料都是NSData型別,而AVCaptureFileOutput代表資料以檔案形式輸出,類似的,AVCcaptureFileOutput也不會直接建立使用,通常會使用其子類:AVCaptureAudioFileOutput、AVCaptureMovieFileOutput。當把一個輸入或者輸出新增到AVCaptureSession之後AVCaptureSession就會在所有相符的輸入、輸出裝置之間建立連線(AVCaptionConnection):
AVCaptureVideoPreviewLayer:相機拍攝預覽圖層,是CALayer的子類,使用該物件可以實時檢視拍照或視訊錄製效果,建立該物件需要指定對應的AVCaptureSession物件。
使用AVFoundation拍照和錄製視訊的一般步驟如下:
- 建立AVCaptureSession物件。
- 使用AVCaptureDevice的靜態方法獲得需要使用的裝置,例如拍照和錄影就需要獲得攝像頭裝置,錄音就要獲得麥克風裝置。
- 利用輸入裝置AVCaptureDevice初始化AVCaptureDeviceInput物件。
- 初始化輸出資料管理物件,如果要拍照就初始化AVCaptureStillImageOutput物件;如果拍攝視訊就初始化AVCaptureMovieFileOutput物件。
- 將資料輸入物件AVCaptureDeviceInput、資料輸出物件AVCaptureOutput新增到媒體會話管理物件AVCaptureSession中。
- 建立視訊預覽圖層AVCaptureVideoPreviewLayer並指定媒體會話,新增圖層到顯示容器中,呼叫AVCaptureSession的startRuning方法開始捕獲。
- 將捕獲的音訊或視訊資料輸出到指定檔案。
拍照
下面看一下如何使用AVFoundation實現一個拍照程式,在這個程式中將實現攝像頭預覽、切換前後攝像頭、閃光燈設定、對焦、拍照儲存等功能。應用大致效果如下:
在程式中定義會話、輸入、輸出等相關物件。
@interface ViewController () @property (strong,nonatomic) AVCaptureSession *captureSession;//負責輸入和輸出裝置之間的資料傳遞 @property (strong,nonatomic) AVCaptureDeviceInput *captureDeviceInput;//負責從AVCaptureDevice獲得輸入資料 @property (strong,nonatomic) AVCaptureStillImageOutput *captureStillImageOutput;//照片輸出流 @property (strong,nonatomic) AVCaptureVideoPreviewLayer *captureVideoPreviewLayer;//相機拍攝預覽圖層 @property (weak, nonatomic) IBOutlet UIView *viewContainer; @property (weak, nonatomic) IBOutlet UIButton *takeButton;//拍照按鈕 @property (weak, nonatomic) IBOutlet UIButton *flashAutoButton;//自動閃光燈按鈕 @property (weak, nonatomic) IBOutlet UIButton *flashOnButton;//開啟閃光燈按鈕 @property (weak, nonatomic) IBOutlet UIButton *flashOffButton;//關閉閃光燈按鈕 @property (weak, nonatomic) IBOutlet UIImageView *focusCursor; //聚焦游標 @end
在控制器檢視將要展示時建立並初始化會話、攝像頭裝置、輸入、輸出、預覽圖層,並且新增預覽圖層到檢視中,除此之外還做了一些初始化工作,例如新增手勢(點選螢幕進行聚焦)、初始化介面等。
-(void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; //初始化會話 _captureSession=[[AVCaptureSession alloc]init]; if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {//設定解析度 _captureSession.sessionPreset=AVCaptureSessionPreset1280x720; } //獲得輸入裝置 AVCaptureDevice *captureDevice=[self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];//取得後置攝像頭 if (!captureDevice) { NSLog(@"取得後置攝像頭時出現問題."); return; } NSError *error=nil; //根據輸入裝置初始化裝置輸入物件,用於獲得輸入資料 _captureDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:captureDevice error:&error]; if (error) { NSLog(@"取得裝置輸入物件時出錯,錯誤原因:%@",error.localizedDescription); return; } //初始化裝置輸出物件,用於獲得輸出資料 _captureStillImageOutput=[[AVCaptureStillImageOutput alloc]init]; NSDictionary *outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG}; [_captureStillImageOutput setOutputSettings:outputSettings];//輸出設定 //將裝置輸入新增到會話中 if ([_captureSession canAddInput:_captureDeviceInput]) { [_captureSession addInput:_captureDeviceInput]; } //將裝置輸出新增到會話中 if ([_captureSession canAddOutput:_captureStillImageOutput]) { [_captureSession addOutput:_captureStillImageOutput]; } //建立視訊預覽層,用於實時展示攝像頭狀態 _captureVideoPreviewLayer=[[AVCaptureVideoPreviewLayer alloc]initWithSession:self.captureSession]; CALayer *layer=self.viewContainer.layer; layer.masksToBounds=YES; _captureVideoPreviewLayer.frame=layer.bounds; _captureVideoPreviewLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//填充模式 //將視訊預覽層新增到介面中 //[layer addSublayer:_captureVideoPreviewLayer]; [layer insertSublayer:_captureVideoPreviewLayer below:self.focusCursor.layer]; [self addNotificationToCaptureDevice:captureDevice]; [self addGenstureRecognizer]; [self setFlashModeButtonStatus]; }
在控制器檢視展示和檢視離開介面時啟動、停止會話。
-(void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; [self.captureSession startRunning]; } -(void)viewDidDisappear:(BOOL)animated{ [super viewDidDisappear:animated]; [self.captureSession stopRunning]; }
定義閃光燈開閉及自動模式功能,注意無論是設定閃光燈、白平衡還是其他輸入裝置屬性,在設定之前必須先鎖定配置,修改完後解鎖。
/** * 改變裝置屬性的統一操作方法 * * @param propertyChange 屬性改變操作 */ -(void)changeDeviceProperty:(PropertyChangeBlock)propertyChange{ AVCaptureDevice *captureDevice= [self.captureDeviceInput device]; NSError *error; //注意改變裝置屬性前一定要首先呼叫lockForConfiguration:呼叫完之後使用unlockForConfiguration方法解鎖 if ([captureDevice lockForConfiguration:&error]) { propertyChange(captureDevice); [captureDevice unlockForConfiguration]; }else{ NSLog(@"設定裝置屬性過程發生錯誤,錯誤資訊:%@",error.localizedDescription); } } /** * 設定閃光燈模式 * * @param flashMode 閃光燈模式 */ -(void)setFlashMode:(AVCaptureFlashMode )flashMode{ [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) { if ([captureDevice isFlashModeSupported:flashMode]) { [captureDevice setFlashMode:flashMode]; } }]; }
定義切換攝像頭功能,切換攝像頭的過程就是將原有輸入移除,在會話中新增新的輸入,但是注意動態修改會話需要首先開啟配置,配置成功後提交配置。
#pragma mark 切換前後攝像頭 - (IBAction)toggleButtonClick:(UIButton *)sender { AVCaptureDevice *currentDevice=[self.captureDeviceInput device]; AVCaptureDevicePosition currentPosition=[currentDevice position]; [self removeNotificationFromCaptureDevice:currentDevice]; AVCaptureDevice *toChangeDevice; AVCaptureDevicePosition toChangePosition=AVCaptureDevicePositionFront; if (currentPosition==AVCaptureDevicePositionUnspecified||currentPosition==AVCaptureDevicePositionFront) { toChangePosition=AVCaptureDevicePositionBack; } toChangeDevice=[self getCameraDeviceWithPosition:toChangePosition]; [self addNotificationToCaptureDevice:toChangeDevice]; //獲得要調整的裝置輸入物件 AVCaptureDeviceInput *toChangeDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:toChangeDevice error:nil]; //改變會話的配置前一定要先開啟配置,配置完成後提交配置改變 [self.captureSession beginConfiguration]; //移除原有輸入物件 [self.captureSession removeInput:self.captureDeviceInput]; //新增新的輸入物件 if ([self.captureSession canAddInput:toChangeDeviceInput]) { [self.captureSession addInput:toChangeDeviceInput]; self.captureDeviceInput=toChangeDeviceInput; } //提交會話配置 [self.captureSession commitConfiguration]; [self setFlashModeButtonStatus]; }
新增點選手勢操作,點按預覽檢視時進行聚焦、白平衡設定。
/** * 設定聚焦點 * * @param point 聚焦點 */ -(void)focusWithMode:(AVCaptureFocusMode)focusMode exposureMode:(AVCaptureExposureMode)exposureMode atPoint:(CGPoint)point{ [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) { if ([captureDevice isFocusModeSupported:focusMode]) { [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus]; } if ([captureDevice isFocusPointOfInterestSupported]) { [captureDevice setFocusPointOfInterest:point]; } if ([captureDevice isExposureModeSupported:exposureMode]) { [captureDevice setExposureMode:AVCaptureExposureModeAutoExpose]; } if ([captureDevice isExposurePointOfInterestSupported]) { [captureDevice setExposurePointOfInterest:point]; } }]; } /** * 新增點按手勢,點按時聚焦 */ -(void)addGenstureRecognizer{ UITapGestureRecognizer *tapGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapScreen:)]; [self.viewContainer addGestureRecognizer:tapGesture]; } -(void)tapScreen:(UITapGestureRecognizer *)tapGesture{ CGPoint point= [tapGesture locationInView:self.viewContainer]; //將UI座標轉化為攝像頭座標 CGPoint cameraPoint= [self.captureVideoPreviewLayer captureDevicePointOfInterestForPoint:point]; [self setFocusCursorWithPoint:point]; [self focusWithMode:AVCaptureFocusModeAutoFocus exposureMode:AVCaptureExposureModeAutoExpose atPoint:cameraPoint]; }
定義拍照功能,拍照的過程就是獲取連線,從連線中獲得捕獲的輸出資料並做儲存操作。
#pragma mark 拍照 - (IBAction)takeButtonClick:(UIButton *)sender { //根據裝置輸出獲得連線 AVCaptureConnection *captureConnection=[self.captureStillImageOutput connectionWithMediaType:AVMediaTypeVideo]; //根據連線取得裝置輸出的資料 [self.captureStillImageOutput captureStillImageAsynchronouslyFromConnection:captureConnection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) { if (imageDataSampleBuffer) { NSData *imageData=[AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer]; UIImage *image=[UIImage imageWithData:imageData]; UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); // ALAssetsLibrary *assetsLibrary=[[ALAssetsLibrary alloc]init]; // [assetsLibrary writeImageToSavedPhotosAlbum:[image CGImage] orientation:(ALAssetOrientation)[image imageOrientation] completionBlock:nil]; } }]; }
最後附上完整程式碼:
// // ViewController.m // AVFoundationCamera // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "ViewController.h" #import <AVFoundation/AVFoundation.h> #import <AssetsLibrary/AssetsLibrary.h> typedef void(^PropertyChangeBlock)(AVCaptureDevice *captureDevice); @interface ViewController () @property (strong,nonatomic) AVCaptureSession *captureSession;//負責輸入和輸出裝置之間的資料傳遞 @property (strong,nonatomic) AVCaptureDeviceInput *captureDeviceInput;//負責從AVCaptureDevice獲得輸入資料 @property (strong,nonatomic) AVCaptureStillImageOutput *captureStillImageOutput;//照片輸出流 @property (strong,nonatomic) AVCaptureVideoPreviewLayer *captureVideoPreviewLayer;//相機拍攝預覽圖層 @property (weak, nonatomic) IBOutlet UIView *viewContainer; @property (weak, nonatomic) IBOutlet UIButton *takeButton;//拍照按鈕 @property (weak, nonatomic) IBOutlet UIButton *flashAutoButton;//自動閃光燈按鈕 @property (weak, nonatomic) IBOutlet UIButton *flashOnButton;//開啟閃光燈按鈕 @property (weak, nonatomic) IBOutlet UIButton *flashOffButton;//關閉閃光燈按鈕 @property (weak, nonatomic) IBOutlet UIImageView *focusCursor; //聚焦游標 @end @implementation ViewController #pragma mark - 控制器檢視方法 - (void)viewDidLoad { [super viewDidLoad]; } -(void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; //初始化會話 _captureSession=[[AVCaptureSession alloc]init]; if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {//設定解析度 _captureSession.sessionPreset=AVCaptureSessionPreset1280x720; } //獲得輸入裝置 AVCaptureDevice *captureDevice=[self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];//取得後置攝像頭 if (!captureDevice) { NSLog(@"取得後置攝像頭時出現問題."); return; } NSError *error=nil; //根據輸入裝置初始化裝置輸入物件,用於獲得輸入資料 _captureDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:captureDevice error:&error]; if (error) { NSLog(@"取得裝置輸入物件時出錯,錯誤原因:%@",error.localizedDescription); return; } //初始化裝置輸出物件,用於獲得輸出資料 _captureStillImageOutput=[[AVCaptureStillImageOutput alloc]init]; NSDictionary *outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG}; [_captureStillImageOutput setOutputSettings:outputSettings];//輸出設定 //將裝置輸入新增到會話中 if ([_captureSession canAddInput:_captureDeviceInput]) { [_captureSession addInput:_captureDeviceInput]; } //將裝置輸出新增到會話中 if ([_captureSession canAddOutput:_captureStillImageOutput]) { [_captureSession addOutput:_captureStillImageOutput]; } //建立視訊預覽層,用於實時展示攝像頭狀態 _captureVideoPreviewLayer=[[AVCaptureVideoPreviewLayer alloc]initWithSession:self.captureSession]; CALayer *layer=self.viewContainer.layer; layer.masksToBounds=YES; _captureVideoPreviewLayer.frame=layer.bounds; _captureVideoPreviewLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//填充模式 //將視訊預覽層新增到介面中 //[layer addSublayer:_captureVideoPreviewLayer]; [layer insertSublayer:_captureVideoPreviewLayer below:self.focusCursor.layer]; [self addNotificationToCaptureDevice:captureDevice]; [self addGenstureRecognizer]; [self setFlashModeButtonStatus]; } -(void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; [self.captureSession startRunning]; } -(void)viewDidDisappear:(BOOL)animated{ [super viewDidDisappear:animated]; [self.captureSession stopRunning]; } -(void)dealloc{ [self removeNotification]; } #pragma mark - UI方法 #pragma mark 拍照 - (IBAction)takeButtonClick:(UIButton *)sender { //根據裝置輸出獲得連線 AVCaptureConnection *captureConnection=[self.captureStillImageOutput connectionWithMediaType:AVMediaTypeVideo]; //根據連線取得裝置輸出的資料 [self.captureStillImageOutput captureStillImageAsynchronouslyFromConnection:captureConnection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) { if (imageDataSampleBuffer) { NSData *imageData=[AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer]; UIImage *image=[UIImage imageWithData:imageData]; UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); // ALAssetsLibrary *assetsLibrary=[[ALAssetsLibrary alloc]init]; // [assetsLibrary writeImageToSavedPhotosAlbum:[image CGImage] orientation:(ALAssetOrientation)[image imageOrientation] completionBlock:nil]; } }]; } #pragma mark 切換前後攝像頭 - (IBAction)toggleButtonClick:(UIButton *)sender { AVCaptureDevice *currentDevice=[self.captureDeviceInput device]; AVCaptureDevicePosition currentPosition=[currentDevice position]; [self removeNotificationFromCaptureDevice:currentDevice]; AVCaptureDevice *toChangeDevice; AVCaptureDevicePosition toChangePosition=AVCaptureDevicePositionFront; if (currentPosition==AVCaptureDevicePositionUnspecified||currentPosition==AVCaptureDevicePositionFront) { toChangePosition=AVCaptureDevicePositionBack; } toChangeDevice=[self getCameraDeviceWithPosition:toChangePosition]; [self addNotificationToCaptureDevice:toChangeDevice]; //獲得要調整的裝置輸入物件 AVCaptureDeviceInput *toChangeDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:toChangeDevice error:nil]; //改變會話的配置前一定要先開啟配置,配置完成後提交配置改變 [self.captureSession beginConfiguration]; //移除原有輸入物件 [self.captureSession removeInput:self.captureDeviceInput]; //新增新的輸入物件 if ([self.captureSession canAddInput:toChangeDeviceInput]) { [self.captureSession addInput:toChangeDeviceInput]; self.captureDeviceInput=toChangeDeviceInput; } //提交會話配置 [self.captureSession commitConfiguration]; [self setFlashModeButtonStatus]; } #pragma mark 自動閃光燈開啟 - (IBAction)flashAutoClick:(UIButton *)sender { [self setFlashMode:AVCaptureFlashModeAuto]; [self setFlashModeButtonStatus]; } #pragma mark 開啟閃光燈 - (IBAction)flashOnClick:(UIButton *)sender { [self setFlashMode:AVCaptureFlashModeOn]; [self setFlashModeButtonStatus]; } #pragma mark 關閉閃光燈 - (IBAction)flashOffClick:(UIButton *)sender { [self setFlashMode:AVCaptureFlashModeOff]; [self setFlashModeButtonStatus]; } #pragma mark - 通知 /** * 給輸入裝置新增通知 */ -(void)addNotificationToCaptureDevice:(AVCaptureDevice *)captureDevice{ //注意新增區域改變捕獲通知必須首先設定裝置允許捕獲 [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) { captureDevice.subjectAreaChangeMonitoringEnabled=YES; }]; NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter]; //捕獲區域發生改變 [notificationCenter addObserver:self selector:@selector(areaChange:) name:AVCaptureDeviceSubjectAreaDidChangeNotification object:captureDevice]; } -(void)removeNotificationFromCaptureDevice:(AVCaptureDevice *)captureDevice{ NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter]; [notificationCenter removeObserver:self name:AVCaptureDeviceSubjectAreaDidChangeNotification object:captureDevice]; } /** * 移除所有通知 */ -(void)removeNotification{ NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter]; [notificationCenter removeObserver:self]; } -(void)addNotificationToCaptureSession:(AVCaptureSession *)captureSession{ NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter]; //會話出錯 [notificationCenter addObserver:self selector:@selector(sessionRuntimeError:) name:AVCaptureSessionRuntimeErrorNotification object:captureSession]; } /** * 裝置連線成功 * * @param notification 通知物件 */ -(void)deviceConnected:(NSNotification *)notification{ NSLog(@"裝置已連線..."); } /** * 裝置連線斷開 * * @param notification 通知物件 */ -(void)deviceDisconnected:(NSNotification *)notification{ NSLog(@"裝置已斷開."); } /** * 捕獲區域改變 * * @param notification 通知物件 */ -(void)areaChange:(NSNotification *)notification{ NSLog(@"捕獲區域改變..."); } /** * 會話出錯 * * @param notification 通知物件 */ -(void)sessionRuntimeError:(NSNotification *)notification{ NSLog(@"會話發生錯誤."); } #pragma mark - 私有方法 /** * 取得指定位置的攝像頭 * * @param position 攝像頭位置 * * @return 攝像頭裝置 */ -(AVCaptureDevice *)getCameraDeviceWithPosition:(AVCaptureDevicePosition )position{ NSArray *cameras= [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; for (AVCaptureDevice *camera in cameras) { if ([camera position]==position) { return camera; } } return nil; } /** * 改變裝置屬性的統一操作方法 * * @param propertyChange 屬性改變操作 */ -(void)changeDeviceProperty:(PropertyChangeBlock)propertyChange{ AVCaptureDevice *captureDevice= [self.captureDeviceInput device]; NSError *error; //注意改變裝置屬性前一定要首先呼叫lockForConfiguration:呼叫完之後使用unlockForConfiguration方法解鎖 if ([captureDevice lockForConfiguration:&error]) { propertyChange(captureDevice); [captureDevice unlockForConfiguration]; }else{ NSLog(@"設定裝置屬性過程發生錯誤,錯誤資訊:%@",error.localizedDescription); } } /** * 設定閃光燈模式 * * @param flashMode 閃光燈模式 */ -(void)setFlashMode:(AVCaptureFlashMode )flashMode{ [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) { if ([captureDevice isFlashModeSupported:flashMode]) { [captureDevice setFlashMode:flashMode]; } }]; } /** * 設定聚焦模式 * * @param focusMode 聚焦模式 */ -(void)setFocusMode:(AVCaptureFocusMode )focusMode{ [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) { if ([captureDevice isFocusModeSupported:focusMode]) { [captureDevice setFocusMode:focusMode]; } }]; } /** * 設定曝光模式 * * @param exposureMode 曝光模式 */ -(void)setExposureMode:(AVCaptureExposureMode)exposureMode{ [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) { if ([captureDevice isExposureModeSupported:exposureMode]) { [captureDevice setExposureMode:exposureMode]; } }]; } /** * 設定聚焦點 * * @param point 聚焦點 */ -(void)focusWithMode:(AVCaptureFocusMode)focusMode exposureMode:(AVCaptureExposureMode)exposureMode atPoint:(CGPoint)point{ [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) { if ([captureDevice isFocusModeSupported:focusMode]) { [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus]; } if ([captureDevice isFocusPointOfInterestSupported]) { [captureDevice setFocusPointOfInterest:point]; } if ([captureDevice isExposureModeSupported:exposureMode]) { [captureDevice setExposureMode:AVCaptureExposureModeAutoExpose]; } if ([captureDevice isExposurePointOfInterestSupported]) { [captureDevice setExposurePointOfInterest:point]; } }]; } /** * 新增點按手勢,點按時聚焦 */ -(void)addGenstureRecognizer{ UITapGestureRecognizer *tapGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapScreen:)]; [self.viewContainer addGestureRecognizer:tapGesture]; } -(void)tapScreen:(UITapGestureRecognizer *)tapGesture{ CGPoint point= [tapGesture locationInView:self.viewContainer]; //將UI座標轉化為攝像頭座標 CGPoint cameraPoint= [self.captureVideoPreviewLayer captureDevicePointOfInterestForPoint:point]; [self setFocusCursorWithPoint:point]; [self focusWithMode:AVCaptureFocusModeAutoFocus exposureMode:AVCaptureExposureModeAutoExpose atPoint:cameraPoint]; } /** * 設定閃光燈按鈕狀態 */ -(void)setFlashModeButtonStatus{ AVCaptureDevice *captureDevice=[self.captureDeviceInput device]; AVCaptureFlashMode flashMode=captureDevice.flashMode; if([captureDevice isFlashAvailable]){ self.flashAutoButton.hidden=NO; self.flashOnButton.hidden=NO; self.flashOffButton.hidden=NO; self.flashAutoButton.enabled=YES; self.flashOnButton.enabled=YES; self.flashOffButton.enabled=YES; switch (flashMode) { case AVCaptureFlashModeAuto: self.flashAutoButton.enabled=NO; break; case AVCaptureFlashModeOn: self.flashOnButton.enabled=NO; break; case AVCaptureFlashModeOff: self.flashOffButton.enabled=NO; break; default: break; } }else{ self.flashAutoButton.hidden=YES; self.flashOnButton.hidden=YES; self.flashOffButton.hidden=YES; } } /** * 設定聚焦游標位置 * * @param point 游標位置 */ -(void)setFocusCursorWithPoint:(CGPoint)point{ self.focusCursor.center=point; self.focusCursor.transform=CGAffineTransformMakeScale(1.5, 1.5); self.focusCursor.alpha=1.0; [UIView animateWithDuration:1.0 animations:^{ self.focusCursor.transform=CGAffineTransformIdentity; } completion:^(BOOL finished) { self.focusCursor.alpha=0; }]; } @end
執行效果:
視訊錄製
其實有了前面的拍照應用之後要在此基礎上做視訊錄製功能並不複雜,程式只需要做如下修改:
- 新增一個音訊輸入到會話(使用[[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] firstObject]獲得輸入裝置,然後根據此輸入裝置建立一個裝置輸入物件),在拍照程式中已經新增了視訊輸入所以此時不需要新增視訊輸入。
- 建立一個音樂播放檔案輸出物件AVCaptureMovieFileOutput取代原來的照片輸出物件。
- 將捕獲到的視訊資料寫入到臨時檔案並在停止錄製之後儲存到相簿(通過AVCaptureMovieFileOutput的代理方法)。
相比拍照程式,程式的修改主要就是以上三點。當然為了讓程式更加完善在下面的視訊錄製程式中加入了螢幕旋轉視訊、自動佈局和後臺儲存任務等細節。下面是修改後的程式:
// // ViewController.m // AVFoundationCamera // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // 視訊錄製 #import "ViewController.h" #import <AVFoundation/AVFoundation.h> #import <AssetsLibrary/AssetsLibrary.h> typedef void(^PropertyChangeBlock)(AVCaptureDevice *captureDevice); @interface ViewController ()<AVCaptureFileOutputRecordingDelegate>//視訊檔案輸出代理 @property (strong,nonatomic) AVCaptureSession *captureSession;//負責輸入和輸出裝置之間的資料傳遞 @property (strong,nonatomic) AVCaptureDeviceInput *captureDeviceInput;//負責從AVCaptureDevice獲得輸入資料 @property (strong,nonatomic) AVCaptureMovieFileOutput *captureMovieFileOutput;//視訊輸出流 @property (strong,nonatomic) AVCaptureVideoPreviewLayer *captureVideoPreviewLayer;//相機拍攝預覽圖層 @property (assign,nonatomic) BOOL enableRotation;//是否允許旋轉(注意在視訊錄製過程中禁止螢幕旋轉) @property (assign,nonatomic) CGRect *lastBounds;//旋轉的前大小 @property (assign,nonatomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier;//後臺任務標識 @property (weak, nonatomic) IBOutlet UIView *viewContainer; @property (weak, nonatomic) IBOutlet UIButton *takeButton;//拍照按鈕 @property (weak, nonatomic) IBOutlet UIImageView *focusCursor; //聚焦游標 @end @implementation ViewController #pragma mark - 控制器檢視方法 - (void)viewDidLoad { [super viewDidLoad]; } -(void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; //初始化會話 _captureSession=[[AVCaptureSession alloc]init]; if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {//設定解析度 _captureSession.sessionPreset=AVCaptureSessionPreset1280x720; } //獲得輸入裝置 AVCaptureDevice *captureDevice=[self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];//取得後置攝像頭 if (!captureDevice) { NSLog(@"取得後置攝像頭時出現問題."); return; } //新增一個音訊輸入裝置 AVCaptureDevice *audioCaptureDevice=[[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] firstObject]; NSError *error=nil; //根據輸入裝置初始化裝置輸入物件,用於獲得輸入資料 _captureDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:captureDevice error:&error]; if (error) { NSLog(@"取得裝置輸入物件時出錯,錯誤原因:%@",error.localizedDescription); return; } AVCaptureDeviceInput *audioCaptureDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:audioCaptureDevice error:&error]; if (error) { NSLog(@"取得裝置輸入物件時出錯,錯誤原因:%@",error.localizedDescription); return; } //初始化裝置輸出物件,用於獲得輸出資料 _captureMovieFileOutput=[[AVCaptureMovieFileOutput alloc]init]; //將裝置輸入新增到會話中 if ([_captureSession canAddInput:_captureDeviceInput]) { [_captureSession addInput:_captureDeviceInput]; [_captureSession addInput:audioCaptureDeviceInput]; AVCaptureConnection *captureConnection=[_captureMovieFileOutput connectionWithMediaType:AVMediaTypeVideo]; if ([captureConnection isVideoStabilizationSupported ]) { captureConnection.preferredVideoStabilizationMode=AVCaptureVideoStabilizationModeAuto; } } //將裝置輸出新增到會話中 if ([_captureSession canAddOutput:_captureMovieFileOutput]) { [_captureSession addOutput:_captureMovieFileOutput]; } //建立視訊預覽層,用於實時展示攝像頭狀態 _captureVideoPreviewLayer=[[AVCaptureVideoPreviewLayer alloc]initWithSession:self.captureSession]; CALayer *layer=self.viewContainer.layer; layer.masksToBounds=YES; _captureVideoPreviewLayer.frame=layer.bounds; _captureVideoPreviewLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//填充模式 //將視訊預覽層新增到介面中 //[layer addSublayer:_captureVideoPreviewLayer]; [layer insertSublayer:_captureVideoPreviewLayer below:self.focusCursor.layer]; _enableRotation=YES; [self addNotificationToCaptureDevice:captureDevice]; [self addGenstureRecognizer]; } -(void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; [self.captureSession startRunning]; } -(void)viewDidDisappear:(BOOL)animated{ [super viewDidDisappear:animated]; [self.captureSession stopRunning]; } -(BOOL)shouldAutorotate{ return self.enableRotation; } ////螢幕旋轉時調整視訊預覽圖層的方向 //-(void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator{ // [super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator]; //// NSLog(@"%i,%i",newCollection.verticalSizeClass,newCollection.horizontalSizeClass); // UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; // NSLog(@"%i",orientation); // AVCaptureConnection *captureConnection=[self.captureVideoPreviewLayer connection]; // captureConnection.videoOrientation=orientation; // //} //螢幕旋轉時調整視訊預覽圖層的方向 -(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration{ AVCaptureConnection *captureConnection=[self.captureVideoPreviewLayer connection]; captureConnection.videoOrientation=(AVCaptureVideoOrientation)toInterfaceOrientation; } //旋轉後重新設定大小 -(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{ _captureVideoPreviewLayer.frame=self.viewContainer.bounds; } -(void)dealloc{ [self removeNotification]; } #pragma mark - UI方法 #pragma mark 視訊錄製 - (IBAction)takeButtonClick:(UIButton *)sender { //根據裝置輸出獲得連線 AVCaptureConnection *captureConnection=[self.captureMovieFileOutput connectionWithMediaType:AVMediaTypeVideo]; //根據連線取得裝置輸出的資料 if (![self.captureMovieFileOutput isRecording]) { self.enableRotation=NO; //如果支援多工則則開始多工 if ([[UIDevice currentDevice] isMultitaskingSupported]) { self.backgroundTaskIdentifier=[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil]; } //預覽圖層和視訊方向保持一致 captureConnection.videoOrientation=[self.captureVideoPreviewLayer connection].videoOrientation; NSString *outputFielPath=[NSTemporaryDirectory() stringByAppendingString:@"myMovie.mov"]; NSLog(@"save path is :%@",outputFielPath); NSURL *fileUrl=[NSURL fileURLWithPath:outputFielPath]; [self.captureMovieFileOutput startRecordingToOutputFileURL:fileUrl recordingDelegate:self]; } else{ [self.captureMovieFileOutput stopRecording];//停止錄製 } } #pragma mark 切換前後攝像頭 - (IBAction)toggleButtonClick:(UIButton *)sender { AVCaptureDevice *currentDevice=[self.captureDeviceInput device]; AVCaptureDevicePosition currentPosition=[currentDevice position]; [self removeNotificationFromCaptureDevice:currentDevice]; AVCaptureDevice *toChangeDevice; AVCaptureDevicePosition toChangePosition=AVCaptureDevicePositionFront; if (currentPosition==AVCaptureDevicePositionUnspecified||currentPosition==AVCaptureDevicePositionFront) { toChangePosition=AVCaptureDevicePositionBack; } toChangeDevice=[self getCameraDeviceWithPosition:toChangePosition]; [self addNotificationToCaptureDevice:toChangeDevice]; //獲得要調整的裝置輸入物件 AVCaptureDeviceInput *toChangeDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:toChangeDevice error:nil]; //改變會話的配置前一定要先開啟配置,配置完成後提交配置改變 [self.captureSession beginConfiguration]; //移除原有輸入物件 [self.captureSession removeInput:self.captureDeviceInput]; //新增新的輸入物件 if ([self.captureSession canAddInput:toChangeDeviceInput]) { [self.captureSession addInput:toChangeDeviceInput]; self.captureDeviceInput=toChangeDeviceInput; } //提交會話配置 [self.captureSession commitConfiguration]; } #pragma mark - 視訊輸出代理 -(void)captureOutput:(AVCaptureFileOutput *)captureOutput didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray *)connections{ NSLog(@"開始錄製..."); } -(void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error{ NSLog(@"視訊錄製完成."); //視訊錄入完成之後在後臺將視訊儲存到相簿 self.enableRotation=YES; UIBackgroundTaskIdentifier lastBackgroundTaskIdentifier=self.backgroundTaskIdentifier; self.backgroundTaskIdentifier=UIBackgroundTaskInvalid; ALAssetsLibrary *assetsLibrary=[[ALAssetsLibrary alloc]init]; [assetsLibrary writeVideoAtPathToSavedPhotosAlbum:outputFileURL completionBlock:^(NSURL *assetURL, NSError *error) { if (error) { NSLog(@"儲存視訊到相簿過程中發生錯誤,錯誤資訊:%@",error.localizedDescription); } if (lastBackgroundTaskIdentifier!=UIBackgroundTaskInvalid) { [[UIApplication sharedApplication] endBackgroundTask:lastBackgroundTaskIdentifier]; } NSLog(@"成功儲存視訊到相簿."); }]; } #pragma mark - 通知 /** * 給輸入裝置新增通知 */ -(void)addNotificationToCaptureDevice:(AVCaptureDevice *)captureDevice{ //注意新增區域改變捕獲通知必須首先設定裝置允許捕獲 [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) { captureDevice.subjectAreaChangeMonitoringEnabled=YES; }]; NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter]; //捕獲區域發生改變 [notificationCenter addObserver:self selector:@selector(areaChange:) name:AVCaptureDeviceSubjectAreaDidChangeNotification object:captureDevice]; } -(void)removeNotificationFromCaptureDevice:(AVCaptureDevice *)captureDevice{ NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter]; [notificationCenter removeObserver:self name:AVCaptureDeviceSubjectAreaDidChangeNotification object:captureDevice]; } /** * 移除所有通知 */ -(void)removeNotification{ NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter]; [notificationCenter removeObserver:self]; } -(void)addNotificationToCaptureSession:(AVCaptureSession *)captureSession{ NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter]; //會話出錯 [notificationCenter addObserver:self selector:@selector(sessionRuntimeError:) name:AVCaptureSessionRuntimeErrorNotification object:captureSession]; } /** * 裝置連線成功 * * @param notification 通知物件 */ -(void)deviceConnected:(NSNotification *)notification{ NSLog(@"裝置已連線..."); } /** * 裝置連線斷開 * * @param notification 通知物件 */ -(void)deviceDisconnected:(NSNotification *)notification{ NSLog(@"裝置已斷開."); } /** * 捕獲區域改變 * * @param notification 通知物件 */ -(void)areaChange:(NSNotification *)notification{ NSLog(@"捕獲區域改變..."); } /** * 會話出錯 * * @param notification 通知物件 */ -(void)sessionRuntimeError:(NSNotification *)notification{ NSLog(@"會話發生錯誤."); } #pragma mark - 私有方法 /** * 取得指定位置的攝像頭 * * @param position 攝像頭位置 * * @return 攝像頭裝置 */ -(AVCaptureDevice *)getCameraDeviceWithPosition:(AVCaptureDevicePosition )position{ NSArray *cameras= [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; for (AVCaptureDevice *camera in cameras) { if ([camera position]==position) { return camera; } } return nil; } /** * 改變裝置屬性的統一操作方法 * * @param propertyChange 屬性改變操作 */ -(void)changeDeviceProperty:(PropertyChangeBlock)propertyChange{ AVCaptureDevice *captureDevice= [self.captureDeviceInput device]; NSError *error; //注意改變裝置屬性前一定要首先呼叫lockForConfiguration:呼叫完之後使用unlockForConfiguration方法解鎖 if ([captureDevice lockForConfiguration:&error]) { propertyChange(captureDevice); [captureDevice unlockForConfiguration]; }else{ NSLog(@"設定裝置屬性過程發生錯誤,錯誤資訊:%@",error.localizedDescription); } } /** * 設定閃光燈模式 * * @param flashMode 閃光燈模式 */ -(void)setFlashMode:(AVCaptureFlashMode )flashMode{ [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) { if ([captureDevice isFlashModeSupported:flashMode]) { [captureDevice setFlashMode:flashMode]; } }]; } /** * 設定聚焦模式 * * @param focusMode 聚焦模式 */ -(void)setFocusMode:(AVCaptureFocusMode )focusMode{ [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) { if ([captureDevice isFocusModeSupported:focusMode]) { [captureDevice setFocusMode:focusMode]; } }]; } /** * 設定曝光模式 * * @param exposureMode 曝光模式 */ -(void)setExposureMode:(AVCaptureExposureMode)exposureMode{ [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) { if ([captureDevice isExposureModeSupported:exposureMode]) { [captureDevice setExposureMode:exposureMode]; } }]; } /** * 設定聚焦點 * * @param point 聚焦點 */ -(void)focusWithMode:(AVCaptureFocusMode)focusMode exposureMode:(AVCaptureExposureMode)exposureMode atPoint:(CGPoint)point{ [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) { if ([captureDevice isFocusModeSupported:focusMode]) { [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus]; } if ([captureDevice isFocusPointOfInterestSupported]) { [captureDevice setFocusPointOfInterest:point]; } if ([captureDevice isExposureModeSupported:exposureMode]) { [captureDevice setExposureMode:AVCaptureExposureModeAutoExpose]; } if ([captureDevice isExposurePointOfInterestSupported]) { [captureDevice setExposurePointOfInterest:point]; } }]; } /** * 新增點按手勢,點按時聚焦 */ -(void)addGenstureRecognizer{ UITapGestureRecognizer *tapGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapScreen:)]; [self.viewContainer addGestureRecognizer:tapGesture]; } -(void)tapScreen:(UITapGestureRecognizer *)tapGesture{ CGPoint point= [tapGesture locationInView:self.viewContainer]; //將UI座標轉化為攝像頭座標 CGPoint cameraPoint= [self.captureVideoPreviewLayer captureDevicePointOfInterestForPoint:point]; [self setFocusCursorWithPoint:point]; [self focusWithMode:AVCaptureFocusModeAutoFocus exposureMode:AVCaptureExposureModeAutoExpose atPoint:cameraPoint]; } /** * 設定聚焦游標位置 * * @param point 游標位置 */ -(void)setFocusCursorWithPoint:(CGPoint)point{ self.focusCursor.center=point; self.focusCursor.transform=CGAffineTransformMakeScale(1.5, 1.5); self.focusCursor.alpha=1.0; [UIView animateWithDuration:1.0 animations:^{ self.focusCursor.transform=CGAffineTransformIdentity; } completion:^(BOOL finished) { self.focusCursor.alpha=0; }]; } @end
執行效果:
總結
前面用了大量的篇幅介紹了iOS中的音、視訊播放和錄製,有些地方用到了封裝好的播放器、錄音機直接使用,有些是直接呼叫系統服務自己組織封裝,正如本篇開頭所言,iOS對於多媒體支援相當靈活和完善,那麼開放過程中如何選擇呢,下面就以一個表格簡單對比一下各個開發技術的優缺點。
提示:從本文及以後的文章中可能慢慢使用storyboard或xib,原因如下:1.蘋果官方目前主推storyboard;2.後面的文章中做螢幕適配牽扯到很多內容都是storyboard中進行(儘管純程式碼也可以實現,但是純程式碼對autolayout支援不太好)3.通過前面的一系列文章大家對於純程式碼程式設計應該已經有一定的積累了(純程式碼確實可以另初學者更加了解程式執行原理)。