從QQ音樂開發,探討如何利用騰訊雲SDK在直播中加入視頻動畫
本文由騰訊遊戲雲發表於雲+社區專欄
看著精彩的德甲賽事,突然裁判一聲口哨,球賽斷掉了,屏幕開始自動播放“吃麥趣雞盒,看德甲比賽”的視頻廣告
那麽問題來了,如何在直播流中,無縫的插入點播視頻文件呢?
本文介紹了QQ音樂基於騰訊雲AVSDK,實現互動直播插播動畫的方案以及踩過的坑。
01
從產品經理給的需求說起
“開場動畫?插播廣告?”
不久之前,產品同學說我們要在音視頻直播中,加一個開場動畫。
要播放插播動畫,怎麽做呢?對於視頻直播來說,當前直播畫面流怎麽處理?對於音頻來說,又怎麽輸入一路流呢?
02
梳理技術方案
互動直播的方式,是把主播的畫面推送到觀眾面前,而主播端的畫面,既可以來自攝像頭采集的數據,也可以來自其它的輸入流。那麽如果騰訊雲的AVSDK能支持到播放輸入流,就能通過在主播端本地解碼一個視頻文件,然後把這路流的數據推到觀眾端的方式,讓所有的角色都能播放插播動畫了。幸運的是,騰訊雲AVSDK可以支持到這個特性,具體的方法有下面兩種:
第一種:替換視頻畫面
/*!
@abstract 對本地采集視頻進行預處理的回調。
@discussion 主線程回調,方面直接在回調中實現視頻渲染。
@param frameData 本地采集的視頻幀,對其中data數據的美顏、濾鏡、特效等圖像處理,會回傳給SDK編碼、發送,在遠端收到的視頻中生效。
@see QAVVideoFrame
*/
- (void)OnLocalVideoPreProcess:(QAVVideoFrame *)frameData;
主播側本地在采集到攝像頭的數據後,在編碼上行到服務器之前,會提供一個接口給予業務側做預處理的回調,所以,對於視頻直播,我們可以利用這個接口,把上行輸入的視頻畫面修改為要插播進來動畫的視頻幀,這樣,從觀眾角度看,被插播了視頻動畫。
第二種:使用外部輸入流
/*! @abstract 開啟外部視頻采集功能時,向SDK傳入外部采集的視頻幀。 @return QAV_OK 成功。 QAV_ERR_ROOM_NOT_EXIST 房間不存在,進房後調用才生效。 QAV_ERR_DEVICE_NOT_EXIST 視頻設備不存在。 QAV_ERR_FAIL 失敗。 @see QAVVideoFrame */ - (int)fillExternalCaptureFrame:(QAVVideoFrame *)frame;
最開始時,我錯誤的認為,僅僅使用第二種方式就能夠滿足同時在音視頻兩種直播中插播動畫的需求,但是實際實踐的時候發現,如果要播放外部輸入流,必須要先關閉攝像頭畫面。這個操作會引起騰訊雲後臺的視頻位切換,並通過下面這個函數通知到觀眾端:
/*!
@abstract 房間成員狀態變化通知的函數。
@discussion 當房間成員發生狀態變化(如是否發音頻、是否發視頻等)時,會通過該函數通知業務側。
@param eventID 狀態變化id,詳見QAVUpdateEvent的定義。
@param endpoints 發生狀態變化的成員id列表。
*/
- (void)OnEndpointsUpdateInfo:(QAVUpdateEvent)eventID endpointlist:(NSArray *)endpoints;
視頻位短時間內的切換,會導致一些時序上的問題,跟SDK側討論也認為不建議這樣做。最終,QQ音樂采用了兩個方案共存的方式。
03
視頻格式選型
對於插播動畫的視頻文件,如果考慮到如果需要支持流式播放,碼率低,高畫質,可以使用H264裸流+VideoToolBox硬解的方式。如果說只播放本地文件,可以采用H264編碼的mp4+AVURLAsset解碼的方式。因為目前還沒有流式播放的需求,而設計同學直接給到的是一個mp4文件,所以後者則看起來更合理。筆者出於個人興趣,對兩種方案的實現都做了嘗試,但是也遇到了下面的一些坑,總結一下,希望能讓其它同學少走點彎路:
1.分辨率與幀率的配置
視頻的分辨率需要與騰訊雲後臺的SPEAR引擎配置中的上行分辨率一致,QQ音樂選擇的視頻上行配置是960x540,幀率是15幀。但是實際的播放中,發現效果並不理想,所以需要播放更高分辨率的數據,這一步可以通過更換AVSDK的角色RoleName來實現,這裏不做延伸。
另外一個問題是從攝像頭采集上來的數據,是下圖的角度為1的圖像,在渲染的時候,會默認被旋轉90度,在更改視頻畫面時,需要保持兩者的一致性。攝像頭采集的數據格式是NV12,而本地填充畫面的格式可以是I420。在繪制時,可以根據數據格式來判斷是否需要旋轉圖像展示。
2.ffmpeg 轉h264裸流解碼問題
從iOS8開始,蘋果開放了VideoToolBox,使得應用程序擁有了硬解碼h264格式的能力。具體的實現與分析,可以參考《iOS-H264 硬解碼》這篇文章。因為設計同學給到的是一個mp4文件,所以首先需要先把mp4轉為H264的裸碼流,再做解碼。這裏我使用ffmpeg來做轉換:
ffmpeg -i test.mp4 -codec copy -bsf: h264_mp4toannexb -s 960*540 -f h264 output.264
其中,annexb就是h264裸碼流Elementary Stream的格式。對於Elementary Stream,sps跟pps並沒有單獨的包,而是附加在I幀前面,一般長這樣:
00 00 00 01 sps 00 00 00 01 pps 00 00 00 01 I 幀
VideoToolBox的硬解碼一般通過以下幾個步驟:
1. 讀取視頻流
2. 找出sps,pps的信息,創建CMVideoFormatDescriptionRef,傳入下一步作為參數
3. VTDecompressionSessionCreate:創建解碼會話
4. VTDecompressionSessionDecodeFrame:解碼一個視頻幀
5. VTDecompressionSessionInvalidate:釋放解碼會話
但是對上面轉換後的裸碼流解碼,發現總是會遇到解不出來數據的問題。分析轉換後的文件發現,轉換後的格式並不是純碼流,而被ffmpeg加入了一些無關的信息:
但是也不是沒有辦法,可以使用這個工具H264Naked來找出二進制文件中的這一段數據一並刪掉。再嘗試,發現依然播放不了,原因是在上面的第3步解碼會話創建失敗了,錯誤碼OSStatus = -5。很坑的是,這個錯誤碼在OSStatus.com中無法查到對應的錯誤信息,通過對比好壞兩個文件的差異發現,解碼失敗的文件中,pps 前面的 startcode並不是3個0開頭的,而是這樣子
00 00 00 01 sps 00 00 01 pps 00 00 00 01 I 幀
但是實際上,通過查看h264的官方文檔,發現兩種形式都是正確的
而我只考慮了第一種情況,卻忽略了第二種,導致解出來的pps數據錯了。通過手動插入一個00,或者×××兼容這種情況,都可以解決這個問題。但是同時也看出,這種方式很不直觀。所以也就引入了下面的第二種方法。
\3. AVAssetReader 解碼視頻
使用AVAssetReader解碼出yuv比較簡單,下面直接貼出代碼:
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[[NSURL alloc] initFileURLWithPath:path] options:nil];
NSError *error;
AVAssetReader* reader = [[AVAssetReader alloc] initWithAsset:asset error:&error];
NSArray* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
AVAssetTrack* videoTrack = [videoTracks objectAtIndex:0];
int m_pixelFormatType = kCVPixelFormatType_420YpCbCr8Planar;
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];
// 讀取視頻每一個buffer轉換成CGImageRef
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while ([reader status] == AVAssetReaderStatusReading && videoTrack.nominalFrameRate > 0) {
CMSampleBufferRef sampleBuff = [videoReaderOutput copyNextSampleBuffer];
// 對sampleBuff 做點什麽
});
這裏只說遇到的坑,有的mp4視頻解碼後繪制時會有一個迷之綠條,就像下面這個圖
這是為什麽,代碼實現如下所示,我們先取出y分量的數據,再取出uv分量的數據,看起來沒有問題,但是這實際上卻不是我們的視頻格式對應的數據存儲方式。
// 首先把Samplebuff轉成cvBufferRef, cvBufferRef中存儲了像素緩沖區的數據
CVImageBufferRef cvBufferRef = CMSampleBufferGetImageBuffer(sampleBuff);
// 鎖定地址,這樣才能之後從主存訪問到數據
CVPixelBufferLockBaseAddress(cvBufferRef, kCVPixelBufferLock_ReadOnly);
// 獲取y分量的數據
unsigned char *y_frame = (unsigned char*)CVPixelBufferGetBaseAddressOfPlane(cvBufferRef, 0);
// 獲取uv分量的數據
unsigned char *uv_frame = (unsigned char*)CVPixelBufferGetBaseAddressOfPlane(cvBufferRef, 1);
這份代碼cvBufferRef中存儲數據格式應該是:
typedef struct CVPlanarPixelBufferInfo_YCbCrPlanar CVPlanarPixelBufferInfo_YCbCrPlanar;
struct CVPlanarPixelBufferInfo_YCbCrBiPlanar {
CVPlanarComponentInfo componentInfoY;
CVPlanarComponentInfo componentInfoCbCr;
};
然而第一份代碼中,使用的pixelFormatType是kCVPixelFormatType_420YpCbCr8Planar,存儲的數據格式卻是:
typedef struct CVPlanarPixelBufferInfo CVPlanarPixelBufferInfo;
struct CVPlanarPixelBufferInfo_YCbCrPlanar {
CVPlanarComponentInfo componentInfoY;
CVPlanarComponentInfo componentInfoCb;
CVPlanarComponentInfo componentInfoCr;
};
也就是說,這裏應該把yuv按照三個分量來解碼,而不是兩個分量。 實現正確的解碼方式,成功消除了綠條。
至此,遇到的坑就都踩完了,效果也不錯。
最後,希望這篇文章能夠對你有所幫助,在直播開發上,少走點彎路
相關閱讀
欲練JS,必先攻CSS
交互微動效設計指南
【每日課程推薦】機器學習實戰!快速入門在線廣告業務及CTR相應知識
此文已由作者授權騰訊雲+社區發布,更多原文請點擊
搜索關註公眾號「雲加社區」,第一時間獲取技術幹貨,關註後回復1024 送你一份技術課程大禮包!
海量技術實踐經驗,盡在雲加社區!
從QQ音樂開發,探討如何利用騰訊雲SDK在直播中加入視頻動畫