隨著移動互聯網的發展,如今的手機早已不是打電話、發短信那麼簡單了,播放音樂、視頻、錄音、拍照等都是很常用的功能。在iOS中對於多媒體的支持是非常強大的,無論是音視頻播放、錄制,還是對麥克風、攝像頭的操作都提供了多套API。在今天的文章中將會對這些內容進行一一介紹:
在iOS中音頻播放從形式上可以分為音效播放和音樂播放。前者主要指的是一些短音頻播放,通常作為點綴音頻,對於這類音頻不需要進行進度、循環等控制。後者指的是一些較長的音頻,通常是主音頻,對於這些音頻的播放通常需要進行精確的控制。在iOS中播放兩類音頻分別使用AudioToolbox.framework和AVFoundation.framework來完成音效和音樂播放。
AudioToolbox.framework是一套基於C語言的框架,使用它來播放音效其本質是將短音頻注冊到系統聲音服務(System Sound Service)。System Sound Service是一種簡單、底層的聲音播放服務,但是它本身也存在著一些限制:
使用System Sound Service 播放音效的步驟如下:
下面是一個簡單的示例程序:
// // KCMainViewController.m // Audio // // Created by Kenshin Cui on 14/03/30. // Copyright (c) 2014年 cmjstudio. All rights reserved. // 音效播放 #import "KCMainViewController.h" #import @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實現一個簡單播放器,在這個播放器中實現了播放、暫停、顯示播放進度功能,當然例如調節音量、設置循環模式、甚至是聲波圖像(通過分析音頻分貝值)等功能都可以實現,這裡就不再一一演示。界面效果如下:
當然由於AVAudioPlayer一次只能播放一個音頻文件,所有上一曲、下一曲其實可以通過創建多個播放器對象來完成,這裡暫不實現。播放進度的實現主要依靠一個定時器實時計算當前播放時長和音頻總時長的比例,另外為了演示委托方法,下面的代碼中也實現了播放完成委托方法,通常如果有下一曲功能的話播放完可以觸發下一曲音樂播放。下面是主要代碼:
// // ViewController.m // KCAVAudioPlayer // // Created by Kenshin Cui on 14/03/30. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "ViewController.h" #import #define kMusicFile @"劉若英 - 原來你也在這裡.mp3" #define kMusicSinger @"劉若英" #define kMusicTitle @"原來你也在這裡" @interface ViewController () @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 #define kMusicFile @"劉若英 - 原來你也在這裡.mp3" #define kMusicSinger @"劉若英" #define kMusicTitle @"原來你也在這裡" @interface ViewController () @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 播放器狀態,枚舉類型:那麼接下來的問題就是如何獲取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@interface ViewController () @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創建一個錄音機,實現了錄音、暫停、停止、播放等功能,實現效果大致如下:
在這個示例中將實行一個完整的錄音控制,包括錄音、暫停、恢復、停止,同時還會實時展示用戶錄音的聲音波動,當用戶點擊完停止按鈕還會自動播放錄音文件。程序的構建主要分為以下幾步:
下面是主要代碼:
// // ViewController.m // AVAudioRecorder // // Created by Kenshin Cui on 14/03/30. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "ViewController.h" #import #define kRecordAudioFile @"myRecord.caf" @interface ViewController () @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訂閱、播放中斷等很多強大的功能,甚至還包含了一個音頻分析器,有興趣的朋友可以訪問官網查看詳細用法
在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 媒體播放狀態,枚舉類型:注意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@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@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
截圖效果:
通過前面的方法大家應該已經看到,使用MPMoviePlayerController來生成縮略圖足夠簡單,但是如果僅僅是是為了生成縮略圖而不進行視頻播放的話,此刻使用MPMoviePlayerController就有點大材小用了。其實使用AVFundation框架中的AVAssetImageGenerator就可以獲取視頻縮略圖。使用AVAssetImageGenerator獲取縮略圖大致分為三個步驟:
// // ViewController.m // AVAssetImageGenerator // // Created by Kenshin Cui on 14/03/30. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "ViewController.h" #import @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
生成的縮略圖效果:
其實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@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,那麼新的對象就無法完成初始化,這樣也就不能再次進行播放。
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 @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, 具體使用方式今天就不再做詳細介紹。
下面看一下在iOS如何拍照和錄制視頻。在iOS中要拍照和錄制視頻最簡單的方法就是使用UIImagePickerController。UIImagePickerController繼承於UINavigationController,前面的文章中主要使用它來選取照片,其實UIImagePickerController的功能不僅如此,它還可以用來拍照和錄制視頻。首先看一下這個類常用的屬性和方法:
屬性 說明 @property(nonatomic) UIImagePickerControllerSourceType sourceType 拾取源類型,sourceType是枚舉類型:要用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#import @interface ViewController () @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
運行效果(視頻錄制):
不得不說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拍照和錄制視頻的一般步驟如下:
下面看一下如何使用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 #import 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
運行效果:
其實有了前面的拍照應用之後要在此基礎上做視頻錄制功能並不復雜,程序只需要做如下修改:
相比拍照程序,程序的修改主要就是以上三點。當然為了讓程序更加完善在下面的視頻錄制程序中加入了屏幕旋轉視頻、自動布局和後台保存任務等細節。下面是修改後的程序:
// // ViewController.m // AVFoundationCamera // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // 視頻錄制 #import "ViewController.h" #import #import typedef void(^PropertyChangeBlock)(AVCaptureDevice *captureDevice); @interface ViewController ()vcapturefileoutputrecordingdelegate>//視頻文件輸出代理 @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)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.通過前面的一系列文章大家對於純代碼編程應該已經有一定的積累了(純代碼確實可以另初學者更加了解程序運行原理)。