日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

美团外卖商家端视频探索之旅

發(fā)布時(shí)間:2025/3/21 编程问答 48 豆豆
生活随笔 收集整理的這篇文章主要介紹了 美团外卖商家端视频探索之旅 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

美團(tuán)外賣商家端視頻探索之旅

背景

美團(tuán)外賣至今已迅猛發(fā)展了六年,隨著外賣業(yè)務(wù)量級(jí)與日俱增,單一的文字和圖片已無(wú)法滿足商家的需求,商家迫切需要更豐富的商品描述手段吸引用戶,增加流量,進(jìn)而提高下單轉(zhuǎn)化率和下單量。商品視頻的引入,在一定程度上可以提升商品信息描述豐富度,以更加直觀的方式為商家引流,增加收益。為此,商家端引入了視頻功能,進(jìn)行了一系列視頻功能開(kāi)發(fā),核心功能包含視頻處理(混音,濾鏡,加水印,動(dòng)畫(huà)等)、視頻拍攝、合成等,最終效果圖如下所示:

自視頻功能上線后,每周視頻樣本量及使用視頻的商家量大幅增加,視頻錄制成功率達(dá)99.533%,視頻處理成功率98.818%,音頻處理成功率99.959%,Crash率穩(wěn)定在0.1‰,穩(wěn)定性高且可用性強(qiáng)。目前,視頻功能已在蜜蜂App、閃購(gòu)業(yè)務(wù)和商家業(yè)務(wù)上使用。

對(duì)于視頻鏈路的開(kāi)發(fā),我們經(jīng)歷了方案選型、架構(gòu)設(shè)計(jì)及優(yōu)化、業(yè)務(wù)實(shí)踐、功能測(cè)試、監(jiān)控運(yùn)維、更新維護(hù)等各個(gè)環(huán)節(jié),核心環(huán)節(jié)如下圖所示。在開(kāi)發(fā)過(guò)程中,我們遇到了各種技術(shù)問(wèn)題和挑戰(zhàn),下文會(huì)針對(duì)遇到的問(wèn)題、挑戰(zhàn),及其解決方案進(jìn)行重點(diǎn)闡述。

方案選型

在方案選型時(shí),重點(diǎn)對(duì)核心流程和視頻格式進(jìn)行選型。我們以功能覆蓋度、穩(wěn)定性及效率、可定制性、成本及開(kāi)源性做為核心指標(biāo),從而衡量方案的高可用性和可行性。

1.核心流程選型?

視頻開(kāi)發(fā)涉及的核心流程包括播放、錄制、合成、裁剪、后期處理(編解碼、濾鏡、混音、動(dòng)畫(huà)、水印)等。結(jié)合商家端業(yè)務(wù)場(chǎng)景,我們有針對(duì)性的進(jìn)行方案調(diào)研。重點(diǎn)調(diào)研了業(yè)界現(xiàn)有方案,如阿里的云視頻點(diǎn)播方案、騰訊云視頻點(diǎn)播方案、大眾點(diǎn)評(píng)App的UGC方案,及其它的一些第三方開(kāi)源方案等,并進(jìn)行了整體匹配度的對(duì)比,如下圖所示:

阿里和騰訊的云視頻點(diǎn)播方案比較成熟,集成度高,且能力豐富,穩(wěn)定性及效率也很高。但兩者成本較高,需要收費(fèi),且SDK大小均在15M以上,對(duì)于我們的業(yè)務(wù)場(chǎng)景來(lái)說(shuō)有些過(guò)于臃腫,定制性較弱,無(wú)法迅速的支持我們做定制性擴(kuò)展。?

當(dāng)時(shí)的點(diǎn)評(píng)App UGC方案,基礎(chǔ)能力是滿足的,但因業(yè)務(wù)場(chǎng)景差異:

  • 比如外賣的視頻拍攝功能要求在豎屏下保證16:9的視頻寬高比,這就需要對(duì)原有的采集區(qū)域進(jìn)行截取,視頻段落的裁剪支持不夠等,業(yè)務(wù)場(chǎng)景的差異導(dǎo)致了實(shí)現(xiàn)方案存在巨大的差異,故放棄了點(diǎn)評(píng)App UGC方案。其他的一些開(kāi)源方案(比如Grafika等),也無(wú)法滿足要求,這里不再一一贅述。?

通過(guò)技術(shù)調(diào)研和分析,吸取各開(kāi)源項(xiàng)目的優(yōu)點(diǎn),并參考點(diǎn)評(píng)App UGC、Google CTS方案,對(duì)核心流程做了最終的方案選型,打造一個(gè)適合我們業(yè)務(wù)場(chǎng)景的方案,如下表所示:

2.視頻格式選型?

  • 采用H.264的視頻協(xié)議:H.264的標(biāo)準(zhǔn)成熟穩(wěn)定,普及率高。其最大的優(yōu)勢(shì)是具有很高的數(shù)據(jù)壓縮比率,在同等圖像質(zhì)量的條件下,H.264的壓縮比是MPEG-2的2倍以上,是MPEG-4的1.5~2倍。?
  • 采用AAC的音頻協(xié)議:AAC是一種專為聲音數(shù)據(jù)設(shè)計(jì)的文件壓縮格式。它采用了全新的算法進(jìn)行編碼,是新一代的音頻有損壓縮技術(shù),具有更加高效,更具有”性價(jià)比“的特點(diǎn)。

整體架構(gòu)

我們整體的架構(gòu)設(shè)計(jì),用以滿足業(yè)務(wù)擴(kuò)展和平臺(tái)化需要,可復(fù)用、可擴(kuò)展,且可快速接入。架構(gòu)采用分層設(shè)計(jì),基礎(chǔ)能力和組件進(jìn)行下沉,業(yè)務(wù)和視頻能力做分離,最大化降低業(yè)務(wù)方的接入成本,三方業(yè)務(wù)只需要接入視頻基礎(chǔ)SDK,直接使用相關(guān)能力組件或者工具即可。

整體架構(gòu)分為四層,分別為平臺(tái)層、核心能力層、基礎(chǔ)組件層、業(yè)務(wù)層。?

  • 平臺(tái)層:依賴系統(tǒng)提供的平臺(tái)能力,比如Camera、OpenGL、MediaCodec和MediaMuxer等,也包括引入的平臺(tái)能力,比如ijkplayer播放器、mp4parser?。
  • 核心能力層:該層提供了視頻服務(wù)的核心能力,包括音視頻編解碼、音視頻的轉(zhuǎn)碼引擎、濾鏡渲染能力等?。
  • 基礎(chǔ)能力層:暴露了基礎(chǔ)組件和能力,提供了播放、裁剪、錄屏等基礎(chǔ)組件和對(duì)應(yīng)的基礎(chǔ)工具類,并提供了可定制的播放面板,可定制的緩存接口等。?
  • 業(yè)務(wù)層:包括段落拍攝、自由拍攝、視頻空間、拍攝模版預(yù)覽及加載等。

我們的視頻能力層對(duì)業(yè)務(wù)層是透明的,業(yè)務(wù)層與能力層隔離,并對(duì)業(yè)務(wù)層提供了部分定制化的接口支持,這樣的設(shè)計(jì)降低了業(yè)務(wù)方的接入成本,并方便業(yè)務(wù)方的擴(kuò)展,比如支持蜜蜂App的播放面板定制,還支持緩存策略、編解碼策略的可定制。整體設(shè)計(jì)如下圖所示:

實(shí)踐經(jīng)驗(yàn)

在視頻開(kāi)發(fā)實(shí)踐中,因業(yè)務(wù)場(chǎng)景的復(fù)雜性,我們遇到了多種問(wèn)題和挑戰(zhàn)。下面以核心功能為基點(diǎn),圍繞各功能遇到的問(wèn)題做詳細(xì)介紹。

視頻播放

播放器是視頻播放基礎(chǔ)。針對(duì)播放器,我們進(jìn)行了一系列的方案調(diào)研和選擇。在此環(huán)節(jié),遇到的挑戰(zhàn)如下:

1.兼容性問(wèn)題

2.緩存問(wèn)題

針對(duì)兼容性問(wèn)題,Android有原生的MediaPlayer,但其版本兼容問(wèn)題偏多且支持格式有限,而我們需要支持播放本地視頻,本地視頻格式又無(wú)法控制,故該方案被舍棄。ijkplayer基于FFmpeg,與MediaPlayer相比,優(yōu)點(diǎn)比較突出:具備跨平臺(tái)能力,支持Android與iOS;提供了類似MediaPlayer的API,可兼容不同版本;可實(shí)現(xiàn)軟硬解碼自由切換,擁有FFmpeg的能力,支持多種流媒體協(xié)議。基于上述原因,我們最終決定選用ijkplayer。

但緊接著我們又發(fā)現(xiàn)ijkplayer本身不支持邊緩存邊播放,頻繁的加載視頻導(dǎo)致耗費(fèi)大量的流量,且在弱網(wǎng)或者3G網(wǎng)絡(luò)下很容易導(dǎo)致播放卡頓,所以這里就衍生出了緩存的問(wèn)題。

針對(duì)緩存問(wèn)題,我們引入AndroidVideoCache的技術(shù)方案,利用本地的代理去請(qǐng)求數(shù)據(jù),先本地保存文件緩存,客戶端通過(guò)Socket讀取本地的文件緩存進(jìn)行視頻播放,這樣就做到了邊播放邊緩存的策略,流程如下圖:

此外,我們還對(duì)AndroidVideoCache做了一些技術(shù)改造:

  • 優(yōu)化緩存策略。針對(duì)緩存策略的單一性,支持有限的最大文件數(shù)和文件大小問(wèn)題,我們調(diào)整為由業(yè)務(wù)方可以動(dòng)態(tài)定制緩存策略;
  • 解決內(nèi)存泄露隱患。對(duì)其頁(yè)面退出時(shí)請(qǐng)求不關(guān)閉會(huì)導(dǎo)致的內(nèi)存泄露,我們?yōu)槠涮砑恿送暾纳芷诒O(jiān)控,解決了內(nèi)存泄露問(wèn)題。

視頻錄制

在視頻拍攝的時(shí)候,最為常用的方式是采用MediaRecorder+Camera技術(shù),采集攝像頭可見(jiàn)區(qū)域。但因我們的業(yè)務(wù)場(chǎng)景要求視頻采集的時(shí)候,只錄制采集區(qū)域的部分區(qū)域且比例保持寬高比16:9,在保證預(yù)覽圖像不拉伸的情況下,只能對(duì)完整的采集區(qū)域做裁剪,這無(wú)形增加了開(kāi)發(fā)難度和挑戰(zhàn)。通過(guò)大量的資料分析,我們重點(diǎn)調(diào)研了有兩種方案:

  • Camera+AudioRecord+MediaCodec+Surface
  • MediaRecorder+MediaCodec
  • 方案1需要Camera采集YUV幀,進(jìn)行截取采集,最后再將YUV幀和PCM幀進(jìn)行編碼生成mp4文件,雖然其效率高,但存在不可把控的風(fēng)險(xiǎn)。

    方案2綜合評(píng)估后是改造風(fēng)險(xiǎn)最小的。?綜合成本和風(fēng)險(xiǎn)考量,我們保守的采用了方案2,該方案是對(duì)裁剪區(qū)域進(jìn)行坐標(biāo)換算(如果用前置攝像頭拍攝錄制視頻,會(huì)出現(xiàn)預(yù)覽畫(huà)面和錄制的視頻是鏡像的問(wèn)題,需要處理)。當(dāng)錄制完視頻后,生成了mp4文件,用MediaCodec對(duì)其編碼,在編碼階段再利用OpenGL做內(nèi)容區(qū)域的裁剪來(lái)實(shí)現(xiàn)。但該方案又引發(fā)了如下挑戰(zhàn):

    (1)對(duì)焦問(wèn)題

    因我們對(duì)采集區(qū)域做了裁剪,引發(fā)了點(diǎn)觸對(duì)焦問(wèn)題。比如用戶點(diǎn)擊了相機(jī)預(yù)覽畫(huà)面,正常情況下會(huì)觸發(fā)相機(jī)的對(duì)焦動(dòng)作,但是用戶的點(diǎn)擊區(qū)域只是預(yù)覽畫(huà)面的部分區(qū)域,這就導(dǎo)致了相機(jī)的對(duì)焦區(qū)域錯(cuò)亂,不能正常進(jìn)行對(duì)焦。后期經(jīng)過(guò)問(wèn)題排查,對(duì)點(diǎn)觸區(qū)域再次進(jìn)行相應(yīng)的坐標(biāo)變換,最終得到正確的對(duì)焦區(qū)域。

    (2)兼容適配

    我們的視頻錄制利用MediaRecorder,在獲取配置信息時(shí),由于Android碎片化問(wèn)題,不同的設(shè)備支持的配置信息不同,所以就會(huì)出現(xiàn)設(shè)備適配問(wèn)題。

    // VIVO Y66 模版拍攝時(shí)候,播放某些有問(wèn)題的視頻文件的同時(shí)去錄制視頻,會(huì)導(dǎo)致MediaServer掛掉的問(wèn)題// 發(fā)現(xiàn)將1080P尺寸的配置降低到720P即可避免此問(wèn)題// 但是720P尺寸的配置下,又存在綠邊問(wèn)題,因此再降到480if(isVIVOY66() && mMediaServerDied) {return getCamcorderProfile(CamcorderProfile.QUALITY_480P);}//SM-C9000,在1280 x 720 分辨率時(shí)有一條綠邊。網(wǎng)上有種說(shuō)法是GPU對(duì)數(shù)據(jù)進(jìn)行了優(yōu)化,使得GPU產(chǎn)生的圖像分辨率//和常規(guī)分辨率存在微小差異,造成圖像色彩混亂,修復(fù)后存在綠邊問(wèn)題。//測(cè)試發(fā)現(xiàn),降低分辨率或者升高分辨率都可以繞開(kāi)這個(gè)問(wèn)題。if (VideoAdapt.MODEL_SM_C9000.equals(Build.MODEL)) {return getCamcorderProfile(CamcorderProfile.QUALITY_HIGH);}// 優(yōu)先選擇 1080 P的配置CamcorderProfile camcorderProfile = getCamcorderProfile(CamcorderProfile.QUALITY_1080P);if (camcorderProfile == null) {camcorderProfile = getCamcorderProfile(CamcorderProfile.QUALITY_720P);}// 某些機(jī)型上這個(gè) QUALITY_HIGH 有點(diǎn)問(wèn)題,可能通過(guò)這個(gè)參數(shù)拿到的配置是1080p,所以這里也可能拿不到if (camcorderProfile == null) {camcorderProfile = getCamcorderProfile(CamcorderProfile.QUALITY_HIGH);}// 兜底if (camcorderProfile == null) {camcorderProfile = getCamcorderProfile(CamcorderProfile.QUALITY_480P);}

    視頻合成

    我們的視頻拍攝有段落拍攝這種場(chǎng)景,商家可根據(jù)事先下載的模板進(jìn)行分段拍攝,最后會(huì)對(duì)每一段的視頻做拼接,拼接成一個(gè)完整的mp4文件。mp4由若干個(gè)Box組成,所有數(shù)據(jù)都封裝在Box中,且Box可再包含Box的被稱為Container Box。mp4中Track表示一個(gè)視頻或音頻序列,是Sample的集合,而Sample又可分為Video Smaple和Audio Sample。Video Smaple代表一幀或一組連續(xù)視頻幀,Audio Sample即為一段連續(xù)的壓縮音頻數(shù)據(jù)。(詳見(jiàn)mp4文件結(jié)構(gòu)。)

    基于上面的業(yè)務(wù)場(chǎng)景需要,視頻合成的基礎(chǔ)能力我們采用mp4parser技術(shù)實(shí)現(xiàn)(也可用FFmpeg等其他手段)。mp4parser在拼接視頻時(shí),先將視頻的音軌和視頻軌進(jìn)行分離,然后進(jìn)行視頻和音頻軌的追加,最終將合成后的視頻軌和音頻軌放入容器里(這里的容器就是mp4的Box)。采用mp4parser技術(shù)簡(jiǎn)單高效,API設(shè)計(jì)簡(jiǎn)潔清晰,滿足需求。

    但我們發(fā)現(xiàn)某些被編碼或處理過(guò)的mp4文件可能會(huì)存在特殊的Box,并且mp4parser是不支持的。經(jīng)過(guò)源碼分析和原因推導(dǎo),發(fā)現(xiàn)當(dāng)遇到這種特殊格式的Box時(shí),會(huì)申請(qǐng)分配一個(gè)比較大的空間用來(lái)存放數(shù)據(jù),很容易造成OOM(內(nèi)存溢出),見(jiàn)下圖所示。于是,我們對(duì)這種拼接場(chǎng)景下做了有效規(guī)避,僅在段落拍攝下使用mp4parser的拼接功能,保證我們處理過(guò)的文件不會(huì)包含這種特殊的Box。

    視頻裁剪

    我們剛開(kāi)始采用mp4parser技術(shù)完成視頻裁剪,在實(shí)踐中發(fā)現(xiàn)其精度誤差存在很大的問(wèn)題,甚至?xí)绊懻5臉I(yè)務(wù)需求。比如我們禁止裁剪出3s以下的視頻,但是由于mp4parser產(chǎn)生的精度誤差,導(dǎo)致4-5s的視頻很容易裁剪出少于3s的視頻。究其原因,mp4parser只能在關(guān)鍵幀(又稱I幀,在視頻編碼中是一種自帶全部信息的獨(dú)立幀)進(jìn)行切割,這樣就可能存在一些問(wèn)題。比如在視頻截取的起始時(shí)間位置并不是關(guān)鍵幀,因此會(huì)造成誤差,無(wú)法保證精度而且是秒級(jí)誤差。以下為mp4parser裁剪的關(guān)鍵代碼:

    public static double correctTimeToSyncSample(Track track, double cutHere, boolean next) {double[] timeOfSyncSamples = new double[track.getSyncSamples().length];long currentSample = 0;double currentTime = 0;for (int i = 0; i < track.getSampleDurations().length; i++) {long delta = track.getSampleDurations()[i];int index = Arrays.binarySearch(track.getSyncSamples(), currentSample + 1);if (index >= 0) {timeOfSyncSamples[index] = currentTime;}currentTime += ((double) delta / (double) track.getTrackMetaData().getTimescale());currentSample++;}double previous = 0;for (double timeOfSyncSample : timeOfSyncSamples) {if (timeOfSyncSample > cutHere) {if (next) {return timeOfSyncSample;} else {return previous;}}previous = timeOfSyncSample;}return timeOfSyncSamples[timeOfSyncSamples.length - 1]; }

    為了解決精度問(wèn)題,我們廢棄了mp4parser,采用MediaCodec的方案,雖然該方案會(huì)增加復(fù)雜度,但是誤差精度大大降低。

    方案具體實(shí)施如下:先獲得目標(biāo)時(shí)間的上一幀信息,對(duì)視頻解碼,然后根據(jù)起始時(shí)間和截取時(shí)長(zhǎng)進(jìn)行切割,最后將裁剪后的音視頻信息進(jìn)行壓縮編碼,再封裝進(jìn)mp4容器中,這樣我們的裁剪精度從秒級(jí)誤差降低到微秒級(jí)誤差,大大提高了容錯(cuò)率。

    視頻處理

    視頻處理是整個(gè)視頻能力最核心的部分,會(huì)涉及硬編解碼(遵循OpenMAX框架)、OpenGL、音頻處理等相關(guān)能力。?

    下圖是視頻處理的核心流程,會(huì)先將音視頻做分離,并行處理音視頻的編解碼,并加入特效處理,最后合成進(jìn)一個(gè)mp4文件中。

    在實(shí)踐過(guò)程中,我們遇到了一些需要特別注意的問(wèn)題,比如開(kāi)發(fā)時(shí)遇到的坑,嚴(yán)重的兼容性問(wèn)題(包括硬件兼容性和系統(tǒng)版本兼容性問(wèn)題)等。下面重點(diǎn)講幾個(gè)有代表性的問(wèn)題。

    1.偶數(shù)寬高的編解碼器

    視頻經(jīng)過(guò)編碼后輸出特定寬高的視頻文件時(shí)出現(xiàn)了如下錯(cuò)誤,信息里僅提示了Colorformat錯(cuò)誤,具體如下:

    查閱大量資料,也沒(méi)能解釋清楚這個(gè)異常的存在。基于日志錯(cuò)誤信息,并通過(guò)系統(tǒng)源碼定位,也只是發(fā)現(xiàn)了是和設(shè)置的參數(shù)不兼容導(dǎo)致的。經(jīng)過(guò)反復(fù)的試錯(cuò),最后確認(rèn)是部分編解碼器只支持偶數(shù)的視頻寬高,所以我們對(duì)視頻的寬高做了偶數(shù)限制。引起該問(wèn)題的核心代碼如下:

    status_t ACodec::setupVideoEncoder(const char *mime, const sp<AMessage> &msg,sp<AMessage> &outputFormat, sp<AMessage> &inputFormat) {if (!msg->findInt32("color-format", &tmp)) {return INVALID_OPERATION;}OMX_COLOR_FORMATTYPE colorFormat =static_cast<OMX_COLOR_FORMATTYPE>(tmp);status_t err = setVideoPortFormatType(kPortIndexInput, OMX_VIDEO_CodingUnused, colorFormat);if (err != OK) {ALOGE("[%s] does not support color format %d",mComponentName.c_str(), colorFormat);return err;}....... } status_t ACodec::setVideoPortFormatType(OMX_U32 portIndex,OMX_VIDEO_CODINGTYPE compressionFormat,OMX_COLOR_FORMATTYPE colorFormat,bool usingNativeBuffers) {......for (OMX_U32 index = 0; index <= kMaxIndicesToCheck; ++index) {format.nIndex = index;status_t err = mOMX->getParameter(mNode, OMX_IndexParamVideoPortFormat,&format, sizeof(format));if (err != OK) {return err;}...... }

    2. 顏色格式

    我們?cè)谔幚硪曨l幀的時(shí)候,一開(kāi)始獲得的是從Camera讀取到的基本的YUV格式數(shù)據(jù),如果給編碼器設(shè)置YUV幀格式,需要考慮YUV的顏色格式。這是因?yàn)閅UV根據(jù)其采樣比例,UV分量的排列順序有很多種不同的顏色格式,Android也支持不同的YUV格式,如果顏色格式不對(duì),會(huì)導(dǎo)致花屏等問(wèn)題。

    3. 16位對(duì)齊

    這也是硬編碼中老生常談的問(wèn)題了,因?yàn)镠264編碼需要16*16的編碼塊大小。如果一開(kāi)始設(shè)置輸出的視頻寬高沒(méi)有進(jìn)行16字節(jié)對(duì)齊,在某些設(shè)備(華為,三星等)就會(huì)出現(xiàn)綠邊,或者花屏。

    4. 二次渲染

    4.1 視頻旋轉(zhuǎn)

    在最后的視頻處理階段,用戶可以實(shí)時(shí)的看到加濾鏡后的視頻效果。這就需要對(duì)原始的視頻幀進(jìn)行二次處理,然后在播放器的Surface上渲染。首先我們需要OpenGL 的渲染環(huán)境(通過(guò)OpenGL的固有流程創(chuàng)建),渲染環(huán)境完成后就可以對(duì)視頻的幀數(shù)據(jù)進(jìn)行二次處理了。通過(guò)SurfaceTexture的updateTexImage接口,可將視頻流中最新的幀數(shù)據(jù)更新到對(duì)應(yīng)的GL紋理,再操作GL紋理進(jìn)行濾鏡、動(dòng)畫(huà)等處理。在處理視頻幀數(shù)據(jù)的時(shí)候,首先遇到的是角度問(wèn)題。在正常播放下(不利用OpenGL處理情況下)通過(guò)設(shè)置TextureView的角度(和視頻的角度做轉(zhuǎn)換)就可以解決,但是加了濾鏡后這一方案就失效了。原因是視頻的原始數(shù)據(jù)經(jīng)過(guò)紋理處理再渲染到Surface上,單純?cè)O(shè)置TextureView的角度就失效了,解決方案就是對(duì)OpenGL傳入的紋理坐標(biāo)做相應(yīng)的旋轉(zhuǎn)(依據(jù)視頻的本身的角度)。

    4.2 渲染停滯

    視頻在二次渲染后會(huì)出現(xiàn)偶現(xiàn)的畫(huà)面停滯現(xiàn)象,主要是SurfaceTexture的OnFrameAvailableListener不返回?cái)?shù)據(jù)了。該問(wèn)題的根本原因是GPU的渲染和視頻幀的讀取不同步,進(jìn)而導(dǎo)致SurfaceTexture的底層核心BufferQueue讀取Buffer出了問(wèn)題。下面我們通過(guò)BufferQueue的機(jī)制和核心源碼深入研究下:

    首先從二次渲染的工作流程入手。從圖像流(來(lái)自Camera預(yù)覽、視頻解碼、GL繪制場(chǎng)景等)中獲得幀數(shù)據(jù),此時(shí)OnFrameAvailableListener會(huì)回調(diào)。再調(diào)用updateTexImage(),會(huì)根據(jù)內(nèi)容流中最近的圖像更新SurfaceTexture對(duì)應(yīng)的GL紋理對(duì)象。我們?cè)賹?duì)紋理對(duì)象做處理,比如添加濾鏡等效果。SurfaceTexture底層核心管理者是BufferQueue,本身基于生產(chǎn)者消費(fèi)者模式。

    BufferQueue管理的Buffer狀態(tài)分為:FREE,DEQUEUED,QUEUED,ACQUIRED,SHARED。當(dāng)Producer需要填充數(shù)據(jù)時(shí),需要先Dequeue一個(gè)Free狀態(tài)的Buffer,此時(shí)Buffer的狀態(tài)為DEQUEUED,成功后持有者為Producer。隨后Producer填充數(shù)據(jù)完畢后,進(jìn)行Queue操作,Buffer狀態(tài)流轉(zhuǎn)為QUEUED,且Owner變?yōu)锽ufferQueue,同時(shí)會(huì)回調(diào)BufferQueue持有的ConsumerListener的onFrameAvailable,進(jìn)而通知Consumer可對(duì)數(shù)據(jù)進(jìn)行二次處理了。Consumer先通過(guò)Acquire操作,獲取處于QUEUED狀態(tài)的Buffer,此時(shí)Owner為Consumer。當(dāng)Consumer消費(fèi)完Buffer后,會(huì)執(zhí)行Release,該Buffer會(huì)流轉(zhuǎn)回BufferQueue以便重用。BufferQueue核心數(shù)據(jù)為GraphicBuffer,而GraphicBuffer會(huì)根據(jù)場(chǎng)景、申請(qǐng)的內(nèi)存大小、申請(qǐng)方式等的不同而有所不同。

    SurfaceTexture的核心流程如下圖:

    通過(guò)上圖可知,我們的Producer是Video,填充視頻幀后,再對(duì)紋理進(jìn)行特效處理(濾鏡等),最后再渲染出來(lái)。前面我們分析了BufferQueue的工作流程,但是在Producer要填充數(shù)據(jù),執(zhí)行dequeueBuffer操作時(shí),如果有Buffer已經(jīng)QUEUED,且申請(qǐng)的dequeuedCount大于mMaxDequeuedBufferCount,就不會(huì)再繼續(xù)申請(qǐng)F(tuán)ree Buffer了,Producer就無(wú)法DequeueBuffer,也就導(dǎo)致onFrameAvailable無(wú)法最終調(diào)用,核心源碼如下:

    status_t BufferQueueProducer::dequeueBuffer(int *outSlot,sp<android::Fence> *outFence, uint32_t width, uint32_t height,PixelFormat format, uint32_t usage,FrameEventHistoryDelta* outTimestamps) {......int found = BufferItem::INVALID_BUFFER_SLOT;while (found == BufferItem::INVALID_BUFFER_SLOT) {status_t status = waitForFreeSlotThenRelock(FreeSlotCaller::Dequeue,& found);if (status != NO_ERROR) {return status;}}...... } status_t BufferQueueProducer::waitForFreeSlotThenRelock(FreeSlotCaller caller,int*found) const{......while (tryAgain) {int dequeuedCount = 0;int acquiredCount = 0;for (int s : mCore -> mActiveBuffers) {if (mSlots[s].mBufferState.isDequeued()) {++dequeuedCount;}if (mSlots[s].mBufferState.isAcquired()) {++acquiredCount;}}// Producers are not allowed to dequeue more than// mMaxDequeuedBufferCount buffers.// This check is only done if a buffer has already been queuedif (mCore -> mBufferHasBeenQueued &&dequeuedCount >= mCore -> mMaxDequeuedBufferCount) {BQ_LOGE("%s: attempting to exceed the max dequeued buffer count ""(%d)", callerString, mCore -> mMaxDequeuedBufferCount);return INVALID_OPERATION;}}.......}

    5. 碼流適配

    視頻的監(jiān)控體系發(fā)現(xiàn),Android 9.0的系統(tǒng)出現(xiàn)大量的編解碼失敗問(wèn)題,錯(cuò)誤信息都是相同的。在MediaCodec的Configure時(shí)候出異常了,主要原因是我們強(qiáng)制使用了CQ碼流,Android 9.0以前并無(wú)問(wèn)題,但9.0及以后對(duì)CQ碼流增加了新的校驗(yàn)機(jī)制而我們沒(méi)有適配。核心流程代碼如下:

    status_t ACodec::configureCodec(const char *mime, const sp<AMessage> &msg) {.......if (encoder) {if (mIsVideo || mIsImage) {if (!findVideoBitrateControlInfo(msg, &bitrateMode, &bitrate, &quality)) {return INVALID_OPERATION;}} else if (strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_FLAC)&& !msg->findInt32("bitrate", &bitrate)) {return INVALID_OPERATION;}}....... } static bool findVideoBitrateControlInfo(const sp<AMessage> &msg,OMX_VIDEO_CONTROLRATETYPE *mode, int32_t *bitrate, int32_t *quality) {*mode = getVideoBitrateMode(msg);bool isCQ = (*mode == OMX_Video_ControlRateConstantQuality);return (!isCQ && msg->findInt32("bitrate", bitrate))|| (isCQ && msg->findInt32("quality", quality)); } 9.0前并無(wú)對(duì)CQ碼流的強(qiáng)校驗(yàn),如果不支持該碼流也會(huì)使用默認(rèn)支持的碼流, static OMX_VIDEO_CONTROLRATETYPE getBitrateMode(const sp<AMessage> &msg) {int32_t tmp;if (!msg->findInt32("bitrate-mode", &tmp)) {return OMX_Video_ControlRateVariable;}return static_cast<OMX_VIDEO_CONTROLRATETYPE>(tmp); }

    關(guān)于碼流還有個(gè)問(wèn)題就是如果通過(guò)系統(tǒng)的接口isBitrateModeSupported(int mode),判斷是否支持該碼流可能會(huì)出現(xiàn)誤判,究其原因是framework層寫(xiě)死了該返回值,而并沒(méi)有從硬件層或從media_codecs.xml去獲取該值。關(guān)于碼流各硬件廠商支持的差異性,可能谷歌也認(rèn)為碼流的兼容性太碎片化,不建議用非默認(rèn)的碼流。

    6. 音頻處理

    音頻處理還括對(duì)音頻的混音,消聲等操作。在混音操作的時(shí)候,還要注意音頻文件的單聲道轉(zhuǎn)換等問(wèn)題。

    其實(shí)視頻問(wèn)題總結(jié)起來(lái),大部分是都會(huì)牽扯到編解碼(尤其是使用硬編碼),需要大量的適配工作(以上也只是部分問(wèn)題,碎片化還是很嚴(yán)峻的),所以就需要兜底容錯(cuò)方案,比如加入軟編。

    線上監(jiān)控

    視頻功能引入了埋點(diǎn),日志,鏈路監(jiān)控等技術(shù)手段進(jìn)行線上的監(jiān)控,我們可以針對(duì)監(jiān)控結(jié)果進(jìn)行降級(jí)或維護(hù)更新。埋點(diǎn)更多的是產(chǎn)品維度的數(shù)據(jù)收集,日志是輔助定位問(wèn)題的,而鏈路監(jiān)控則可以做到監(jiān)控預(yù)警。我們加了拍攝流程,音視頻處理,視頻上傳流程的全鏈路監(jiān)控,整個(gè)鏈路如果任何一個(gè)節(jié)點(diǎn)出問(wèn)題都認(rèn)為是整個(gè)鏈路的失敗,若失敗次數(shù)超過(guò)閾值就會(huì)通過(guò)大象或郵件進(jìn)行報(bào)警,我們?cè)谶m配Andorid 9.0碼流問(wèn)題時(shí),最早發(fā)現(xiàn)也是由于鏈路監(jiān)控的預(yù)警。所有全鏈路的成功率目標(biāo)值均為98%,若成功率低于92%的目標(biāo)閾值就會(huì)觸發(fā)報(bào)警,我們會(huì)根據(jù)報(bào)警的信息和日志定位分析,該異常的影響范圍,再根據(jù)影響范圍確定是否熱修復(fù)或者降級(jí)。如下以拍攝流程為例,其鏈路各核心節(jié)點(diǎn)的監(jiān)控:

    拍攝流程全鏈路,如下圖(各關(guān)鍵節(jié)點(diǎn)監(jiān)控):

    容災(zāi)降級(jí)

    視頻功能目前只支持粗粒度的降級(jí)策略。我們?cè)谝曨l入口處做了開(kāi)關(guān)控制,關(guān)掉后所有的視頻功能都無(wú)法使用。我們通過(guò)線上監(jiān)控到視頻的穩(wěn)定性和成功率在特定機(jī)型無(wú)法保證,導(dǎo)致影響用戶正常的使用商家端App,我們支持針對(duì)特定設(shè)備做降級(jí)。后續(xù)我們可以做更細(xì)粒度的降級(jí)策略,比如根據(jù)P0級(jí)功能做降級(jí),或者編解碼策略的降級(jí)等

    維護(hù)更新

    視頻功能上線后,經(jīng)歷了幾個(gè)穩(wěn)定的版本,保持著較高的成功率,但近期收到了sniffer的郵件報(bào)警,發(fā)現(xiàn)視頻處理鏈路的失敗次數(shù)明顯增多,通過(guò)sniffer收集的信息發(fā)現(xiàn)大部分都是Android 9.0的問(wèn)題(也就是上面講的Android 9.0碼流適配的問(wèn)題),我們?cè)谏碳叶?.2版本進(jìn)行了修復(fù),該問(wèn)題解決后我們的視頻處理鏈路成功率也恢復(fù)到了98%以上。

    總結(jié)和規(guī)劃

    視頻功能上線后,穩(wěn)定性、內(nèi)存、CPU等一些相關(guān)指標(biāo)數(shù)據(jù)比較理想,我們建設(shè)的視頻監(jiān)控體系,也支撐著視頻核心業(yè)務(wù)的監(jiān)控,一些異常報(bào)警也讓我們及時(shí)發(fā)現(xiàn)問(wèn)題并迅速對(duì)異常進(jìn)行維護(hù)更新,但視頻技術(shù)棧也是遠(yuǎn)比本文介紹的要龐大,怎么提高秒播率,怎么提高編解碼效率,還有硬編解碼過(guò)程中可能造成的花屏,綠邊等問(wèn)題都是挑戰(zhàn),需要更深入的研究解決。

    未來(lái)我們會(huì)繼續(xù)致力于提高視頻處理的兼容性和效率,優(yōu)化現(xiàn)有流程,我們會(huì)對(duì)音頻和視頻處理合并處理,也會(huì)引入軟編和自定義編解碼算法。

    美團(tuán)外賣大前端團(tuán)隊(duì)將來(lái)也會(huì)繼續(xù)致力于提高用戶的體驗(yàn),并且會(huì)將在實(shí)踐過(guò)程中遇到的問(wèn)題進(jìn)行總結(jié),沉底技術(shù),積極的和大家分享,如果你也對(duì)視頻感興趣,歡迎加入我們。

    參考資料

  • Android開(kāi)發(fā)者官網(wǎng)
  • Google CTS
  • Grafika
  • BufferQueue原理介紹
  • MediaCodec原理
  • 微信Android 視頻編碼爬過(guò)的坑
  • mp4文件結(jié)構(gòu)
  • AndroidVideoCache 代理策略
  • ijkplayer
  • mp4parser
  • GPUImage
  • 作者簡(jiǎn)介

    金輝李瓊,美團(tuán)外賣商家終端研發(fā)工程師。

    招聘信息

    美團(tuán)外賣商家終端研發(fā)團(tuán)隊(duì)的主要職責(zé)是為商家提供穩(wěn)定可靠的生產(chǎn)經(jīng)營(yíng)工具,在保障穩(wěn)定的需求迭代的基礎(chǔ)之上,持續(xù)優(yōu)化APP、PC和H5的性能和用戶體驗(yàn),并不斷優(yōu)化提升團(tuán)隊(duì)的研發(fā)效率。團(tuán)隊(duì)主要負(fù)責(zé)的業(yè)務(wù)主要包括外賣訂單、商品管理、門(mén)店裝修、服務(wù)市場(chǎng)、門(mén)店運(yùn)營(yíng)、三方會(huì)話、藍(lán)牙打印、自動(dòng)接單、視頻、語(yǔ)音和實(shí)時(shí)消息觸達(dá)等基礎(chǔ)業(yè)務(wù),支撐整個(gè)外賣鏈路的高可用性及穩(wěn)定發(fā)展。

    團(tuán)隊(duì)通過(guò)架構(gòu)演進(jìn)及平臺(tái)化體系化建設(shè),有效支撐業(yè)務(wù)發(fā)展,提升了業(yè)務(wù)的可靠性和安全性;通過(guò)大規(guī)模落地跨平臺(tái)和動(dòng)態(tài)化技術(shù),加快了業(yè)務(wù)迭代效率,幫助產(chǎn)品(PM)加快產(chǎn)品方案的落地及上線;通過(guò)監(jiān)控容災(zāi)體系建設(shè),有效保障業(yè)務(wù)的高可用性和穩(wěn)定性;通過(guò)性能優(yōu)化建設(shè),保證APP的流暢性和良好用戶體驗(yàn)。團(tuán)隊(duì)開(kāi)發(fā)的技術(shù)棧包括Android、iOS、React、Flutter和React Native。

    總結(jié)

    以上是生活随笔為你收集整理的美团外卖商家端视频探索之旅的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。