小視訊是微信的一個重大創新功能,而在開發小視訊時,由於這個功能比較新,需求也沒那麼多,查閱了大量資料,包括檢視各種官方文件、下載所有的視訊官方Demo和去GitHub上面檢視各種視訊庫,也踩了很多坑才完成了這個功能。這也是我在完成以後,想要做這樣一個小視訊的開源庫PKShortVideo的原因。
GitHub連結:https://github.com/pepsikirk/PKShortVideo,歡迎star和提issue。
小視訊的錄製
錄製的第一種方案
錄製視訊最開始用的是網上找的案例 AVCaptureSession + AVCaptureMovieFileOutput 來錄製視訊,然後用 AVAssetExportSeeion 來轉碼壓縮視訊。這就遇到了問題,那就是
壓縮後視訊的解析度以及保證預覽拍攝視訊與最終生成視訊影像一致。
根據 AVFoundation Programming Guide 的 Still and Video Media Capture 部分,AVCaptureSession的解析度所輸出的視訊解析度是固定的由AVCaptureSessionPreset引數決定,無法達到需求所需要的解析度(微信的小視訊解析度為320 X 240)。所以先根據微信小視訊的解析度選擇了一個最為接近的AVCaptureSessionPresetMedium(解析度豎屏情況下為360 X 480)。
預覽Layer的 videoGravity 模式我出於攝像頭位置據居中考慮使用的是 ResizeAspectFill :
1 2 |
AVCaptureVideoPreviewLayer *previewLayer = [self.recorder previewLayer]; previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; |
所以在保持長寬比的前提下,會縮放圖片,使圖片充滿容器View。這樣需要擷取的視訊就為去掉上下兩端多餘最中間的部分。
我通過查詢以後找到的解決方案就是壓縮以後進行處理,而AVAssetExportSeeion設定壓縮輸出後的質量與AVCaptureSession類似,也是通過一個字串型別AVAssetExportPreset來確定的,也並不能自定義解析度。
按照這個思路尋找答案,後來經過多番查詢,發現 AVAssetExportSeeionyou 有著這樣的一個介面提供自定義的設定。
1 2 |
/* Indicates whether video composition is enabled for export and supplies the instructions for video composition. Ignored when export preset is AVAssetExportPresetPassthrough. */ @property (nonatomic, copy, nullable) AVVideoComposition *videoComposition; |
最終程式碼如下:(此程式碼年久失修,不確定是否還能用,這裡的只提供思路)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
AVAsset *asset = [AVAsset assetWithURL:mediaURL]; CMTime assetTime = [asset duration]; AVAssetTrack *assetTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; AVMutableComposition *composition = [AVMutableComposition composition]; AVMutableCompositionTrack *compositionTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; [compositionTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, assetTime) ofTrack:assetTrack atTime:kCMTimeZero error:nil]; AVMutableVideoCompositionLayerInstruction *videoCompositionLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:assetTrack]; CGAffineTransform transform = CGAffineTransformMake(0, 8/9.0, -8/9.0, 0, 320, -93.3333333); [videoCompositionLayerInstruction setTransform:transform atTime:kCMTimeZero]; AVMutableVideoCompositionInstruction *videoCompositionInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction]; [videoCompositionInstruction setTimeRange:CMTimeRangeMake(kCMTimeZero, [composition duration])]; videoCompositionInstruction.layerInstructions = [NSArray arrayWithObject:videoCompositionLayerInstruction]; AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition]; videoComposition.renderSize = CGSizeMake(320.0f, 240.0f); videoComposition.frameDuration = CMTimeMake(1, 30); videoComposition.instructions = [NSArray arrayWithObject:videoCompositionInstruction]; AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:asset presetName:AVAssetExportPresetMediumQuality]; exportSession.outputURL = [NSURL fileURLWithPath:outputPath]; exportSession.outputFileType = AVFileTypeMPEG4; exportSession.shouldOptimizeForNetworkUse = YES; [exportSession setVideoComposition:videoComposition]; [exportSession exportAsynchronouslyWithCompletionHandler:^(void) { if (exportSession.status == AVAssetExportSessionStatusCompleted) { //壓縮完成 } }]; |
AVAssetTrack 、AVMutableComposition 、 AVMutableCompositionTrack、 AVMutableVideoCompositionLayerInstruction、 AVMutableVideoCompositionInstruction、 AVMutableVideoComposition 相信大家看到一下子這麼多搞不清什麼區別命名什麼的又差不多的類都暈了吧 ,在這裡就不細談了,有興趣的可以自行了解。
關鍵在於這段程式碼
1 2 |
CGAffineTransform transform = CGAffineTransformMake(0, 8/9.0, -8/9.0, 0, 320, -93.3333333); [videoCompositionLayerInstruction setTransform:transform atTime:kCMTimeZero]; |
通過設定transform可以變換各種視訊輸出樣式,包括只擷取視訊某一部分和各種變換,CGAffineTransform可以好好理解一下,做動畫也經常可以用到(我這裡是寫死了只能擷取原視訊解析度為360 X 480 情況下擷取中間部分 320 X 240的解析度的情況,而且後來想再試試發現好像已經不能用了,後來換了別的方式也沒有再改了)。
錄製第一種方案的缺點
如微信官方開發分享的iOS微信小視訊優化心得裡寫的:
於是用AVCaptureMovieFileOutput(640*480)直接生成視訊檔案,拍視訊很流暢。然而錄製的6s視訊大小有2M+,再用MMovieDecoder+MMovieWriter壓縮至少要7~8s,影響聊天視窗發小視訊的速度。
這個方案需要拍攝完成以後再進行轉碼壓縮,速度比較慢,影響使用者體驗,那有沒有一種方式可以直接在拍攝時就直接進行轉碼壓縮呢?下面來看方案二。
錄製的第二種方案
再次經過多番查詢(最開始開發時並不知道微信官方分享的文章,順便吐槽一下微信訂閱號文章不會被搜尋到),翻找官方demo和搜尋,並沒有找到一個很好的錄製思路,後來想起來喵神主導的ObjC中國好像在之前有過一個視訊期刊模組的分享,於是去看了一下就找到了這篇文章在 iOS 上捕獲視訊,看完之後大有裨益,強烈推薦大家也去看看(ObjC中國裡的文章都很棒,感謝喵神主導的翻譯組)。並直接在上面找到VideoCaptureDemo。我最終的實現方案也是由這個改寫而成。
這裡面也包括含有了UIImagePickerController,AVCaptureSession + AVMovieFileOutput,AVCaptureSession + AVAssetWriter 三種方案,UIImagePickerController就是最基本的系統拍攝照片和錄製視訊的庫了,一般普通視訊和拍照時會用到。第二種就是我上面說的第一個錄製方案,最後一種就是我們想要的定製性最強的錄製方案了。
這個方案借用在 iOS 上捕獲視訊的一張圖和話:
如果你想要對影音輸出有更多的操作,你可以使用 AVCaptureVideoDataOutput 和 AVCaptureAudioDataOutput 而不是我們上節討論的 AVCaptureMovieFileOutput。 這些輸出將會各自捕獲視訊和音訊的樣本快取,接著傳送到它們的代理。代理要麼對取樣緩衝進行處理 (比如給視訊加濾鏡),要麼保持原樣傳送。使用 AVAssetWriter 物件可以將樣本快取寫入檔案:
這個方案就相當於自己對每一幀影像都可以進行處理,SCRecorder就是用類似的方式做的。這種方案在iPhone4上不會出現iOS微信小視訊優化心得中說的:
在4s以上的裝置拍攝小視訊挺流暢,幀率能達到要求。但是在iPhone4,錄製的時候特別卡,錄到的視訊只有6~8幀/秒。嘗試把錄製視訊時的介面動畫去掉,稍微流暢些,幀率多了3~4幀/秒,還是不滿足需求。
這個的問題。由於微信方面沒有開原始碼,也無法對比,不過也就沒有其後面寫的其它問題了。
同樣的,這個方案也需要考慮
壓縮後視訊的解析度
以及保證預覽拍攝視訊與最終生成視訊影像一致
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
NSInteger numPixels = self.outputSize.width * self.outputSize.height; //每畫素位元 CGFloat bitsPerPixel = 6.0; NSInteger bitsPerSecond = numPixels * bitsPerPixel; // 位元速率和幀率設定 NSDictionary *compressionProperties = @{ AVVideoAverageBitRateKey : @(bitsPerSecond), AVVideoExpectedSourceFrameRateKey : @(30), AVVideoAverageBitRateKey : @(30) }; NSDictionary *videoCompressionSettings = @{ AVVideoCodecKey : AVVideoCodecH264, AVVideoScalingModeKey : AVVideoScalingModeResizeAspectFill, AVVideoWidthKey : @(self.outputSize.height), AVVideoHeightKey : @(self.outputSize.width), AVVideoCompressionPropertiesKey : compressionProperties }; self.videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoCompressionSettings sourceFormatHint:videoFormatDescription]; self.videoInput.transform = CGAffineTransformMakeRotation(M_PI_2);//人像方向; |
AVVideoHeightKey和AVVideoHeightKey分別是高和寬賦值是相反的。因為一般以人觀看的方向做為參考標準來說小視訊的解析度 寬 X 高 是 320 X 240,而裝置預設的方向是Landscape Left,即裝置向左偏移90度,所以實際的視訊解析度就是240 X 320與一般認為的相反。
由於小視訊是隻支援豎屏拍攝即裝置方向為Portrait,就可以固定設定self.videoInput.transform = CGAffineTransformMakeRotation(M_PI_2)
固定向右偏移90°。通過MediaInfo
檢視出相當於給輸出視訊新增了一個90°的角度資訊,這樣在播放時就能通過角度資訊對視訊進行播放糾正。
AVVideoScalingModeResizeAspectFill 也是非常重要的引數,對應著 AVLayerVideoGravityResizeAspectFill 就可以統一擷取中間部分,不會變形並且與預覽圖一致。達到可以自定義解析度不會變形的功能。
小視訊的播放
小視訊點選放大播放
小視訊點選放大以後播放比較簡單,基本使用MPMoviePlayerController(無法定製UI)和AVPlayer(可以定製UI)可以解決。程式碼可參考我的PKShortVideo。或官方demoAVPlayerDemo
小視訊在聊天介面播放第一種方案
聊天頁面的播放比較特殊,原因是需要能夠同時播放多個小視訊,並且在播放時滾動介面也需要一定的流暢性,對效能要求比較高。
最初我的實現方案就是通過AVPlayer在聊天介面直接建立播放。但是很快就遇到了問題:
- 第一個是播放起來比較卡頓。後來通過測試,微信是有著立刻當前顯示列表就停止播放,滾動停止後才開始播放的優化,使用以後流暢了很多。
- 第二就是AVPlayer最多隻能夠建立16個播放的視訊,這個問題我後來通過一個單例管理類用簡單的演算法來解決此問題。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
- (AVPlayer *)getAVQueuePlayWithPlayerItem:(AVPlayerItem *)item messageID:(NSString *)messageID { //通過messageID取Player物件 AVPlayer *player = self.playerDict[messageID]; if (player) { //物件不等時替換player物件的item if (player.currentItem != item) { [player replaceCurrentItemWithPlayerItem:item]; } return player; } else { //未在介面建立小視訊時返回nil if (!self.playerArray.count) { return nil; } //按順序平均分配player陣列裡面的player AVPlayer *player = self.playerArray[_playerIndex]; if (_playerIndex == PlayerCount - 1) { _playerIndex = 0; } else { _playerIndex = _playerIndex + 1; } [player replaceCurrentItemWithPlayerItem:item]; //快取play可以快速獲取對應的player [self.playerDict setObject:player forKey:messageID]; return player; } } //在進入聊天介面時建立player物件 - (void)creatMessagePlayer { if (self.playerArray.count > 0) { return; } dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (NSInteger i = 0; i < PlayerCount ; i++) { AVPlayer *player = [AVPlayer new]; //小視訊聊天介面播放無聲 player.volume = 0; [self.playerArray addObject:player]; } }); } //離開聊天介面時清除所有AVPlayer - (void)removeAllPlayer { [self.playerDict removeAllObjects]; for (AVPlayer *player in self.playerArray) { [player pause]; [player.currentItem cancelPendingSeeks]; [player.currentItem.asset cancelLoading]; [player replaceCurrentItemWithPlayerItem:nil]; } [self.playerArray removeAllObjects]; } |
這個方案經過較長時間使用能夠保持穩定,沒有出現什麼明顯問題。
小視訊在聊天介面播放第二種方案
直到看到iOS微信小視訊優化心得中:
另外AVPlayer在使用時會佔用AudioSession,這個會影響用到AudioSession的地方,如聊天視窗開啟小視訊功能。還有AVPlayer釋放時最好先把AVPlayerItem置空,否則會有解碼執行緒殘留著。最後是效能問題,如果聊天視窗連續播放幾個小視訊,列表滑動時會非常卡。通過Instrument測試效能,看不出哪裡耗時,懷疑是視訊播放互相搶鎖引起的。
開始重新開發文中提到的 AVAssetReader + AVAssetReaderTrackOutput 的方案,程式碼在我的DevelopPlayerDemo裡面。
由於文中程式碼不夠完整,我自己實現了一套類似的,區別在於簡單的使用定時器來獲取 CMSampleBufferRef
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
//定時器按照幀率獲取 self.timer = [NSTimer scheduledTimerWithTimeInterval:(1.0/self.frameRate) target:self selector:@selector(captureLoop) userInfo:nil repeats:YES]; - (void)captureLoop { dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self captureNext]; }); } - (void)captureNext { [self.lock lock]; [self processForDecoding]; [self.lock unlock]; } - (void)processForDecoding { if( self.assetReader.status != AVAssetReaderStatusReading ){ if(self.assetReader.status == AVAssetReaderStatusCompleted ){ if(!self.loop ){ [self.timer invalidate]; self.timer = nil; self.resetFlag = YES; self.currentTime = 0; [self releaseReader]; return; } else { self.currentTime = 0; [self initReader]; } if (self.delegate && [self.delegate respondsToSelector:@selector(videoDecoderDidFinishDecoding:)]) { [self.delegate videoDecoderDidFinishDecoding:self]; } } } CMSampleBufferRef sampleBuffer = [self.assetReaderOutput copyNextSampleBuffer]; if(!sampleBuffer ){ return; } self.currentTime = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)); CVImageBufferRef pixBuff = CMSampleBufferGetImageBuffer(sampleBuffer); if (self.delegate && [self.delegate respondsToSelector:@selector(videoDecoderDidDecodeFrame:pixelBuffer:)]) { [self.delegate videoDecoderDidDecodeFrame:self pixelBuffer:pixBuff]; } CMSampleBufferInvalidate(sampleBuffer); } |
播放和錄製視訊的CPU佔用,並不單單只是 Debug Session 的 CPU Report 裡直接寫的CPU佔用,還包括了系統程式 mediaserverd 對視訊解碼的處理,可以通過 Useage Comparison裡面的 Other Processes 看到,或者可以直接用 instruments 裡面的 Activity Monitor檢視。
後來監測CPU效能發現與APlayer相去甚遠,佔用率提高了超過100%。通過 CPU Report 用5S對比測試,AVPlayer的程式CPU基本都是0%,Other Processes 在60%左右。而自己的兩項資料大概是20%,100%左右。於是尋求更好的解決方案,希望能夠找到能夠GPU加速的方法。
後來經過一番查詢想到了使用 GPUImage 裡面的給通過給視訊加入濾鏡中使用 OpenGL ES 播放視訊的方案。新增修改完成以後再次測試發現效能上也並沒有質的提高,於是百思百思不得其解。直到後來突發奇想覺得有可能是AVPlayer 對視訊輸出解析度和質量會根據輸出的視窗大小進行一定程度上的壓縮
。於是試了試放大了 AVPlayerLayer 的 size,發現果然CPU的佔用率提高了,這也確認了我這個猜想。
於是給 AVAssetReaderTrackOutput 增加了 outputSettings 引數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
NSError *error = nil; AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:self.asset error:&error]; AVAssetTrack *assetTrack = [[self.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; CGSize outputSize = CGSizeZero; if (self.size.width > assetTrack.naturalSize.width) { outputSize = assetTrack.naturalSize; } else { outputSize= self.size; } NSDictionary *outputSettings = @{ (id)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange), (id)kCVPixelBufferWidthKey:@(outputSize.width), (id)kCVPixelBufferHeightKey:@(outputSize.height), }; AVAssetReaderTrackOutput *readerVideoTrackOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:assetTrack outputSettings:outputSettings]; |
最後就發現確實CPU佔用率已經降到了跟 AVPlayer 一個水平線上。只是本程式的 CPU 還是需要佔用10%左右,這個無法避免。
相關連線
iOS微信小視訊優化心得
在 iOS 上捕獲視訊
Core Image 和視訊
GPUImage
SCRecorder
AVPlayerDemo
AVFoundation Programming Guide
最後打個小廣告,我最近想換個新工作環境,座標深圳,有興趣的可以微博私信我或者直接發email給我,謝謝!
Weibo: @-湛藍_
Email: pepsikirk@gmail.com