iOS微信小視頻優化心得


小視頻是微信6.0版本重大功能之一,在開發過程中遇到不少問題。本文先敘述小視頻的產品需求,介紹了幾個實現方案,分析每個方案的優缺點,最后總結出最優的解決方案。

 

小視頻播放需求

 

  1. 可以同時播放多個視頻

  2. 用戶操作界面時視頻可以繼續播放

  3. 播放時不能卡住界面,視頻滑進界面內后要立即播放

  4. 視頻在列表內播放是靜音播放,點擊放大是有聲播放

     

 

小視頻播放方案

 

1. MPMoviePlayerController

MPMoviePlayerController是一個簡單易用的視頻播放控件,可以播放本地文件和網絡流媒體,支持mov、mp4、mpv、3gp等H.264和MPEG-4視頻編碼格式,支持拖動進度條、快進、后退、暫停、全屏等操作,並為開發者提供了一系列播放狀態事件通知。使用時先設置URL,然后把它的view add到某個parent view里,再調用play即可。

但這方案的缺點是,同一時間只能有一個MPMoviePlayerController對象播放,不滿足同時多個播放的需求;而且也不支持靜音播放。MPMoviePlayerController適合於全屏播放視頻的場景。

 

2. AVPlayer

AVPlayer是AVFoundation.Framework提供的偏向於底層的視頻播放控件,用起來復雜,但功能強大。單獨使用AVPlayer是無法顯示視頻的,要把它添加到AVPlayerLayer里才行。另外它需要配合AVPlayerItem使用,AVPlayerItem類似於MVC里的Model層,負責資源加載、視頻播放設置及播放狀態管理(通過KVO方式來觀察狀態)。它們關系如下:

 

 

首先創建一個AVPlayerItem對象:

NSURL* videoUrl = [NSURL fileURLWithPath:m_path isDirectory:NO];
m_playItem = [AVPlayerItem playerItemWithURL:videoUrl];
// 監聽playItem的status屬性
[m_playItem addObserver:self forKeyPath:@"status" 
    options:NSKeyValueObservingOptionNew context:nil];

接下來是創建AVPlayer和AVPlayerLayerView對象。AVPlayerLayerView是自定義的UIView,用於AVPlayer播放,其layerClass是AVPlayerLayer:

// AVPlayer
m_player = [AVPlayer playerWithPlayerItem:m_playItem];
m_player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
// AVPlayerLayerView
m_playerView = [[AVPlayerLayerView alloc] initWithFrame:self.bounds];
[self addSubview:m_playerView];
// 把AVPlayer添加到AVPlayerLayer
[(AVPlayerLayer*)[m_playerView layer] setPlayer:m_player];
// 觀察AVPlayerItem播放結束的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(itemPlayEnded:) 
    name:AVPlayerItemDidPlayToEndTimeNotification object:m_playItem];

AVPlayerItem的status屬性有三種狀態:AVPlayerStatusUnknown、AVPlayerStatusReadyToPlay及AVPlayerStatusFailed。當status=AVPlayerStatusReadyToPlay時,就代表視頻能播放了,此時調用AVPlayer的play方法就能播放視頻了。

相比MPMoviePlayerController,AVPlayer有最多可以同時播放16個視頻。另外AVPlayer在使用時會占用AudioSession,這個會影響用到AudioSession的地方,如聊天窗口開啟小視頻功能。還有AVPlayer釋放時最好先把AVPlayerItem置空,否則會有解碼線程殘留着。最后是性能問題,如果聊天窗口連續播放幾個小視頻,列表滑動時會非常卡。通過Instrument測試性能,看不出哪里耗時,懷疑是視頻播放互相搶鎖引起的。

 

3. AVAssetReader+AVAssetReaderTrackOutput

既然AVPlayer在播放視頻時會有性能問題,我們不如做自己的播放器。AVAssetReader可以從原始數據里獲取解碼后的音視頻數據。結合AVAssetReaderTrackOutput,能讀取一幀幀的CMSampleBufferRef。CMSampleBufferRef可以轉化成CGImageRef。為此,我們可以寫個MMovieDecoder的類,負責視頻解碼,每讀出一個SampleBuffer就往上層回調:

AVAssetReader* reader = [[AVAssetReader alloc] initWithAsset:m_asset error:&error];
NSArray* videoTracks = [m_asset tracksWithMediaType:AVMediaTypeVideo];
AVAssetTrack* videoTrack = [videoTracks objectAtIndex:0];
// 視頻播放時,m_pixelFormatType=kCVPixelFormatType_32BGRA
// 其他用途,如視頻壓縮,m_pixelFormatType=kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
NSDictionary* options = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:
        (int)m_pixelFormatType] forKey:(id)kCVPixelBufferPixelFormatTypeKey];
AVAssetReaderTrackOutput* videoReaderOutput = [[AVAssetReaderTrackOutput alloc] 
        initWithTrack:videoTrack outputSettings:options];
[reader addOutput:videoReaderOutput];
[reader startReading];
// 要確保nominalFrameRate>0,之前出現過android拍的0幀視頻
while ([reader status] == AVAssetReaderStatusReading && videoTrack.nominalFrameRate > 0) {
    // 讀取video sample
    CMSampleBufferRef videoBuffer = [videoReaderOutput copyNextSampleBuffer];
    [m_delegate mMovieDecoder:self onNewVideoFrameReady:videoBuffer);
    CFRelease(videoBuffer);    
    // 根據需要休眠一段時間;比如上層播放視頻時每幀之間是有間隔的
    [NSThread sleepForTimeInterval:sampleInternal];
}
    
// 告訴上層視頻解碼結束
[m_delegate mMovieDecoderOnDecodeFinished:self];

另一個是MVideoPlayerView,負責視頻的顯示,它接收MMovieDecoder回調的CMSampleBufferRef后,把它轉為CGImageRef,然后設置layer.contents為這個CGImageRef對象。創建CGImageRef不會做圖片數據的內存拷貝,它只會當Core Animation執行Transaction::commit()觸發layer -display時,才把圖片數據拷貝到layer buffer里。

AVAssetReader也能decode音頻的SampleBuffer,不過本人還沒想到如何播放CMSampleBufferRef的音頻,目前只能靜音播放。

 

4. 方案對比

對方案二、三做了滑動性能對比和耗電對比,測試條件分別是

滑動:在iPhone4的聊天窗口,有30個小視頻,來回做4次列表滑動

耗電:在iPhone5s,屏幕亮度調到最大,禁止自動鎖屏,開啟飛行模式,聊天窗口同時播放着3個小視頻,10分鍾

 

方案三無論滑動性能和耗電均優於方案二,由於方案三只能靜音播放,所以方案三用於聊天窗口和朋友圈列表播放,方案二用於點擊放大時的有聲播放。

 

小視頻錄制需求

  1. 支持白平衡、對焦、縮放

  2. 錄制視頻長度6秒,30幀/秒,盡量不丟幀

  3. 能錄制不同尺寸和碼率的視頻

 

小視頻錄制方案

對於需求1,AVFoundation有API可以支持,這里不多說。這里重點說說需求2、3的實現方案。

 

前期錄制方案如下:

  1. 創建AVCaptureSession,設置拍攝分辨率

  2. 添加AVCaptureInput,如攝像頭和麥克風

  3. 添加AVCaptureOutput,如AVCaptureVideoDataOutput、AVCaptureAudioDataOutput。這里AVCaptureAudioDataOutput建議在Session -startRunning后才添加,避免影響攝像頭啟動時間

  4. 添加AVCaptureVideoPreviewLayer,為用戶提供拍攝預覽界面

  5. 創建MMovieWriter,里面包含AVAssetWriter對象,用於寫視頻

  6. 開始捕捉-startRunning

  7. AVCaptureVideoDataOutput和AVCaptureAudioDataOutput不停地往MMovieWriter傳遞VideoSampleBuffer和AudioSampleBuffer,MMovieWriter對VideoSampleBuffer做分辨率壓縮,以及對AudioSampleBuffer做碼率壓縮

  8. 結束捕捉-stopRunning,MMovieWriter停止寫視頻,把生成的視頻文件拋給上層

在4s以上的設備拍攝小視頻挺流暢,幀率能達到要求。但是在iPhone4,錄制的時候特別卡,錄到的視頻只有6~8幀/秒。嘗試把錄制視頻時的界面動畫去掉,稍微流暢些,幀率多了3~4幀/秒,還是不滿足需求。通過Instrument檢測,發現跟寫音頻時的壓縮有關,寫音頻時阻塞了AVFoundation的線程,引起后續的丟幀。網上也有人反饋類似問題 http://stackoverflow.com/questions/16686076/performance-issues-with-avassetwriterinput-audio-and-single-core-devices。把寫音頻去掉后,幀率果然上去了。但是系統相機的拍攝視頻是非常流暢的。於是用AVCaptureMovieFileOutput(640*480)直接生成視頻文件,拍視頻很流暢。然而錄制的6s視頻大小有2M+,再用MMovieDecoder+MMovieWriter壓縮至少要7~8s,影響聊天窗口發小視頻的速度。

綜上所述,要想拍視頻不卡,就要在錄制過程中盡量不做CPU耗時操作,而且AVCaptureOutput傳遞數據給上層時不能卡住AV線程。最終想到個方案,加個Cache層,先把AVCaptureOutput傳遞的SampleBuffer緩存下來,不在AV的線程寫視頻;等CPU空閑時,再喚起movieWriter線程寫視頻。流程如下圖所示:

通過這樣處理,拍視頻流暢度跟系統相機接近了,只是剛拍的前1s幀數只有18幀,后面穩定到30幀/秒左右了。而且用戶松手拍完后,最多等1s就能把視頻寫完文件了;也優化了之前的視頻截圖生成接口,減少200ms。不過拍攝穩定性不夠好,經常出現下面的寫失敗錯誤,頻率大概是6次/100次:

[GL] <MMovieWriter.mm:476::-[MMovieWriter appendAudioSampleBufferInternal:]> INFO: audio writer status 3, desc Error Domain=AVFoundationErrorDomain Code=-11800 "這項操作無法完成" UserInfo=0x11495910 {NSLocalizedDescription=這項操作無法完成, NSUnderlyingError=0x1146e8d0 "The operation couldn’t be completed. (OSStatus error -12633.)", NSLocalizedFailureReason=發生未知錯誤(-12633)}

通過google搜索,網上說這錯誤原因是同一個FrameTime寫入了兩幀。但是FrameTime是從SampleBuffer里取的,理論上不會時間重合(我沒打log驗證);而且老方案沒出現這種錯誤,新方案延后處理才會出現的。經過多次試驗,把Buffer Cache設置上限,當Buffer數達到一定數量后強制讓MovieWriter寫入文件,同時把下面這行代碼注釋,錯誤不再出現了:

//m_writer.movieFragmentInterval = CMTimeMakeWithSeconds(1.0, 1000); // AVAssetWriter


方案對比:

在iPhone4聊天窗口拍攝若干個6s視頻10次,算平均值


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM