小視頻是微信的一個(gè)重大創(chuàng)新功能,而在開發(fā)小視頻時(shí),由于這個(gè)功能比較新,需求也沒那么多,查閱了大量資料,包括查看各種官方文檔、下載所有的視頻官方 Demo 和去 GitHub 上面查看各種視頻庫,也踩了很多坑才完成了這個(gè)功能。這也是我在完成以后,想要做這樣一個(gè)小視頻的開源庫 PKShortVideo 的原因。
GitHub 鏈接:https://github.com/pepsikirk/PKShortVideo,歡迎 star 和提 issue 。

小視頻的錄制
錄制的第一種方案
錄制視頻最開始用的是網(wǎng)上找的案例 AVCaptureSession + AVCaptureMovieFileOutput 來錄制視頻,然后用 AVAssetExportSeeion 來轉(zhuǎn)碼壓縮視頻。這就遇到了問題,那就是
壓縮后視頻的分辨率以及保證預(yù)覽拍攝視頻與最終生成視頻圖像一致。
根據(jù) AVFoundation Programming Guide 的 Still and Video Media Capture 部分, AVCaptureSession 的分辨率所輸出的視頻分辨率是固定的由 AVCaptureSessionPreset 參數(shù)決定,無法達(dá)到需求所需要的分辨率(微信的小視頻分辨率為320 X 240)。所以先根據(jù)微信小視頻的分辨率選擇了一個(gè)最為接近的 AVCaptureSessionPresetMedium (分辨率豎屏情況下為360 X 480)。
預(yù)覽 Layer 的 videoGravity 模式我出于攝像頭位置據(jù)居中考慮使用的是 ResizeAspectFill :
AVCaptureVideoPreviewLayer *previewLayer = [self.recorder previewLayer];
previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
所以在保持長寬比的前提下,會縮放圖片,使圖片充滿容器 View 。這樣需要截取的視頻就為去掉上下兩端多余最中間的部分。
我通過查找以后找到的解決方案就是壓縮以后進(jìn)行處理,而 AVAssetExportSeeion 設(shè)定壓縮輸出后的質(zhì)量與 AVCaptureSession 類似,也是通過一個(gè)字符串類型 AVAssetExportPreset 來確定的,也并不能自定義分辨率。
按照這個(gè)思路尋找答案,后來經(jīng)過多番查找,發(fā)現(xiàn) AVAssetExportSeeion 有著這樣的一個(gè)接口提供自定義的設(shè)置。
/* 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;
最終代碼如下:(此代碼年久失修,不確定是否還能用,這里的只提供思路)
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 相信大家看到一下子這么多搞不清什么區(qū)別命名什么的又差不多的類都暈了吧 ,在這里就不細(xì)談了,有興趣的可以自行了解。
關(guān)鍵在于這段代碼
CGAffineTransform transform = CGAffineTransformMake(0, 8/9.0, -8/9.0, 0, 320, -93.3333333);
[videoCompositionLayerInstruction setTransform:transform atTime:kCMTimeZero];
通過設(shè)置transform可以變換各種視頻輸出樣式,包括只截取視頻某一部分和各種變換,CGAffineTransform可以好好理解一下,做動(dòng)畫也經(jīng)??梢杂玫剑ㄎ疫@里是寫死了只能截取原視頻分辨率為 360 X 480 情況下截取中間部分 320 X 240 的分辨率的情況,而且后來想再試試發(fā)現(xiàn)好像已經(jīng)不能用了,后來換了別的方式也沒有再改了)。
錄制第一種方案的缺點(diǎn)
如微信官方開發(fā)分享的iOS微信小視頻優(yōu)化心得里寫的:
于是用AVCaptureMovieFileOutput(640*480)直接生成視頻文件,拍視頻很流暢。然而錄制的6s視頻大小有2M+,再用MMovieDecoder+MMovieWriter壓縮至少要7~8s,影響聊天窗口發(fā)小視頻的速度。
這個(gè)方案需要拍攝完成以后再進(jìn)行轉(zhuǎn)碼壓縮,速度比較慢,影響用戶體驗(yàn),那有沒有一種方式可以直接在拍攝時(shí)就直接進(jìn)行轉(zhuǎn)碼壓縮呢?下面來看方案二。
錄制的第二種方案
再次經(jīng)過多番查找(最開始開發(fā)時(shí)并不知道微信官方分享的文章,順便吐槽一下微信訂閱號文章不能在 Google 搜索到),翻找官方 demo 和搜索,并沒有找到一個(gè)很好的錄制思路。后來想起來喵神主導(dǎo)的ObjC中國
)好像在之前有過一個(gè)視頻期刊模塊的分享,于是去看了一下就找到了這篇文章在 iOS 上捕獲視頻。
看完之后大有裨益,強(qiáng)烈推薦大家也去看看(ObjC 中國里的文章都很棒,感謝喵神主導(dǎo)的翻譯組)。并直接在上面找到VideoCaptureDemo。我最終的實(shí)現(xiàn)方案也是由這個(gè)改寫而成。
這里面也包括含有了UIImagePickerController,AVCaptureSession + AVMovieFileOutput,AVCaptureSession + AVAssetWriter 三種方案,UIImagePickerController 就是最基本的系統(tǒng)拍攝照片和錄制視頻的庫了,一般普通視頻和拍照時(shí)會用到。第二種就是我上面說的第一個(gè)錄制方案,最后一種就是我們想要的定制性最強(qiáng)的錄制方案了。
這個(gè)方案借用在 iOS 上捕獲視頻的一張圖和話:

如果你想要對影音輸出有更多的操作,你可以使用 AVCaptureVideoDataOutput 和 AVCaptureAudioDataOutput 而不是我們上節(jié)討論的 AVCaptureMovieFileOutput。 這些輸出將會各自捕獲視頻和音頻的樣本緩存,接著發(fā)送到它們的代理。代理要么對采樣緩沖進(jìn)行處理 (比如給視頻加濾鏡),要么保持原樣傳送。使用 AVAssetWriter 對象可以將樣本緩存寫入文件:
這個(gè)方案就相當(dāng)于自己對每一幀圖像都可以進(jìn)行處理,SCRecorder 就是用類似的方式做的。這種方案在 iPhone4上不會出現(xiàn)iOS微信小視頻優(yōu)化心得中說的:
在4s以上的設(shè)備拍攝小視頻挺流暢,幀率能達(dá)到要求。但是在iPhone4,錄制的時(shí)候特別卡,錄到的視頻只有6~8幀/秒。嘗試把錄制視頻時(shí)的界面動(dòng)畫去掉,稍微流暢些,幀率多了3~4幀/秒,還是不滿足需求。
這個(gè)的問題。由于微信方面沒有開源代碼,也無法對比,不過也就沒有其后面寫的其它問題了。
同樣的,這個(gè)方案也需要考慮
壓縮后視頻的分辨率以及保證預(yù)覽拍攝視頻與最終生成視頻圖像一致。
NSInteger numPixels = self.outputSize.width * self.outputSize.height;
//每像素比特
CGFloat bitsPerPixel = 6.0;
NSInteger bitsPerSecond = numPixels * bitsPerPixel;
// 碼率和幀率設(shè)置
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 分別是高和寬賦值是相反的。因?yàn)橐话阋匀擞^看的方向做為參考標(biāo)準(zhǔn)來說小視頻的分辨率 寬 X 高 是 320 X 240,而設(shè)備默認(rèn)的方向是 Landscape Left ,即設(shè)備向左偏移90度,所以實(shí)際的視頻分辨率就是 240 X 320 與一般認(rèn)為的相反。
由于小視頻是只支持豎屏拍攝即設(shè)備方向?yàn)?Portrait ,就可以固定設(shè)置self.videoInput.transform = CGAffineTransformMakeRotation(M_PI_2)固定向右偏移90°。通過MediaInfo查看出相當(dāng)于給輸出視頻添加了一個(gè)90°的角度信息,這樣在播放時(shí)就能通過角度信息對視頻進(jìn)行播放糾正。
AVVideoScalingModeResizeAspectFill 也是非常重要的參數(shù),對應(yīng)著 AVLayerVideoGravityResizeAspectFill 就可以統(tǒng)一截取中間部分,不會變形并且與預(yù)覽圖一致。達(dá)到可以自定義分辨率不會變形的功能。
小視頻的播放
小視頻點(diǎn)擊放大播放
小視頻點(diǎn)擊放大以后播放比較簡單,基本使用 MPMoviePlayerController (無法定制UI)和 AVPlayer(可以定制UI)可以解決。代碼可參考我的 PKShortVideo ?;蚬俜?demo AVPlayerDemo
小視頻在聊天界面播放第一種方案
聊天頁面的播放比較特殊,原因是需要能夠同時(shí)播放多個(gè)小視頻,并且在播放時(shí)滾動(dòng)界面也需要一定的流暢性,對性能要求比較高。
最初我的實(shí)現(xiàn)方案就是通過 AVPlayer 在聊天界面直接創(chuàng)建播放。但是很快就遇到了問題:
第一個(gè)是播放起來比較卡頓。后來通過測試,微信是有著立刻當(dāng)前顯示列表就停止播放,滾動(dòng)停止后才開始播放的優(yōu)化,使用以后流暢了很多。
第二就是 AVPlayer 最多只能夠創(chuàng)建16個(gè)播放的視頻,這個(gè)問題我后來通過一個(gè)單例管理類用簡單的算法來解決此問題。
- (AVPlayer *)getAVQueuePlayWithPlayerItem:(AVPlayerItem *)item messageID:(NSString *)messageID {
//通過messageID取Player對象
AVPlayer *player = self.playerDict[messageID];
if (player) {
//對象不等時(shí)替換player對象的item
if (player.currentItem != item) {
[player replaceCurrentItemWithPlayerItem:item];
}
return player;
} else {
//未在界面創(chuàng)建小視頻時(shí)返回nil
if (!self.playerArray.count) {
return nil;
}
//按順序平均分配player數(shù)組里面的player
AVPlayer *player = self.playerArray[_playerIndex];
if (_playerIndex == PlayerCount - 1) {
_playerIndex = 0;
} else {
_playerIndex = _playerIndex + 1;
}
[player replaceCurrentItemWithPlayerItem:item];
//緩存play可以快速獲取對應(yīng)的player
[self.playerDict setObject:player forKey:messageID];
return player;
}
}
//在進(jìn)入聊天界面時(shí)創(chuàng)建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];
}
});
}
//離開聊天界面時(shí)清除所有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];
}
這個(gè)方案經(jīng)過較長時(shí)間使用能夠保持穩(wěn)定,沒有出現(xiàn)什么明顯問題。
小視頻在聊天界面播放第二種方案
直到看到iOS微信小視頻優(yōu)化心得中:
另外 AVPlayer 在使用時(shí)會占用 AudioSession ,這個(gè)會影響用到 AudioSession 的地方,如聊天窗口開啟小視頻功能。還有AVPlayer釋放時(shí)最好先把 AVPlayerItem 置空,否則會有解碼線程殘留著。最后是性能問題,如果聊天窗口連續(xù)播放幾個(gè)小視頻,列表滑動(dòng)時(shí)會非???。通過 Instrument 測試性能,看不出哪里耗時(shí),懷疑是視頻播放互相搶鎖引起的。
開始重新開發(fā)文中提到的 AVAssetReader + AVAssetReaderTrackOutput 的方案,代碼在我的DevelopPlayerDemo里面。
由于文中代碼不夠完整,我自己實(shí)現(xiàn)了一套類似的,區(qū)別在于簡單的使用定時(shí)器來獲取 CMSampleBufferRef
//定時(shí)器按照幀率獲取
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 占用,還包括了系統(tǒng)進(jìn)程 mediaserverd 對視頻解碼的處理,可以通過 Useage Comparison里面的 Other Processes 看到,或者可以直接用 instruments 里面的 Activity Monitor 查看。
后來監(jiān)測 CPU 性能發(fā)現(xiàn)與 APlayer 相去甚遠(yuǎn),占用率提高了超過100%。通過 CPU Report 用5S對比測試,AVPlayer的進(jìn)程CPU基本都是0%,Other Processes 在60%左右。而自己的兩項(xiàng)數(shù)據(jù)大概是20%,100%左右。于是尋求更好的解決方案,希望能夠找到能夠GPU加速的方法。
后來經(jīng)過一番查找想到了使用 GPUImage 里面的給通過給視頻加入濾鏡中使用 OpenGL ES 播放視頻的方案。添加修改完成以后再次測試發(fā)現(xiàn)性能上也并沒有質(zhì)的提高,于是百思百思不得其解。直到后來突發(fā)奇想覺得有可能是AVPlayer 對視頻輸出分辨率和質(zhì)量會根據(jù)輸出的窗口大小進(jìn)行一定程度上的壓縮。于是試了試放大了 AVPlayerLayer 的 size,發(fā)現(xiàn)果然CPU的占用率提高了,這也確認(rèn)了我這個(gè)猜想。
于是給 AVAssetReaderTrackOutput 增加了 outputSettings 參數(shù)。
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];
最后就發(fā)現(xiàn)確實(shí) CPU 占用率已經(jīng)降到了跟 AVPlayer 一個(gè)水平線上。只是本進(jìn)程的 CPU 還是需要占用10%左右,這個(gè)無法避免。
相關(guān)連接
iOS微信小視頻優(yōu)化心得
在 iOS 上捕獲視頻
Core Image 和視頻
GPUImage
SCRecorder
AVPlayerDemo
AVFoundation Programming Guide