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