雙錄系統(tǒng)趟坑總結

1.背景

行業(yè)監(jiān)管要求,需要對保險銷售過程關鍵環(huán)節(jié)進行錄音錄像,所以我們要做一個app可以進行音視頻錄制,并上傳錄制的視頻文件,同時錄制過程中自動播報語音以及語音識別,功能看起來并不復雜,但是涉及到音視頻這種功能并不好做,尤其android兼容性問題,加上之前沒有相關的經驗,可能很多問題考慮并不全面,整個1個多月的研發(fā)過程下來還是發(fā)現不少坑,簡單復盤留作紀念。

2.功能規(guī)劃

  1. 核心業(yè)務功能
  • 音視頻錄制功能
  • 音視頻文件上傳功能
  • 語音播報功能:錄制過程中,有大量的文字原來是需要業(yè)務員讀給客戶聽,為了減輕業(yè)務員的負擔,需要實現系統(tǒng)自動播報,系統(tǒng)會根據每一個保單實際數據生成對應的話術,在錄制過程中進行語音播報。
  • 語音識別:錄制過程中,部分話術播報后需要客戶回答“同意”,“確認”等詞,系統(tǒng)對客戶回答的文字進行語音識別。
  1. 技術點
  • Android 音視頻錄制:一期采用的MediaRecorder,二期采用的MediaCodec+MediaMuxer。
  • IOS音視頻錄制:AVCapture+AVAssetWriter
  • 語音播報技術:基于騰訊云的語音合成功能,每一段話術在播報的時候會調用騰訊云的語音合成,將文字轉換成語音音頻文件,本地在進行播放。
  • 語音識別技術:基于騰訊云的語音識別,客戶回答時,會實時的采集音頻數據,將音頻數據發(fā)給騰訊云語音識別服務,返回識別后的文本。
    像語音合成和語音識別這種關鍵的技術都是基于外部平臺,音視頻錄制功能是用的系統(tǒng)的API,看起來很容易是不是?這里面的坑誰做誰知道。

3.基礎概念

  • 幀率:幀率(Frame rate)是稱為幀的位圖圖像連續(xù)出現在顯示器上的頻率(速率),在我們這簡單的理解,就是視頻錄制每一秒錄制了多少副畫面。
  • 碼率:碼率就是數據傳輸時單位時間傳送的數據位數,一般我們用的單位是kbps即千位每秒,比如我們一期上線設置的碼率是2kbps,那么文件大小怎么計算呢?
    2000/8=250 KB 每秒,半個小時視頻長度為:2506030=450M
    所以評估錄制一個視頻文件的大小,關鍵就是看這個參數。
  • 分辨率:指視頻成像產品所形成的圖像大小或尺寸,比如我們系統(tǒng)設置的720p,1280*720,分辨率本質上跟清晰度無關,我們可以想一下,如果同樣的碼率,也就是說假設每一秒的數據量是一樣的,那生成一副大圖清晰還是小圖清晰?
  • 清晰度:
    在碼率一定的情況下,分辨率與清晰度成反比關系:分辨率越高,圖像越不清晰,分辨率越低,圖像越清晰。
    在分辨率一定的情況下,碼率與清晰度成正比關系,碼率越高,圖像越清晰;碼率越低,圖像越不清晰。
    綜上幾個問題:
  • 幀率越高越好么?
    幀率越高,意味著每一秒的畫面越多,同樣的碼率,則分給每一副畫面的數據量就越小,清晰度則會降低,比如上面提到的2000kbps,每一秒才
    250k,假設幀率30,一秒有30副畫面,想想平均一副畫面才多少k? 8k多?是不是不太可能?所以這就是編碼算法的厲害之處了,我簡單的理解就是一副畫面可以做壓縮,因為有很多像素點是一樣的,另外相鄰的畫面重復的也可以進行壓縮,因為一秒整個畫面變動的非常少,所以這30 副畫面差異內容也很少,壓縮空間還是很大的,這里在深入點可以去了解I幀,P幀,B幀
  • 碼率越高越好么?
    從上面可以看出文件大小跟碼率成正比,文件越大用戶上傳以及我們自己存儲、后面的質檢等都需要增加代價,所以我們需要在碼率和視頻清晰度中間找一個平衡,但是也不存在說又要絕對的清晰,又不想文件變大,哪有這樣的好事,別想了。

4.具體問題

  1. 谷歌瀏覽器播放視頻加載失敗的問題
    現象:一期快要開發(fā)完了的時候,給領導演示,用的蘋果手機,錄制視頻很順利,本地播放正常,上傳到云端后,用谷歌瀏覽器加載失敗,但是用手機瀏覽器打開鏈接播放正常。
    原因:查了一些資料,安裝了ffmpeg用來分析視頻,如下圖一是在chrome可以正常播放的視頻,圖二是不能播放的,主要差別就是視頻文件編碼格式H.264和H.265,H265格式是H264的升級版,壓縮算法更好,據說可以節(jié)省50%帶寬,但是兼容性還不夠好,不同瀏覽器支持的視頻格式,編碼格式也不一樣,pc上不能播放但是手機上可以播放,我猜測是解碼器差異的原因,我演示的那個版本當時一行設置編碼H264代碼注釋掉了,iOS 系統(tǒng)默認采用了H265編碼,導致上傳后的視頻無法播放。
    h264.png

    h265.png

    這篇文章有提到一些兩種編碼的差別以及兼容性的情況
    https://juejin.im/post/5bf7697251882521c8114030#heading-10
  2. oppo R9開啟錄制失敗
  • 現象:
    手上有華為、小米、oppo等幾款手機,當時只有oppo這一款手機開啟錄制就失敗。
一些錯誤日志:
E/ACodec: signalError(omxError 0x80001001, internalError -2147483648)
E/MediaCodec: Codec reported err 0x80001001, actionCode 0, while in state 3
E/MediaCodec: configure failed with err 0x80001001, resetting...

android.media.MediaCodec$CodecException: Error 0x80001001
at android.media.MediaCodec.native_configure(Native Method)
at android.media.MediaCodec.configure(MediaCodec.java:590)
  • 原因:
 查看異常代碼如下
//創(chuàng)建編碼器
mVideoEncoder = MediaCodec.createEncoderByType(MIME_TYPE_AVC);
//報錯代碼就是這行,是編碼器configure報錯
mVideoEncoder.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

經過網站一些資料的查找,原來是不同的手機支持的顏色格式不一樣,這款oppo手機不支持COLOR_FormatYUV420SemiPlanar,需要設置成COLOR_FormatYUV420Planar
// 指定編碼器顏色格式
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar

既然不同的手機支持的KEY_COLOR_FORMAT 不一樣,這里就需要動態(tài)的考慮先獲取到手機可支持的顏色格式值,在進行設置,如下代碼也是參考網上的資料。
 private int getSupportColorFormat() {
        int numCodecs = MediaCodecList.getCodecCount();
        MediaCodecInfo codecInfo = null;
        for (int i = 0; i < numCodecs && codecInfo == null; i++) {
            MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
            if (!info.isEncoder()) {
                continue;
            }
            String[] types = info.getSupportedTypes();
            boolean found = false;
            for (int j = 0; j < types.length && !found; j++) {
                if (types[j].equals("video/avc")) {
                    Log.d(TAG, "found");
                    found = true;
                }
            }
            if (!found)
                continue;
            codecInfo = info;
        }
        Log.e("AvcEncoder", "Found " + codecInfo.getName() + " supporting " + "video/avc");
        // Find a color profile that the codec supports
        MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType("video/avc");
        Log.e("AvcEncoder",
                "length-" + capabilities.colorFormats.length + "==" + Arrays.toString(capabilities.colorFormats));
        for (int i = 0; i < capabilities.colorFormats.length; i++) {
            Log.d(TAG, "MediaCodecInfo COLOR FORMAT :" + capabilities.colorFormats[i]);
            if ((capabilities.colorFormats[i] == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar) || (capabilities.colorFormats[i] == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar)) {
                return capabilities.colorFormats[i];
            }
        }
        return MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible;
    }
  1. oppo R9 錄制的視頻是黑白的
    原因:
    這個問題跟上一個問題是有關聯(lián)的,出現黑白視頻或者錄出來視頻畫面的顏色與真實情況不一致,一般來說跟yuv的轉換有關系,上面提到我們設置編碼器顏色格式color format有兩種,COLOR_FormatYUV420SemiPlanar 和COLOR_FormatYUV420Planar,我們在攝像頭預覽格式設置的nv21,所以在進行編碼的時候,先要對yuv進行轉換,一旦轉換不對,最終生成的視頻可能就是黑白或完全無法播放。
parameters.setPreviewFormat(ImageFormat.NV21);

常見YUV格式和Android中的COLOR_FormatYUV420對應關系
https://blog.csdn.net/qq_34557284/article/details/90902363

不同的color  format對應不同的轉換方式,這里使用了谷歌的Libyuv工具包進行轉換,該方法是native方法,據別人測算性能相比自己java寫的轉換會好很多。 if(colorFormat==MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar){
            // MediaCodecInfo color format 為 COLOR_FormatYUV420SemiPlanar 時,需要把nv21數據 轉為 nv12
            LibyuvUtil.convertNV21ToI420(nv21, yuvI420, mCameraHelp.getWidth(), mCameraHelp.getHeight());
            //不壓縮,前置也不進行鏡像,因為鏡像后證件顯示會是反的
            //LibyuvUtil.compressI420(yuvI420, videoWidth, videoHeight, tempYuvI420, videoWidth, videoHeight, rotation, isFrontCamera);
            LibyuvUtil.convertI420ToNV12(yuvI420, toEncodeDate, mCameraHelp.getWidth(), mCameraHelp.getHeight());
        }else {
            // MediaCodecInfo color format 為 COLOR_FormatYUV420Planar 時,需要把nv21數據 轉為 YUVI420
            LibyuvUtil.convertNV21ToI420(nv21,toEncodeDate,mCameraHelp.getWidth(), mCameraHelp.getHeight());
            //不壓縮,前置也不進行鏡像,因為鏡像后證件顯示會是反的
            // LibyuvUtil.compressI420(yuvI420, videoWidth, videoHeight, toEncodeDate, videoWidth, videoHeight, rotation, isFrontCamera);
        }

4.華為p9語音播報問題
現象:我用華為p9測試錄制的時候,發(fā)現每次錄制到第十一段左右的時候就沒聲音了,錯誤日志:mediaplay error(1,-19)
原因:這個問題一開始挺困擾的,因為同一份代碼,我手上幾個手機只有華為p9有這個問題,不過有同事也有其它手機有問題,有些手機是在第二次錄制的時候才有這個問題,很奇怪,如果是資源沒釋放,為什么有的手機正常?而且用工具分析錄制過程中內存的變化,沒有發(fā)現明顯的異常,對我們自己的代碼也review了幾遍,也調試了幾遍,直到后面搜索問題看到這篇文章https://blog.csdn.net/jakera/article/details/85081280,里面提到的dumpsys media.audio_flinger 這個命令很有用,通過這個命令查看數據,發(fā)現每次點下一步新增了一個服務,如下面截圖 client type就是我們應用的進程號,從截圖可以知道存在很多個不活躍的服務,我猜測這個服務是有限制的,達到一定數量后不可在創(chuàng)建,所以這也是為什么一般十來次后就出問題

播報服務.png

到這里結合博客提到的內容基本也就定位到了問題代碼,其實不是我們自己代碼問題,是用的騰訊sdk里面播報音頻服務停止的時候沒有正常的釋放資源,看下反編譯后的代碼也驗證了猜想,如下:

// 有問題的代碼
  if (this.mediaPlayer != null) {
                this.mediaPlayer.stop();
               }
  //改之后的代碼
  if (this.mediaPlayer != null) {
                this.mediaPlayer.stop();
                this.mediaPlayer.release();
            }
            this.mediaPlayer = null;         
       

基本確認是騰訊云sdk問題后,給他們提了一個工單,附上了我查到的幾篇文章,然后回家不久就收到了他們客服電話,希望我在他們demo上去還原問題,我估計他們是不太相信我的話,其實知道了原因要還原就很簡單了,第二天在他們demo上運行,直接連續(xù)語音播報十幾次一樣復現問題,提供相關的截圖和錯誤信息給他們,后面因為考慮到當天已經是我們發(fā)布日期,怕他們無法及時更新sdk,所以只好反編譯他們的代碼,覆蓋跟這個問題有關的三個類,重新運行,一切正常,不過為什么有些手機可以,有些不行,系統(tǒng)版本問題?還是廠商改的?沒有繼續(xù)去深究。
下面幾個鏈接是關于這個問題我找到的其它一些參考資料
MediaPlayer源碼存在的內存泄漏問題,釋放資源的正確方式
https://blog.csdn.net/sdfdzx/article/details/60144834
記一個華為手機上聲音突然消失不見的bug
http://bearcoder.codes/blog/2018/05/17/bugji-lu/

待寫。。。
5.ios視頻模糊的問題
6.為什么Android一期用MediaRecorder,二期改了整個錄制方式呢?

4.總結

整體來說,要把音視頻跑通并不是難事,但是要做穩(wěn)定,兼容性要做好不容易,包括用的第三方的sdk,即使騰訊云提供的sdk在這一個多月時間里,被我們發(fā)現的bug都有四五處,還一些非常隱蔽的問題,不過處理問題響應速度還是很不錯的,至于我們自己需要學習和深挖的點就太多了,幾個待研究的方向:

  • 人臉檢測
  • OCR
  • 聲音降噪,提高錄制的聲音質量
  • 清晰度分析
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容