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