iOS開發系列--音頻播放、錄音、視頻播放、拍照、視頻錄製

概覽

隨着移動互聯網的發展,現在的手機早已不是打電話、發短信那麼簡單了,播放音樂、視頻、錄音、拍照等都是很經常使用的功能。在iOS中對於多媒體的支持是很是 強大的,不管是音視頻播放、錄製,仍是對麥克風、攝像頭的操做都提供了多套API。在今天的文章中將會對這些內容進行一一介紹:html

  1. 音頻
    1. 音效
    2. 音樂
    3. 音頻會話
    4. 錄音
    5. 音頻隊列服務
  2. 視頻
    1. MPMoviePlayerController
    2. MPMoviePlayerViewController
    3. AVPlayer
  3. 攝像頭
    1. UIImagePickerController拍照和視頻錄製
    2. AVFoundation拍照和錄製視頻
  4. 總結

音頻

在iOS中音頻播放從形式上能夠分爲音效播放和音樂播放。前者主要指的是一些短音頻播放,一般做爲點綴音頻,對於這類音頻不須要進行進度、循環等控制。後 者指的是一些較長的音頻,一般是主音頻,對於這些音頻的播放一般須要進行精確的控制。在iOS中播放兩類音頻分別使用 AudioToolbox.framework和AVFoundation.framework來完成音效和音樂播放。ios

音效

AudioToolbox.framework是一套基於C語言的框架,使用它來播放音效其本質是將短音頻註冊到系統聲音服務(System Sound Service)。System Sound Service是一種簡單、底層的聲音播放服務,可是它自己也存在着一些限制:git

  • 音頻播放時間不能超過30s
  • 數據必須是PCM或者IMA4格式
  • 音頻文件必須打包成.caf、.aif、.wav中的一種(注意這是官方文檔的說法,實際測試發現一些.mp3也能夠播放)

使用System Sound Service 播放音效的步驟以下:github

  1. 調用AudioServicesCreateSystemSoundID(   CFURLRef  inFileURL, SystemSoundID*   outSystemSoundID)函數得到系統聲音ID。
  2. 若是須要監聽播放完成操做,則使用AudioServicesAddSystemSoundCompletion(  SystemSoundID inSystemSoundID,
    CFRunLoopRef  inRunLoop, CFStringRef  inRunLoopMode, AudioServicesSystemSoundCompletionProc  inCompletionRoutine, void*  inClientData)
    方法註冊回調函數。
  3. 調用AudioServicesPlaySystemSound(SystemSoundID inSystemSoundID) 或者AudioServicesPlayAlertSound(SystemSoundID inSystemSoundID) 方法播放音效(後者帶有震動效果)。

下面是一個簡單的示例程序: 編程

// // 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的使用比較簡單:緩存

  1. 初始化AVAudioPlayer對象,此時一般指定本地文件路徑。
  2. 設置播放器屬性,例如重複次數、音量大小等。
  3. 調用play方法播放。

下面就使用AVAudioPlayer實現一個簡單播放器,在這個播放器中實現了播放、暫停、顯示播放進度功能,固然例如調節音量、設置循環模式、甚至是聲波圖像(經過分析音頻分貝值)等功能均可以實現,這裏就再也不一一演示。界面效果以下:網絡

AudioPlayerScreen

固然因爲AVAudioPlayer一次只能播放一個音頻文件,全部上一曲、下一曲其實能夠經過建立多個播放器對象來完成,這裏暫不實現。播放進度的實現 主要依靠一個定時器實時計算當前播放時長和音頻總時長的比例,另外爲了演示委託方法,下面的代碼中也實現了播放完成委託方法,一般若是有下一曲功能的話播 放完能夠觸發下一曲音樂播放。下面是主要代碼:session

// // 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

運行效果:app

AVAudioPlayer

音頻會話

事實上上面的播放器還存在一些問題,例如一般咱們看到的播放器即便退出到後臺也是能夠播放的,而這個播放器若是退出到後臺它會自動暫停。若是要支持後臺播放須要作下面幾件事情:

1.設置後臺運行模式:在plist文件中添加Required background modes,而且設置item 0=App plays audio or streams audio/video using AirPlay(其實能夠直接經過Xcode在Project Targets-Capabilities-Background Modes中設置)

BackgroundModes

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建立一個錄音機,實現了錄音、暫停、中止、播放等功能,實現效果大體以下:

AVAudioRecorderSnapshot

在這個示例中將實行一個完整的錄音控制,包括錄音、暫停、恢復、中止,同時還會實時展現用戶錄音的聲音波動,當用戶點擊完中止按鈕還會自動播放錄音文件。程序的構建主要分爲如下幾步:

  1. 設置音頻會話類型爲AVAudioSessionCategoryPlayAndRecord,由於程序中牽扯到錄音和播放操做。
  2. 建立錄音機AVAudioRecorder,指定錄音保存的路徑而且設置錄音屬性,注意對於通常的錄音文件要求的採樣率、位數並不高,須要適當設置以保證錄音文件的大小和效果。
  3. 設置錄音機代理以便在錄音完成後播放錄音,打開錄音測量保證可以實時得到錄音時的聲音強度。(注意聲音強度範圍-160到0,0表明最大輸入)
  4. 建立音頻播放器AVAudioPlayer,用於在錄音完成以後播放錄音。
  5. 建立一個定時器以便實時刷新錄音測量值並更新錄音強度到UIProgressView中顯示。
  6. 添加錄音、暫停、恢復、中止操做,須要注意錄音的恢復操做實際上是有音頻會話管理的,恢復時只要再次調用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]; //每一個採樣點位數,分爲八、1六、2四、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

運行效果:

AVAudioRecorder

音頻隊列服務

你們應該已經注意到了,不管是前面的錄音仍是音頻播放均不支持網絡流媒體播放,固然對於錄音來講這種需求可能不大,可是對於音頻播放來講有時候就頗有必要 了。AVAudioPlayer只能播放本地文件,而且是一次性加載因此音頻數據,初始化AVAudioPlayer時指定的URL也只能是File URL而不能是HTTP URL。固然,將音頻文件下載到本地而後再調用AVAudioPlayer來播放也是一種播放網絡音頻的辦法,可是這種方式最大的弊端就是必須等到整個音 頻播放完成才能播放,而不能使用流式播放,這每每在實際開發中是不切實際的。那麼在iOS中如何播放網絡流媒體呢?就是使用AudioToolbox框架 中的音頻隊列服務Audio Queue Services。

使用音頻隊列服務徹底能夠作到音頻播放和錄製,首先看一下錄音音頻服務隊列:

recording_architecture_2x

一個音頻服務隊列Audio Queue有三部分組成:

三個緩衝器Buffers:每一個緩衝器都是一個存儲音頻數據的臨時倉庫。

一個緩衝隊列Buffer Queue:一個包含音頻緩衝器的有序隊列。

一個回調Callback:一個自定義的隊列回調函數。

聲音經過輸入設備進入緩衝隊列中,首先填充第一個緩衝器;當第一個緩衝器填充滿以後自動填充下一個緩衝器,同時會調用回調函數;在回調函數中須要將緩衝器 中的音頻數據寫入磁盤,同時將緩衝器放回到緩衝隊列中以便重用。下面是Apple官方關於音頻隊列服務的流程示意圖:

recording_callback_function_2x

相似的,看一下音頻播放緩衝隊列,其組成部分和錄音緩衝隊列相似。

playback_architecture_2x

可是在音頻播放緩衝隊列中,回調函數調用的時機不一樣於音頻錄製緩衝隊列,流程恰好相反。將音頻讀取到緩衝器中,一旦一個緩衝器填充滿以後就放到緩衝隊列 中,而後繼續填充其餘緩衝器;當開始播放時,則從第一個緩衝器中讀取音頻進行播放;一旦播放完以後就會觸發回調函數,開始播放下一個緩衝器中的音頻,同時 填充第一個緩衝器放;填充滿以後再次放回到緩衝隊列。下面是詳細的流程:

playback_callback_function_2x

固然,要明白音頻隊列服務的原理並不難,問題是如何實現這個自定義的回調函數,這其中咱們有大量的工做要作,控制播放狀態、處理異常中斷、進行音頻編碼等 等。因爲牽扯內容過多,並且不是本文目的,若是之後有時間將另開一篇文章重點介紹,目前有不少第三方優秀框架能夠直接使用,例如AudioStreamerFreeStreamer。因爲前者當前只有非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

運行效果:

MPMoviePlayerController
從上面的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

截圖效果:

MPMoviePlayerController_Thumbnail1     MPMoviePlayerController_Thumbnail2

擴展--使用AVFoundation生成縮略圖

經過前面的方法你們應該已經看到,使用MPMoviePlayerController來生成縮略圖足夠簡單,可是若是僅僅是是爲了生成縮略圖而不進行視 頻播放的話,此刻使用MPMoviePlayerController就有點大材小用了。其實使用AVFundation框架中的 AVAssetImageGenerator就能夠獲取視頻縮略圖。使用AVAssetImageGenerator獲取縮略圖大體分爲三個步驟:

  1. 建立AVURLAsset對象(此類主要用於獲取媒體信息,包括視頻、聲音等)。
  2. 根據AVURLAsset建立AVAssetImageGenerator對象。
  3. 使用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

生成的縮略圖效果:

AVAssetImageGenerator_Thumbnail

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

這裏須要強調一下,因爲MPMoviePlayerViewController的初始化方法作了大量工做(例如設置URL、自動播放、添加點擊Done 完成的監控等),因此當再次點擊播放彈出新的模態窗口的時若是不銷燬以前的MPMoviePlayerViewController,那麼新的對象就沒法 完成初始化,這樣也就不能再次進行播放。

AVPlayer

MPMoviePlayerController足夠強大,幾乎不用寫幾行代碼就能完成一個播放器,可是正是因爲它的高度封裝使得要自定義這個播放器變得 很複雜,甚至是不可能完成。例若有些時候須要自定義播放器的樣式,那麼若是要使用MPMoviePlayerController就不合適了,若是要對視 頻有自由的控制則可使用AVPlayer。AVPlayer存在於AVFoundation中,它更加接近於底層,因此靈活性也更強:

AVFoundation_Framework

AVPlayer自己並不能顯示視頻,並且它也不像MPMoviePlayerController有一個view屬性。若是AVPlayer要顯示必須 建立一個播放器層AVPlayerLayer用於展現,播放器層繼承於CALayer,有了AVPlayerLayer之添加到控制器視圖的layer中 便可。要使用AVPlayer首先了解一下幾個經常使用的類:

AVAsset:主要用於獲取多媒體信息,是一個抽象類,不能直接使用。

AVURLAsset:AVAsset的子類,能夠根據一個URL路徑建立一個包含媒體信息的AVURLAsset對象。

AVPlayerItem:一個媒體資源管理對象,管理者視頻的一些基本信息和狀態,一個AVPlayerItem對應着一個視頻資源。

下面簡單經過一個播放器來演示AVPlayer的使用,播放器的效果以下:

AVPlayer_Thumbnail

在這個自定義的播放器中實現了視頻播放、暫停、進度展現和視頻列表功能,下面將對這些功能一一介紹。

首先說一下視頻的播放、暫停功能,這也是最基本的功能,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

運行效果:

AVPlayer

到目前爲止不管是MPMoviePlayerController仍是AVPlayer來播放視頻都至關強大,可是它也存在着一些不可迴避的問題,那就是 支持的視頻編碼格式頗有限:H.26四、MPEG-4,擴展名(壓縮格式):.mp四、.mov、.m4v、.m2v、.3gp、.3g2等。可是不管是 MPMoviePlayerController仍是AVPlayer它們都支持絕大多數音頻編碼,因此你們若是純粹是爲了播放音樂的話也能夠考慮使用這 兩個播放器。那麼如何支持更多視頻編碼格式呢?目前來講主要仍是依靠第三方框架,在iOS上經常使用的視頻編碼、解碼框架有:VLCffmpeg, 具體使用方式今天就再也不作詳細介紹。

攝像頭

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來拍照或者錄製視頻一般能夠分爲以下步驟:

  1. 建立UIImagePickerController對象。
  2. 指定拾取源,平時選擇照片時使用的拾取源是照片庫或者相簿,此刻須要指定爲攝像頭類型。
  3. 指定攝像頭,前置攝像頭或者後置攝像頭。
  4. 設置媒體類型mediaType,注意若是是錄像必須設置,若是是拍照此步驟能夠省略,由於mediaType默認包含kUTTypeImage(注意媒體類型定義在MobileCoreServices.framework中)
  5. 指定捕獲模式,拍照或者錄製視頻。(視頻錄製時必須先設置媒體類型再設置捕獲模式
  6. 展現UIImagePickerController(一般以模態窗口形式打開)。
  7. 拍照和錄製視頻結束後在代理方法中展現/保存照片或視頻。

固然這個過程當中有不少細節能夠設置,例如是否顯示拍照控制面板,拍照後是否容許編輯等等,經過上面的屬性/方法列表相信並不難理解。下面就以一個示例展現 如何使用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

運行效果(視頻錄製):

UIImagePickerController

AVFoundation拍照和錄製視頻

不得不說UIImagePickerController確實強大,可是與MPMoviePlayerController相似,因爲它的高度封裝性,要 進行某些自定義工做就比較複雜了。例如要作出一款相似於美顏相機的拍照界面就比較難以實現了,此時就能夠考慮使用AVFoundation來實現。 AVFoundation中提供了不少現成的播放器和錄音機,可是事實上它還有更加底層的內容能夠供開發者使用。由於AVFoundation中抽了不少 和底層輸入、輸出設備打交道的類,依靠這些類開發人員面對的再也不是封裝好的音頻播放器AVAudioPlayer、錄音機 (AVAudioRecorder)、視頻(包括音頻)播放器AVPlayer,而是輸入設備(例如麥克風、攝像頭)、輸出設備(圖片、視頻)等。首先了 解一下使用AVFoundation作拍照和視頻錄製開發用到的相關類:

AVCaptureSession:媒體(音、視頻)捕獲會話,負責把捕獲的音視頻數據輸出到輸出設備中。一個AVCaptureSession能夠有多個輸入輸出:

AVCaptureSession_InputAndOutput

AVCaptureDevice:輸入設備,包括麥克風、攝像頭,經過該對象能夠設置物理設備的一些屬性(例如相機聚焦、白平衡等)。

AVCaptureDeviceInput:設備輸入數據管理對象,能夠根據AVCaptureDevice建立對應的AVCaptureDeviceInput對象,該對象將會被添加到AVCaptureSession中管理。

AVCaptureOutput:輸出數據管理對象,用於接收各種輸出數據,一般使用對應的子類 AVCaptureAudioDataOutput、AVCaptureStillImageOutput、 AVCaptureVideoDataOutput、AVCaptureFileOutput,該對象將會被添加到AVCaptureSession中管 理。注意:前面幾個對象的輸出數據都是NSData類型,而AVCaptureFileOutput表明數據以文件形式輸出,相似 的,AVCcaptureFileOutput也不會直接建立使用,一般會使用其子類:AVCaptureAudioFileOutput、 AVCaptureMovieFileOutput。當把一個輸入或者輸出添加到AVCaptureSession以後AVCaptureSession 就會在全部相符的輸入、輸出設備之間創建鏈接(AVCaptionConnection):

AVCaptureSession_Relation

AVCaptureVideoPreviewLayer:相機拍攝預覽圖層,是CALayer的子類,使用該對象能夠實時查看拍照或視頻錄製效果,建立該對象須要指定對應的AVCaptureSession對象。

使用AVFoundation拍照和錄製視頻的通常步驟以下:

  1. 建立AVCaptureSession對象。
  2. 使用AVCaptureDevice的靜態方法得到須要使用的設備,例如拍照和錄像就須要得到攝像頭設備,錄音就要得到麥克風設備。
  3. 利用輸入設備AVCaptureDevice初始化AVCaptureDeviceInput對象。
  4. 初始化輸出數據管理對象,若是要拍照就初始化AVCaptureStillImageOutput對象;若是拍攝視頻就初始化AVCaptureMovieFileOutput對象。
  5. 將數據輸入對象AVCaptureDeviceInput、數據輸出對象AVCaptureOutput添加到媒體會話管理對象AVCaptureSession中。
  6. 建立視頻預覽圖層AVCaptureVideoPreviewLayer並指定媒體會話,添加圖層到顯示容器中,調用AVCaptureSession的startRuning方法開始捕獲。
  7. 將捕獲的音頻或視頻數據輸出到指定文件。

拍照

下面看一下如何使用AVFoundation實現一個拍照程序,在這個程序中將實現攝像頭預覽、切換先後攝像頭、閃光燈設置、對焦、拍照保存等功能。應用大體效果以下:

AVFoundation_CameraRunEffect

在程序中定義會話、輸入、輸出等相關對象。

@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

運行效果:

 

視頻錄製

其實有了前面的拍照應用以後要在此基礎上作視頻錄製功能並不複雜,程序只須要作以下修改:

  1. 添加一個音頻輸入到會話(使用[[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] firstObject]得到輸入設備,而後根據此輸入設備建立一個設備輸入對象),在拍照程序中已經添加了視頻輸入因此此時不須要添加視頻輸入。
  2. 建立一個音樂播放文件輸出對象AVCaptureMovieFileOutput取代原來的照片輸出對象。
  3. 將捕獲到的視頻數據寫入到臨時文件並在中止錄製以後保存到相簿(經過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對於多媒體支持至關靈活和完善,那麼開放過程當中如何選擇呢,下面就以一個表格簡單對比一下各個開發技術的優缺點。

summary

提示:從本文及之後的文章中可能慢慢使用storyboard或xib,緣由以下:1.蘋果官方目前主推storyboard;2.後面的文章中作屏幕適 配牽扯到不少內容都是storyboard中進行(儘管純代碼也能夠實現,可是純代碼對autolayout支持不太好)3.經過前面的一系列文章你們對 於純代碼編程應該已經有必定的積累了(純代碼確實能夠另初學者更加了解程序運行原理)。

相關文章
相關標籤/搜索