我們?cè)陧?xiàng)目中有時(shí)會(huì)碰到視頻相關(guān)的需求耐亏,一般的可以分為幾種情況:
1. 簡(jiǎn)單的視頻開(kāi)發(fā),對(duì)界面無(wú)要求,可直接使用系統(tǒng)UIImagePickerController洗出。
(1)使用UIImagePickerController視頻錄制,短視頻10秒鐘
(2)在UIImagePickerController代理方法 didFinishPickingMediaWithInfo图谷,使用AVAssetExportSession
轉(zhuǎn)碼MP4(一般要兼容Android播放翩活,iOS默認(rèn)是mov格式)
(3)使用AFNetWorking上傳到服務(wù)器
(4)網(wǎng)絡(luò)請(qǐng)求,使用AVPlayerViewControlller在線播放視頻流便贵。(在iOS9之后,蘋(píng)果已經(jīng)棄用MPMoviePlayerController)
1.1 UIImagePickerController
UIImagePickerController繼承于UINavigationController菠镇。UIImagePickerController可以用來(lái)選擇照片,它還可以用來(lái)拍照和錄制視頻承璃。
-
sourceType: 拾取源類型利耍,sourceType是枚舉類型:
UIImagePickerControllerSourceTypePhotoLibrary:照片庫(kù),默認(rèn)值
UIImagePickerControllerSourceTypeCamera:攝像頭
UIImagePickerControllerSourceTypeSavedPhotosAlbum:相簿 mediaTypes:媒體類型,默認(rèn)情況下此數(shù)組包含kUTTypeImage,所以拍照時(shí)可以不用設(shè)置隘梨;但是當(dāng)要錄像的時(shí)候必須設(shè)置程癌,可以設(shè)置為kUTTypeVideo(視頻,但不帶聲音)或者kUTTypeMovie(視頻并帶有聲音)
videoMaximumDuration:視頻最大錄制時(shí)長(zhǎng)轴猎,默認(rèn)為10 s
videoQuality:視頻質(zhì)量嵌莉,枚舉類型:
UIImagePickerControllerQualityTypeHigh:高清質(zhì)量
UIImagePickerControllerQualityTypeMedium:中等質(zhì)量,適合WiFi傳輸
UIImagePickerControllerQualityTypeLow:低質(zhì)量捻脖,適合蜂窩網(wǎng)傳輸
UIImagePickerControllerQualityType640x480:640480
UIImagePickerControllerQualityTypeIFrame1280x720:1280720
UIImagePickerControllerQualityTypeIFrame960x540:960*540cameraDevice:攝像頭設(shè)備锐峭,cameraDevice是枚舉類型:
UIImagePickerControllerCameraDeviceRear:前置攝像頭
UIImagePickerControllerCameraDeviceFront:后置攝像頭cameraFlashMode:閃光燈模式,枚舉類型:UIImagePickerControllerCameraFlashModeOff:關(guān)閉閃光燈UIImagePickerControllerCameraFlashModeAuto:閃光燈自動(dòng)UIImagePickerControllerCameraFlashModeOn:打開(kāi)閃光燈
(BOOL)isSourceTypeAvailable:(UIImagePickerControllerSourceType)sourceType
指定的源類型是否可用可婶,sourceType是枚舉類型:
UIImagePickerControllerSourceTypePhotoLibrary:照片庫(kù)
UIImagePickerControllerSourceTypeCamera:攝像頭
UIImagePickerControllerSourceTypeSavedPhotosAlbum:相簿UIImageWriteToSavedPhotosAlbum(UIImage *image, id completionTarget, SEL completionSelector, void *contextInfo) //保存照片到相簿
-
UISaveVideoAtPathToSavedPhotosAlbum(NSString *videoPath, id completionTarget, SEL completionSelector, void *contextInfo) //保存視頻到相簿
示例:
//使用UIImagePickerController視頻錄制 UIImagePickerController *picker = [[UIImagePickerController alloc] init]; picker.delegate = self; if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { picker.sourceType = UIImagePickerControllerSourceTypeCamera; } //mediaTypes設(shè)置攝影還是拍照 //kUTTypeImage 對(duì)應(yīng)拍照 //kUTTypeMovie 對(duì)應(yīng)攝像 // NSString *requiredMediaType = ( NSString *)kUTTypeImage; NSString *requiredMediaType1 = ( NSString *)kUTTypeMovie; NSArray *arrMediaTypes=[NSArray arrayWithObjects:requiredMediaType1,nil]; picker.mediaTypes = arrMediaTypes; // picker.videoQuality = UIImagePickerControllerQualityTypeHigh;默認(rèn)是中等 picker.videoMaximumDuration = 60.; //60秒 [self presentViewController:picker animated:YES completion:^{ }];
?
1.2 AVAsset
AVAsset是一個(gè)抽象類和不可變類沿癞。定義了媒體資源混合呈現(xiàn)的方式。主要用于獲取多媒體信息矛渴,既然是一個(gè)抽象類椎扬,那么當(dāng)然不能直接使用∈镄瘢可以讓開(kāi)發(fā)者在處理流媒體時(shí)提供一種簡(jiǎn)單統(tǒng)一的方式盗舰,它并不是媒體資源,但是它可以作為時(shí)基媒體的容器。
實(shí)際上是創(chuàng)建的是它的子類AVUrlAsset的一個(gè)實(shí)例桂躏。通過(guò)AVUrlAsset我們可以創(chuàng)建一個(gè)帶選項(xiàng)(optional)的asset钻趋,以提供更精確的時(shí)長(zhǎng)和計(jì)時(shí)信息。比如通過(guò)AVURLAsset的duration計(jì)算視頻時(shí)長(zhǎng)剂习。
/**
獲取視頻時(shí)長(zhǎng)
@param url url
@return s
*/
- (CGFloat)getVideoLength:(NSURL *)url{
AVURLAsset *avUrl = [AVURLAsset assetWithURL:url];
CMTime time = [avUrl duration];
int second = ceil(time.value/time.timescale);
return second;
}
1.3 AVAssetExportSession
AVAssetExportSession是用于為AVAsset源對(duì)象進(jìn)行轉(zhuǎn)碼輸出蛮位,可進(jìn)行格式轉(zhuǎn)碼,壓縮等鳞绕。
AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:inputURL options:nil];
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPresetMediumQuality];
exportSession.outputURL = outputURL;
exportSession.outputFileType = AVFileTypeMPEG4;
exportSession.shouldOptimizeForNetworkUse= YES;
[exportSession exportAsynchronouslyWithCompletionHandler:nil];
1.4 AVAssetImageGenerator
AVAssetImageGenerator能夠取出視頻中每一幀的縮略圖或者預(yù)覽圖失仁。舉個(gè)??:
// result
UIImage *image = nil;
// AVAssetImageGenerator
AVAsset *asset = [AVAsset assetWithURL:videoURL];
AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:asset];
imageGenerator.appliesPreferredTrackTransform = YES;
// calculate the midpoint time of video
Float64 duration = CMTimeGetSeconds([asset duration]);
// 取某個(gè)幀的時(shí)間,參數(shù)一表示哪個(gè)時(shí)間(秒)们何,參數(shù)二表示每秒多少幀
// 通常來(lái)說(shuō)萄焦,600是一個(gè)常用的公共參數(shù),蘋(píng)果有說(shuō)明:
// 24 frames per second (fps) for film, 30 fps for NTSC (used for TV in North America and
// Japan), and 25 fps for PAL (used for TV in Europe).
// Using a timescale of 600, you can exactly represent any number of frames in these systems
CMTime midpoint = CMTimeMakeWithSeconds(duration / 2.0, 600);
// get the image from
NSError *error = nil;
CMTime actualTime;
// Returns a CFRetained CGImageRef for an asset at or near the specified time.
// So we should mannully release it
CGImageRef centerFrameImage = [imageGenerator copyCGImageAtTime:midpoint
actualTime:&actualTime
error:&error];
if (centerFrameImage != NULL) {
image = [[UIImage alloc] initWithCGImage:centerFrameImage];
// Release the CFRetained image
CGImageRelease(centerFrameImage);
}
return image;
1.5 AVPlayerViewControlller
AVPlayerViewController是UIViewController的子類,它可以用來(lái)顯示AVPlayer對(duì)象的視覺(jué)內(nèi)容和標(biāo)準(zhǔn)的播放控制冤竹。
具體AVPlayer自定義下面再說(shuō)拂封,回到AVPlayerViewController,它不支持UI自定義鹦蠕,實(shí)現(xiàn)比較簡(jiǎn)單冒签,代碼如下:
AVPlayer *player = [AVPlayer playerWithURL:self.finalURL];
AVPlayerViewController *playerVC = [[AVPlayerViewController alloc] init];
playerVC.player = player;
playerVC.view.frame = self.view.frame;
[self.view addSubview:playerVC.view];
[playerVC.player play];
2. 復(fù)雜的業(yè)務(wù)需求,需要自定義視頻界面
(1)使用AVFoundation拍照和錄制視頻钟病,自定義界面
(2)使用AVAssetExportSession
轉(zhuǎn)碼MP4(一般要兼容Android播放萧恕,iOS默認(rèn)是mov格式)
(3)使用AFNetWorking上傳到服務(wù)器
(4)網(wǎng)絡(luò)請(qǐng)求刚梭,使用AVFoundation框架的AVPlayer來(lái)自定義播放界面,在線播放視頻流票唆。播放又分為先下后播和邊下邊播朴读。
2.1 AVFoundation
AVFoundation是一個(gè)可以用來(lái)使用和創(chuàng)建基于時(shí)間的視聽(tīng)媒體的框架,它提供了一個(gè)能使用基于時(shí)間的視聽(tīng)數(shù)據(jù)的詳細(xì)級(jí)別的Objective-C接口惰说。例如:您可以用它來(lái)檢查磨德,創(chuàng)建缘回,編輯或是重新編碼媒體文件吆视。也可以從設(shè)備中獲取輸入流,在視頻實(shí)時(shí)播放時(shí)操作和回放酥宴。
AVCaptureSession:媒體(音啦吧、視頻)捕獲會(huì)話,負(fù)責(zé)把捕獲的音視頻數(shù)據(jù)輸出到輸出設(shè)備中拙寡。一個(gè)AVCaptureSession可以有多個(gè)輸入輸出授滓。
AVCaptureDevice :輸入設(shè)備,包括麥克風(fēng)肆糕、攝像頭般堆,通過(guò)該對(duì)象可以設(shè)置物理設(shè)備的一些屬性(例如相機(jī)聚焦、白平衡等)诚啃。
AVCaptureDeviceInput :設(shè)備輸入數(shù)據(jù)管理對(duì)象淮摔,可以根據(jù)AVCaptureDevice創(chuàng)建對(duì)應(yīng)的AVCaptureDeviceInput對(duì)象,該對(duì)象將會(huì)被添加到AVCaptureSession中管理始赎。
AVCaptureVideoPreviewLayer :相機(jī)拍攝預(yù)覽圖層和橙,是CALayer的子類,使用該對(duì)象可以實(shí)時(shí)查看拍照或視頻錄制效果造垛,創(chuàng)建該對(duì)象需要指定對(duì)應(yīng)的 AVCaptureSession對(duì)象魔招。
AVCaptureOutput :輸出數(shù)據(jù)管理對(duì)象,用于接收各類輸出數(shù)據(jù)五辽,通常使用對(duì)應(yīng)的子類AVCaptureAudioDataOutput办斑、AVCaptureStillImageOutput、AVCaptureVideoDataOutput杆逗、AVCaptureFileOutput, 該對(duì)象將會(huì)被添加到AVCaptureSession中管理乡翅。
建立視頻拍攝的步驟如下:
- 創(chuàng)建AVCaptureSession對(duì)象。
// 創(chuàng)建會(huì)話 (AVCaptureSession) 對(duì)象髓迎。
_captureSession = [[AVCaptureSession alloc] init];
if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) {
// 設(shè)置會(huì)話的 sessionPreset 屬性, 這個(gè)屬性影響視頻的分辨率
[_captureSession setSessionPreset:AVCaptureSessionPreset640x480];
}
- 使用AVCaptureDevice的靜態(tài)方法獲得需要使用的設(shè)備峦朗,例如拍照和錄像就需要獲得攝像頭設(shè)備,錄音就要獲得麥克風(fēng)設(shè)備排龄。
// 獲取攝像頭輸入設(shè)備波势, 創(chuàng)建 AVCaptureDeviceInput 對(duì)象
// 在獲取攝像頭的時(shí)候翎朱,攝像頭分為前后攝像頭,我們創(chuàng)建了一個(gè)方法通過(guò)用攝像頭的位置來(lái)獲取攝像頭
AVCaptureDevice *videoCaptureDevice = [self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];
if (!captureDevice) {
NSLog(@"---- 取得后置攝像頭時(shí)出現(xiàn)問(wèn)題---- ");
return;
}
// 添加一個(gè)音頻輸入設(shè)備
// 直接可以拿數(shù)組中的數(shù)組中的第一個(gè)
AVCaptureDevice *audioCaptureDevice = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] firstObject];
- 利用輸入設(shè)備AVCaptureDevice初始化AVCaptureDeviceInput對(duì)象尺铣。
// 視頻輸入對(duì)象
// 根據(jù)輸入設(shè)備初始化輸入對(duì)象拴曲,用戶獲取輸入數(shù)據(jù)
_videoCaptureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:captureDevice error:&error];
if (error) {
NSLog(@"---- 取得設(shè)備輸入對(duì)象時(shí)出錯(cuò) ------ %@",error);
return;
}
// 音頻輸入對(duì)象
//根據(jù)輸入設(shè)備初始化設(shè)備輸入對(duì)象,用于獲得輸入數(shù)據(jù)
_audioCaptureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioCaptureDevice error:&error];
if (error) {
NSLog(@"取得設(shè)備輸入對(duì)象時(shí)出錯(cuò) ------ %@",error);
return;
}
- 初始化輸出數(shù)據(jù)管理對(duì)象凛忿,如果要拍照就初始化AVCaptureStillImageOutput對(duì)象澈灼;如果拍攝視頻就初始化AVCaptureMovieFileOutput對(duì)象。
// 拍攝視頻輸出對(duì)象
// 初始化輸出設(shè)備對(duì)象店溢,用戶獲取輸出數(shù)據(jù)
_caputureMovieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
- 將數(shù)據(jù)輸入對(duì)象AVCaptureDeviceInput叁熔、數(shù)據(jù)輸出對(duì)象AVCaptureOutput添加到媒體會(huì)話管理對(duì)象AVCaptureSession中。
// 將視頻輸入對(duì)象添加到會(huì)話 (AVCaptureSession) 中
if ([_captureSession canAddInput:_videoCaptureDeviceInput]) {
[_captureSession addInput:_videoCaptureDeviceInput];
}
// 將音頻輸入對(duì)象添加到會(huì)話 (AVCaptureSession) 中
if ([_captureSession canAddInput:_captureDeviceInput]) {
[_captureSession addInput:audioCaptureDeviceInput];
AVCaptureConnection *captureConnection = [_caputureMovieFileOutput connectionWithMediaType:AVMediaTypeVideo];
// 標(biāo)識(shí)視頻錄入時(shí)穩(wěn)定音頻流的接受床牧,我們這里設(shè)置為自動(dòng)
if ([captureConnection isVideoStabilizationSupported]) {
captureConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
}
}
- 創(chuàng)建視頻預(yù)覽圖層AVCaptureVideoPreviewLayer并指定媒體會(huì)話荣回,添加圖層到顯示容器中,調(diào)用AVCaptureSession的startRuning方法開(kāi)始捕獲戈咳。
// 通過(guò)會(huì)話 (AVCaptureSession) 創(chuàng)建預(yù)覽層
_captureVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession];
// 顯示在視圖表面的圖層
CALayer *layer = self.viewContrain.layer;
layer.masksToBounds = true;
_captureVideoPreviewLayer.frame = layer.bounds;
_captureVideoPreviewLayer.masksToBounds = true;
_captureVideoPreviewLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//填充模式
[layer addSublayer:_captureVideoPreviewLayer];
// 讓會(huì)話(AVCaptureSession)勾搭好輸入輸出心软,然后把視圖渲染到預(yù)覽層上
[_captureSession startRunning];
- 將捕獲的音頻或視頻數(shù)據(jù)輸出到指定文件。
//創(chuàng)建一個(gè)拍攝的按鈕著蛙,當(dāng)我們點(diǎn)擊這個(gè)按鈕就會(huì)觸發(fā)視頻錄制删铃,并將這個(gè)錄制的視頻放到 temp 文件夾中
- (IBAction)takeMovie:(id)sender {
[(UIButton *)sender setSelected:![(UIButton *)sender isSelected]];
if ([(UIButton *)sender isSelected]) {
AVCaptureConnection *captureConnection=[self.caputureMovieFileOutput connectionWithMediaType:AVMediaTypeVideo];
// 開(kāi)啟視頻防抖模式
AVCaptureVideoStabilizationMode stabilizationMode = AVCaptureVideoStabilizationModeCinematic;
if ([self.captureDeviceInput.device.activeFormat isVideoStabilizationModeSupported:stabilizationMode]) {
[captureConnection setPreferredVideoStabilizationMode:stabilizationMode];
}
//如果支持多任務(wù)則則開(kāi)始多任務(wù)
if ([[UIDevice currentDevice] isMultitaskingSupported]) {
self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
}
// 預(yù)覽圖層和視頻方向保持一致,這個(gè)屬性設(shè)置很重要,如果不設(shè)置踏堡,那么出來(lái)的視頻圖像可以是倒向左邊的猎唁。
captureConnection.videoOrientation=[self.captureVideoPreviewLayer connection].videoOrientation;
// 設(shè)置視頻輸出的文件路徑,這里設(shè)置為 temp 文件
NSString *outputFielPath=[NSTemporaryDirectory() stringByAppendingString:MOVIEPATH];
// 路徑轉(zhuǎn)換成 URL 要用這個(gè)方法暂吉,用 NSBundle 方法轉(zhuǎn)換成 URL 的話可能會(huì)出現(xiàn)讀取不到路徑的錯(cuò)誤
NSURL *fileUrl=[NSURL fileURLWithPath:outputFielPath];
// 往路徑的 URL 開(kāi)始寫(xiě)入錄像 Buffer ,邊錄邊寫(xiě)
[self.caputureMovieFileOutput startRecordingToOutputFileURL:fileUrl recordingDelegate:self];
}
else {
// 取消視頻拍攝
[self.caputureMovieFileOutput stopRecording];
[self.captureSession stopRunning];
[self completeHandle];
}
}
當(dāng)然我們錄制的開(kāi)始與結(jié)束都是有監(jiān)聽(tīng)方法的胖秒,AVCaptureFileOutputRecordingDelegate 這個(gè)代理里面就有我們想要做的
- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray *)connections
{
NSLog(@"---- 開(kāi)始錄制 ----");
}
- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error
{
NSLog(@"---- 錄制結(jié)束 ----");
}
2.2 AVPlayer自定義播放器
AVPlayer是用于管理媒體資產(chǎn)的播放和定時(shí)的控制器對(duì)象,它提供了控制播放器的有運(yùn)輸行為的接口慕的,如它可以在媒體的時(shí)限內(nèi)播放阎肝,暫停,和改變播放的速度肮街,并有定位各個(gè)動(dòng)態(tài)點(diǎn)的能力风题。我們可以使用AVPlayer來(lái)播放本地和遠(yuǎn)程的視頻媒體文件,如QuickTime影片和MP3音頻文件嫉父,以及視聽(tīng)媒體使用HTTP流媒體直播服務(wù)沛硅。
AVPlayer本身并不能顯示視頻,而且它也不像AVPlayerViewController有一個(gè)view屬性绕辖。如果AVPlayer要顯示必須創(chuàng)建一個(gè)播放器層AVPlayerLayer用于展示摇肌,播放器層繼承于CALayer,有了AVPlayerLayer之添加到控制器視圖的layer中即可仪际。要使用AVPlayer首先了解一下幾個(gè)常用的類:
- AVPlayer:播放器围小,將數(shù)據(jù)解碼處理成為圖像和聲音昵骤。
AVAsset:主要用于獲取多媒體信息,是一個(gè)抽象類肯适,不能直接使用变秦。
AVURLAsset:AVAsset的子類,可以根據(jù)一個(gè)URL路徑創(chuàng)建一個(gè)包含媒體信息的AVURLAsset對(duì)象框舔。負(fù)責(zé)網(wǎng)絡(luò)連接蹦玫,請(qǐng)求數(shù)據(jù)。
-
AVPlayerItem:一個(gè)媒體資源管理對(duì)象刘绣,管理者視頻的一些基本信息和狀態(tài)樱溉,負(fù)責(zé)數(shù)據(jù)的獲取與分發(fā);一個(gè)AVPlayerItem對(duì)應(yīng)著一個(gè)視頻資源额港〗攘可以添加觀察者監(jiān)聽(tīng)播放源的3個(gè)狀態(tài):
//添加觀察者 [_playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil]; //對(duì)播放源的三個(gè)狀態(tài)(status) AVPlayerItemStatusReadyToPlay //播放源準(zhǔn)備好 AVPlayerItemStatusUnknown //播放源未知 AVPlayerItemStatusFailed //播放源失敗
AVPlayerLayer:圖像層歧焦,AVPlayer的圖像要通過(guò)AVPlayerLayer呈現(xiàn)移斩。
使用 AVPlayer
對(duì)象控制資產(chǎn)的播放。在播放期間绢馍,可以使用一個(gè) AVPlayerItem 實(shí)例去管理資產(chǎn)作為一個(gè)整體的顯示狀態(tài)向瓷,AVPlayerItemTrack 對(duì)象來(lái)管理一個(gè)單獨(dú)軌道的顯示狀態(tài)。使用 AVPlayerLayer 顯示視頻舰涌。
需要注意的是猖任,AVPlayer的模式是,你不要主動(dòng)調(diào)用play方法播放視頻瓷耙,而是等待AVPlayerItem告訴你朱躺,我已經(jīng)準(zhǔn)備好播放了,你現(xiàn)在可以播放了摊沉,所以我們要監(jiān)聽(tīng)AVPlayerItem的狀態(tài)锦亦,通過(guò)添加監(jiān)聽(tīng)者的方式獲取AVPlayerItem的狀態(tài)谚攒。當(dāng)監(jiān)聽(tīng)到播放器已經(jīng)準(zhǔn)備好播放的時(shí)候,就可以調(diào)用play方法源请。
創(chuàng)建一個(gè)視頻播放器的思路:
- 創(chuàng)建一個(gè)view用來(lái)放置AVPlayerLayer
- 設(shè)置AVPlayer AVPlayerItem 并將 AVPlayer放到AVPlayerLayer中,在將AVPlayerLayer添加到[view.layer addSubLayer]中
- 添加觀察者彻况,觀察播放源的狀態(tài)
- 如果狀態(tài)是AVPlayerItemStatusReadyToPlay就開(kāi)始播放
- 在做一些功能上的操作
//獲取url 本地url
//NSURL *url = [[NSBundle mainBundle]URLForResource:@"視頻" withExtension:@"mp4"];
NSURL *url = [NSURL URLWithString:@"http://XXX.mp4"];
//創(chuàng)建playerItem
_playerItem = [AVPlayerItem playerItemWithURL:url];
//添加item的觀察者 監(jiān)聽(tīng)播放源的播放狀態(tài)(status)
[_playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
//創(chuàng)建playerItem
_player = [AVPlayer playerWithPlayerItem:_playerItem];
//創(chuàng)建playerLayer
_playerLayer=[AVPlayerLayer playerLayerWithPlayer:_player];
//設(shè)置_layer的frame
_playerLayer.frame=CGRectMake(_playerView.frame.origin.x, _playerView.frame.origin.y, _playerView.frame.size.width,300);
//添加到_playerView中
[_playerView.layer addSublayer:_playerLayer];
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"status"]) {
AVPlayerItem *playerItem = (AVPlayerItem *)object;
AVPlayerItemStatus status = playerItem.status;
switch (status) {
case AVPlayerItemStatusUnknown:{
}
break;
case AVPlayerItemStatusReadyToPlay:{
[self.player play];
self.player.muted = self.mute;
// 顯示圖像邏輯
[self handleShowViewSublayers];
}
break;
case AVPlayerItemStatusFailed:{
}
break;
default:
break;
}
}
}
具體代碼可下載Demo
2.3 邊播放谁尸,邊緩存
如果直接使用上面這種方式, URL 通過(guò) AVPlayer 播放纽甘,系統(tǒng)并不會(huì)做緩存處理良蛮,等下次再播又要重新下載,對(duì)流量和網(wǎng)絡(luò)狀態(tài)來(lái)說(shuō)也是蠻尷尬的悍赢。那如何做到邊播放决瞳,邊緩存咬展,下載完成還能直接從本地讀取呢?
答案就是:在本地開(kāi)啟一個(gè) http 服務(wù)器瞒斩,把需要緩存的請(qǐng)求地址指向本地服務(wù)器破婆,并帶上真正的 url 地址。
當(dāng)我們給播放器設(shè)置好url等一些參數(shù)后胸囱,播放器就會(huì)向url所在的服務(wù)器發(fā)送請(qǐng)求(請(qǐng)求參數(shù)有兩個(gè)值祷舀,一個(gè)是offset偏移量,另一個(gè)是length長(zhǎng)度烹笔,其實(shí)就相當(dāng)于NSRange一樣)裳扯,服務(wù)器就根據(jù)range參數(shù)給播放器返回?cái)?shù)據(jù)。這就是AVPlayer播放大致的原理谤职。
AVAssetResourceLoaderDelegate
我們?cè)谑褂肁VURLAsset時(shí)饰豺,實(shí)際上是可以設(shè)置AVAssetResourceLoaderDelegate代理。
AVURLAsset *urlAsset = ...
[urlAsset.resourceLoader setDelegate:<AVAssetResourceLoaderDelegate> queue:dispatch_get_main_queue()];
AVAssetResourceLoader是負(fù)責(zé)數(shù)據(jù)加載的允蜈,最最重要的是我們只要遵守了AVAssetResourceLoaderDelegate冤吨,就可以成為它的代理,成為它的代理以后饶套,數(shù)據(jù)加載可能會(huì)通過(guò)代理方法詢問(wèn)我們漩蟆。
只要找一個(gè)對(duì)象實(shí)現(xiàn)了 AVAssetResourceLoaderDelegate
這個(gè)協(xié)議的方法,丟給 asset妓蛮,再把 asset 丟給 AVPlayer怠李,AVPlayer 在執(zhí)行播放的時(shí)候就會(huì)去問(wèn)這個(gè) delegate:喂,你能不能播放這個(gè) url 案蚩恕捺癞?然后會(huì)觸發(fā)下面這個(gè)方法:
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
在 AVAssetResourceLoadingRequest
里面,request
代表原始的請(qǐng)求构挤,由于 AVPlayer 是會(huì)觸發(fā)分片下載的策略髓介,還需要從dataRequest
中得到請(qǐng)求范圍的信息。有了請(qǐng)求地址和請(qǐng)求范圍儿倒,我們就可以重新創(chuàng)建一個(gè)設(shè)置了請(qǐng)求 Range 頭的 NSURLRequest 對(duì)象版保,讓下載器去下載這個(gè)文件的 Range 范圍內(nèi)的數(shù)據(jù)。
而對(duì)應(yīng)的下載我們可以使用NSURLSession 實(shí)現(xiàn)斷點(diǎn)下載夫否、離線斷點(diǎn)下載等彻犁。
// 替代NSMutableURL, 可以動(dòng)態(tài)修改scheme
NSURLComponents *actualURLComponents = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
actualURLComponents.scheme = @"http";
// 創(chuàng)建請(qǐng)求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[actualURLComponents URL] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:20.0];
// 修改請(qǐng)求數(shù)據(jù)范圍
if (offset > 0 && self.videoLength > 0) {
[request addValue:[NSString stringWithFormat:@"bytes=%ld-%ld",(unsigned long)offset, (unsigned long)self.videoLength - 1] forHTTPHeaderField:@"Range"];
}
// 重置
[self.session invalidateAndCancel];
// 創(chuàng)建Session,并設(shè)置代理
self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
// 創(chuàng)建會(huì)話對(duì)象
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request];
// 開(kāi)始下載
[dataTask resume];
我們可以在NSURLSession的代理方法中獲得下載的數(shù)據(jù)凰慈,拿到下載的數(shù)據(jù)以后汞幢,為了解決內(nèi)存暴漲的問(wèn)題,我們使用NSOutputStream微谓,將數(shù)據(jù)直接寫(xiě)入到硬盤(pán)中存放臨時(shí)文件的文件夾森篷。在請(qǐng)求結(jié)束的時(shí)候输钩,我們判斷是否成功下載好文件,如果下載成功仲智,就把這個(gè)文件轉(zhuǎn)移到我們的存儲(chǔ)成功文件的文件夾买乃。如果下載失敗,就把臨時(shí)數(shù)據(jù)刪除钓辆。
// 1.接收到服務(wù)器響應(yīng)的時(shí)候
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler;
// 2.接收到服務(wù)器返回?cái)?shù)據(jù)的時(shí)候調(diào)用,會(huì)調(diào)用多次
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data;
// 3.請(qǐng)求結(jié)束的時(shí)候調(diào)用(成功|失敗),如果失敗那么error有值
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;
代理對(duì)象需要實(shí)現(xiàn)的功能
1.接收視頻播放器的請(qǐng)求剪验,并根據(jù)請(qǐng)求的range向服務(wù)器請(qǐng)求本地沒(méi)有獲得的數(shù)據(jù)
2.緩存向服務(wù)器請(qǐng)求回的數(shù)據(jù)到本地
3.如果向服務(wù)器的請(qǐng)求出現(xiàn)錯(cuò)誤,需要通知給視頻播放器前联,以便視頻播放器對(duì)用戶進(jìn)行提示
視頻播放器處理流程
- 1.當(dāng)開(kāi)始播放視頻時(shí)功戚,通過(guò)視頻url判斷本地cache中是否已經(jīng)緩存當(dāng)前視頻,如果有似嗤,則直接播放本地cache中視頻
- 2.如果本地cache中沒(méi)有視頻啸臀,則視頻播放器向代理請(qǐng)求數(shù)據(jù)
- 3.加載視頻時(shí)展示正在加載的提示(菊花轉(zhuǎn))
- 4.如果可以正常播放視頻,則去掉加載提示烁落,播放視頻乘粒,如果加載失敗,去掉加載提示并顯示失敗提示
- 5.在播放過(guò)程中如果由于網(wǎng)絡(luò)過(guò)慢或拖拽原因?qū)е聸](méi)有播放數(shù)據(jù)時(shí)顽馋,要展示加載提示谓厘,跳轉(zhuǎn)到第4步
代理對(duì)象處理流程
- 1.當(dāng)視頻播放器向代理請(qǐng)求dataRequest時(shí),判斷代理是否已經(jīng)向服務(wù)器發(fā)起了請(qǐng)求寸谜,如果沒(méi)有,則發(fā)起下載整個(gè)視頻文件的請(qǐng)求
- 2.如果代理已經(jīng)和服務(wù)器建立鏈接属桦,則判斷當(dāng)前的dataRequest請(qǐng)求的offset是否大于當(dāng)前已經(jīng)緩存的文件的offset熊痴,如果大于則取消當(dāng)前與服務(wù)器的請(qǐng)求,并從offset開(kāi)始到文件尾向服務(wù)器發(fā)起請(qǐng)求(此時(shí)應(yīng)該是由于播放器向后拖拽聂宾,并且超過(guò)了已緩存的數(shù)據(jù)時(shí)才會(huì)出現(xiàn))
- 3.如果當(dāng)前的dataRequest請(qǐng)求的offset小于已經(jīng)緩存的文件的offset果善,同時(shí)大于代理向服務(wù)器請(qǐng)求的range的offset,說(shuō)明有一部分已經(jīng)緩存的數(shù)據(jù)可以傳給播放器系谐,則將這部分?jǐn)?shù)據(jù)返回給播放器(此時(shí)應(yīng)該是由于播放器向前拖拽巾陕,請(qǐng)求的數(shù)據(jù)已經(jīng)緩存過(guò)才會(huì)出現(xiàn))
- 4.如果當(dāng)前的dataRequest請(qǐng)求的offset小于代理向服務(wù)器請(qǐng)求的range的offset,則取消當(dāng)前與服務(wù)器的請(qǐng)求纪他,并從offset開(kāi)始到文件尾向服務(wù)器發(fā)起請(qǐng)求(此時(shí)應(yīng)該是由于播放器向前拖拽鄙煤,并且超過(guò)了已緩存的數(shù)據(jù)時(shí)才會(huì)出現(xiàn))
- 5.只要代理重新向服務(wù)器發(fā)起請(qǐng)求,就會(huì)導(dǎo)致緩存的數(shù)據(jù)不連續(xù)茶袒,則加載結(jié)束后不用將緩存的數(shù)據(jù)放入本地cache
- 6.如果代理和服務(wù)器的鏈接超時(shí)梯刚,重試一次,如果還是錯(cuò)誤則通知播放器網(wǎng)絡(luò)錯(cuò)誤
- 7.如果服務(wù)器返回其他錯(cuò)誤薪寓,則代理通知播放器網(wǎng)絡(luò)錯(cuò)誤
3. 在線直播和視頻點(diǎn)播亡资,開(kāi)發(fā)流程:
AVPlayer + ffmpeg / ijkplayer
我們平時(shí)看到的視頻文件有許多格式澜共,比如 avi, mkv锥腻, rmvb嗦董, mov, mp4等等瘦黑,這些被稱為容器(Container)展懈, 不同的容器格式規(guī)定了其中音視頻數(shù)據(jù)的組織方式(也包括其他數(shù)據(jù),比如字幕等)供璧。容器中一般會(huì)封裝有視頻和音頻軌存崖,也稱為視頻流(stream)和音頻 流,播放視頻文件的第一步就是根據(jù)視頻文件的格式睡毒,解析(demux)出其中封裝的視頻流来惧、音頻流以及字幕(如果有的話),解析的數(shù)據(jù)讀到包 (packet)中演顾,每個(gè)包里保存的是視頻幀(frame)或音頻幀供搀,然后分別對(duì)視頻幀和音頻幀調(diào)用相應(yīng)的解碼器(decoder)進(jìn)行解碼,比如使用 H.264編碼的視頻和MP3編碼的音頻钠至,會(huì)相應(yīng)的調(diào)用H.264解碼器和MP3解碼器葛虐,解碼之后得到的就是原始的圖像(YUV or RGB)和聲音(PCM)數(shù)據(jù),然后根據(jù)同步好的時(shí)間將圖像顯示到屏幕上棉钧,將聲音輸出到聲卡屿脐,最終就是我們看到的視頻。
FFmpeg的API就是根據(jù)這個(gè)過(guò)程設(shè)計(jì)的宪卿,ffmpeg是一個(gè)多平臺(tái)多媒體處理工具的诵,處理視頻和音頻的功能非常強(qiáng)大,能夠解碼佑钾,編碼西疤, 轉(zhuǎn)碼,復(fù)用休溶,解復(fù)用代赁,流,過(guò)濾器和播放大部分的視頻格式兽掰。ffmpeg的代碼是包括兩部分的芭碍,一部分是library,一部分是tool禾进。api都是在library里面豁跑,如果直接調(diào)api來(lái)操作視頻的話,就需要寫(xiě)c或者c++了。另一部分是tool艇拍,使用的是命令行狐蜕,則不需要自己去編碼來(lái)實(shí)現(xiàn)視頻操作的流程。實(shí)際上tool只不過(guò)把命令行轉(zhuǎn)換為api的操作而已卸夕。
ijkplayer
ijkplayer 框架是B站(BiliBili)提供了一個(gè)開(kāi)源的流媒體解決方案层释,集成了 ffmpeg。使用 ijkplayer 框架我們可以很方便地實(shí)現(xiàn)視頻直播功能(HTTP/RTMP/RTSP 這幾種直播源都支持)快集。并且同時(shí)支持 iOS 和 Android 贡羔。
一個(gè)完整直播app實(shí)現(xiàn)流程
1.采集、2.濾鏡處理个初、3.編碼乖寒、4.推流、5.CDN分發(fā)院溺、6.拉流楣嘁、7.解碼、8.播放珍逸、9.聊天互動(dòng)
具體到各個(gè)環(huán)節(jié):推流端(采集逐虚、美顏處理、編碼谆膳、推流)叭爱、服務(wù)端處理(轉(zhuǎn)碼、錄制漱病、截圖买雾、鑒黃)、播放器(拉流缨称、解碼凝果、渲染)、互動(dòng)系統(tǒng)(聊天室睦尽、禮物系統(tǒng)、贊)
一個(gè)完整直播app技術(shù)點(diǎn)
流媒體開(kāi)發(fā)
:網(wǎng)絡(luò)層(socket或st)負(fù)責(zé)傳輸型雳,協(xié)議層(rtmp或hls)負(fù)責(zé)網(wǎng)絡(luò)打包当凡,封裝層(flv、ts)負(fù)責(zé)編解碼數(shù)據(jù)的封裝纠俭,編碼層(h.264和aac)負(fù)責(zé)圖像沿量,音頻壓縮。
幀
:每幀代表一幅靜止的圖像
GOP
- 直播的數(shù)據(jù)冤荆,其實(shí)是一組圖片朴则,包括I幀、P幀钓简、B幀乌妒,當(dāng)用戶第一次觀看的時(shí)候汹想,會(huì)尋找I幀,而播放器會(huì)到服務(wù)器尋找到最近的I幀反饋給用戶撤蚊。因此古掏,GOP Cache增加了端到端延遲,因?yàn)樗仨氁玫阶罱腎幀
- GOP Cache的長(zhǎng)度越長(zhǎng)侦啸,畫(huà)面質(zhì)量越好
碼率
:圖片進(jìn)行壓縮后每秒顯示的數(shù)據(jù)量槽唾。
幀率
- 由于人類眼睛的特殊生理結(jié)構(gòu),如果所看畫(huà)面之幀率高于16的時(shí)候光涂,就會(huì)認(rèn)為是連貫的庞萍,此現(xiàn)象稱之為視覺(jué)暫留。并且當(dāng)幀速達(dá)到一定數(shù)值后忘闻,再增長(zhǎng)的話钝计,人眼也不容易察覺(jué)到有明顯的流暢度提升了。
分辨率
:(矩形)圖片的長(zhǎng)度和寬度服赎,即圖片的尺寸
壓縮前的每秒數(shù)據(jù)量
:幀率X分辨率(單位應(yīng)該是若干個(gè)字節(jié))
壓縮比
:壓縮前的每秒數(shù)據(jù)量/碼率 (對(duì)于同一個(gè)視頻源并采用同一種視頻編碼算法葵蒂,則:壓縮比越高,畫(huà)面質(zhì)量越差重虑。)
視頻文件格式
:文件的后綴
践付,比如.wmv,.mov,.mp4,.mp3,.avi
,
-
主要用處
,根據(jù)文件格式缺厉,系統(tǒng)會(huì)自動(dòng)判斷用什么軟件打開(kāi),
注意: 隨意修改文件格式永高,對(duì)文件的本身不會(huì)造成太大的影響,比如把a(bǔ)vi改成mp4,文件還是avi.
視頻封裝格式
:一種儲(chǔ)存視頻信息的容器
提针,流式封裝可以有TS命爬、FLV
等,索引式的封裝有MP4,MOV,AVI
等辐脖,
-
主要作用
:一個(gè)視頻文件往往會(huì)包含圖像和音頻饲宛,還有一些配置信息(如圖像和音頻的關(guān)聯(lián),如何解碼它們等):這些內(nèi)容需要按照一定的規(guī)則組織嗜价、封裝起來(lái). -
注意
:會(huì)發(fā)現(xiàn)封裝格式跟文件格式一樣艇抠,因?yàn)橐话阋曨l文件格式的后綴名即采用相應(yīng)的視頻封裝格式的名稱,所以視頻文件格式就是視頻封裝格式。
視頻封裝格式和視頻壓縮編碼標(biāo)準(zhǔn)
:就好像項(xiàng)目工程和編程語(yǔ)言久锥,封裝格式就是一個(gè)項(xiàng)目的工程家淤,視頻編碼方式就是編程語(yǔ)言,一個(gè)項(xiàng)目工程可以用不同語(yǔ)言開(kāi)發(fā)瑟由。
另這里有幾款優(yōu)秀的播放器控件可去GitHub下載學(xué)習(xí):
ZFPlayer - 使用人數(shù)也很多絮重。基于AVPlayer,一個(gè)播放器的基本功能幾乎很全面了
mobileplayer-ios - 純 Swift 青伤。文檔詳細(xì)督怜,功能齊全
KRVideoPlayer - 36kr 開(kāi)源項(xiàng)目。類似Weico的播放器
Player - ?? video player in Swift, simple way to play and stream media on iOS/tvOS
參考文獻(xiàn):
AVFoundation Programming Guide(官方文檔翻譯)完整版
IOS 微信聊天發(fā)送小視頻的秘密(AVAssetReader+AVAssetReaderTrackOutput播放視頻)
【補(bǔ)充】NSURLSession 詳解離線斷點(diǎn)下載的實(shí)現(xiàn)
iOS 利用FFmpeg 開(kāi)發(fā)音視頻流(二)——Mac 系統(tǒng)上編譯 iOS 可用的FFmpeg 庫(kù)
【如何快速的開(kāi)發(fā)一個(gè)完整的iOS直播app】(原理篇)