從QQ音樂開發,探討如何利用騰訊雲SDK在直播中加入視頻動畫

歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~javascript

本文由 騰訊遊戲雲發表於 雲+社區專欄

看着精彩的德甲賽事,忽然裁判一聲口哨,球賽斷掉了,屏幕開始自動播放「吃麥趣雞盒,看德甲比賽」的視頻廣告java

那麼問題來了,如何在直播流中,無縫的插入點播視頻文件呢? 服務器

本文介紹了QQ音樂基於騰訊雲AVSDK,實現互動直播插播動畫的方案以及踩過的坑。機器學習

01async

從產品經理給的需求提及ide

「開場動畫?插播廣告?」函數

不久以前,產品同窗說咱們要在音視頻直播中,加一個開場動畫。工具

img

要播放插播動畫,怎麼作呢?對於視頻直播來講,當前直播畫面流怎麼處理?對於音頻來講,又怎麼輸入一路流呢?學習

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。在繪製時,能夠根據數據格式來判斷是否須要旋轉圖像展現。

img

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加入了一些無關的信息:

img

可是也不是沒有辦法,可使用這個工具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的官方文檔,發現兩種形式都是正確的

img

而我只考慮了第一種狀況,卻忽略了第二種,致使解出來的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視頻解碼後繪製時會有一個迷之綠條,就像下面這個圖

img

這是爲何,代碼實現以下所示,咱們先取出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按照三個份量來解碼,而不是兩個份量。 實現正確的解碼方式,成功消除了綠條。

img

至此,遇到的坑就都踩完了,效果也不錯。

最後,但願這篇文章可以對你有所幫助,在直播開發上,少走點彎路

相關閱讀
欲練JS,必先攻CSS
交互微動效設計指南
【每日課程推薦】機器學習實戰!快速入門在線廣告業務及CTR相應知識

此文已由做者受權騰訊雲+社區發佈,更多原文請點擊

搜索關注公衆號「雲加社區」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!

海量技術實踐經驗,盡在雲加社區

相關文章
相關標籤/搜索