Qt/C++音視頻開發67-保存裸流加入sps/pps信息/支持264/265裸流/轉碼保存/拉流推流

一、前言

音視頻組件除了支持保存MP4文件外,同時還支持保存裸流即264/265文件,以及解碼後最原始的yuv文件。在實際使用過程中,會發現部分視頻文件保存的裸流文件,並不能直接用播放器播放,查閱資料得知原來是缺少sps/pps信息,監控行業的rtsp/rtmp/錄像mp4文件都是會帶的,所以很少遇到這個現象。其實sps/pps信息在解碼的時候就已經在對應AVCodecContext或者AVStream的extradata中,只是在av_read_frame後的AVPacket,有些流帶了sps/pps而有些流就沒帶,沒帶的就需要主動加上。那如果帶了也又加上,會不會出問題呢?答案是不會的,在執行av_bsf_send_packet/av_bsf_receive_packet的時候裏面會自動過濾判斷。測試下來發現,只有裸流才需要加入,如果是原數據或者編碼數據保存到MP4文件則不需要,因爲在創建MP4流的時候就已經複製了原始流的參數,那個參數中就包含了extradata,所以在寫入數據的時候就會自動加上。保存裸流到文件這個需求也是有的,尤其是現在人工智能分析,基本上需要傳入264/265裸流進行運算分析,音頻傳輸pcm數據。

在h264流中,有兩種NALU極其的重要,序列參數集(Sequence Paramater Set,SPS)和圖像參數集(Picture ParamaterSet,PPS),SPS中的信息至關重要,記錄了編碼的prfile、level、圖像寬高等,如果其中的數據丟失或出現錯誤,那麼解碼過程很可能會失敗。每一幀編碼後數據所依賴的參數保存於PPS中。一般情況SPS和PPS的NAL Unit通常位於整個碼流的起始位置。封裝文件一般進保存一次,位於文件頭部,sps/sps再整個解碼過程中複用,不發生變化。然而對於實時流,通常是從流中間開始解碼,因此需要在每個I幀前添加SPS和PPS;如果編碼器在編碼過程中改變了碼流參數(如分辨率),需要重新調整SPS和PPS數據。

ffmpeg中SPS、PPS數據從輸入流中解析時,位於AVFormatContext->streams[video_index]->codecpar->extradata中。編碼時,sps/pps數據存放於編碼器上下文AVCodecContext->extradata中,但是該對象通常是空指針,還需要進行額外設置。通常我們編碼保存裸流時,僅第一個I幀前有SPS/PPS數據。但是,如果需要做實時流傳輸,必須要在每一個I幀前添加SPS和PPS。其中SPS和PPS分別是14/5個字節。

有兩種方法可以加上sps/pps,一種是自己判斷並主動加上,一種是用ffmpeg自帶的bsf相關函數,推薦使用bsf相關函數處理,非常準確而且性能高。一開始我也以爲很難,其實非常簡單,總結就是三步走,初始化,不斷寫入,關閉釋放。

添加sps/pps大致步驟:

  1. 初始化AVBSFContext,av_bsf_alloc/avcodec_parameters_copy/av_bsf_init。
  2. 寫入數據 av_bsf_send_packet/av_bsf_receive_packet。
  3. 關閉釋放 av_bsf_free。

二、效果圖


三、體驗地址

  1. 國內站點:https://gitee.com/feiyangqingyun
  2. 國際站點:https://github.com/feiyangqingyun
  3. 個人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
  4. 體驗地址:https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g 提取碼:01jf 文件名:bin_video_demo。
  5. 視頻主頁:https://space.bilibili.com/687803542

四、功能特點

4.1. 基礎功能

  1. 支持各種音頻視頻文件格式,比如mp3、wav、mp4、asf、rm、rmvb、mkv等。
  2. 支持本地攝像頭設備和本地桌面採集,支持多設備和多屏幕。
  3. 支持各種視頻流格式,比如rtp、rtsp、rtmp、http、udp等。
  4. 本地音視頻文件和網絡音視頻文件,自動識別文件長度、播放進度、音量大小、靜音狀態等。
  5. 文件可以指定播放位置、調節音量大小、設置靜音狀態等。
  6. 支持倍速播放文件,可選0.5倍、1.0倍、2.5倍、5.0倍等速度,相當於慢放和快放。
  7. 支持開始播放、停止播放、暫停播放、繼續播放。
  8. 支持抓拍截圖,可指定文件路徑,可選抓拍完成是否自動顯示預覽。
  9. 支持錄像存儲,手動開始錄像、停止錄像,部分內核支持暫停錄像後繼續錄像,跳過不需要錄像的部分。
  10. 支持無感知切換循環播放、自動重連等機制。
  11. 提供播放成功、播放完成、收到解碼圖片、收到抓拍圖片、視頻尺寸變化、錄像狀態變化等信號。
  12. 多線程處理,一個解碼一個線程,不卡主界面。

4.2. 特色功能

  1. 同時支持多種解碼內核,包括qmedia內核(Qt4/Qt5/Qt6)、ffmpeg內核(ffmpeg2/ffmpeg3/ffmpeg4/ffmpeg5/ffmpeg6)、vlc內核(vlc2/vlc3)、mpv內核(mpv1/mp2)、mdk內核、海康sdk、easyplayer內核等。
  2. 非常完善的多重基類設計,新增一種解碼內核只需要實現極少的代碼量,就可以應用整套機制,極易拓展。
  3. 同時支持多種畫面顯示策略,自動調整(原始分辨率小於顯示控件尺寸則按照原始分辨率大小顯示,否則等比縮放)、等比縮放(永遠等比縮放)、拉伸填充(永遠拉伸填充)。所有內核和所有視頻顯示模式下都支持三種畫面顯示策略。
  4. 同時支持多種視頻顯示模式,句柄模式(傳入控件句柄交給對方繪製控制)、繪製模式(回調拿到數據後轉成QImage用QPainter繪製)、GPU模式(回調拿到數據後轉成yuv用QOpenglWidget繪製)。
  5. 支持多種硬件加速類型,ffmpeg可選dxva2、d3d11va等,vlc可選any、dxva2、d3d11va,mpv可選auto、dxva2、d3d11va,mdk可選dxva2、d3d11va、cuda、mft等。不同的系統環境有不同的類型選擇,比如linux系統有vaapi、vdpau,macos系統有videotoolbox。
  6. 解碼線程和顯示窗體分離,可指定任意解碼內核掛載到任意顯示窗體,動態切換。
  7. 支持共享解碼線程,默認開啓並且自動處理,當識別到相同的視頻地址,共享一個解碼線程,在網絡視頻環境中可以大大節約網絡流量以及對方設備的推流壓力。國內頂尖視頻廠商均採用此策略。這樣只要拉一路視頻流就可以共享到幾十個幾百個通道展示。
  8. 自動識別視頻旋轉角度並繪製,比如手機上拍攝的視頻一般是旋轉了90度的,播放的時候要自動旋轉處理,不然默認是倒着的。
  9. 自動識別視頻流播放過程中分辨率的變化,在視頻控件上自動調整尺寸。比如攝像機可以在使用過程中動態配置分辨率,當分辨率改動後對應視頻控件也要做出同步反應。
  10. 音視頻文件無感知自動切換循環播放,不會出現切換期間黑屏等肉眼可見的切換痕跡。
  11. 視頻控件同時支持任意解碼內核、任意畫面顯示策略、任意視頻顯示模式。
  12. 視頻控件懸浮條同時支持句柄、繪製、GPU三種模式,非絕對座標移來移去。
  13. 本地攝像頭設備支持指定設備名稱、分辨率、幀率進行播放。
  14. 本地桌面採集支持設定採集區域、偏移值、指定桌面索引、幀率、多個桌面同時採集等。還支持指定窗口標題採集固定窗口。
  15. 錄像文件同時支持打開的視頻文件、本地攝像頭、本地桌面、網絡視頻流等。
  16. 瞬間響應打開和關閉,無論是打開不存在的視頻或者網絡流,探測設備是否存在,讀取中的超時等待,收到關閉指令立即中斷之前的操作並響應。
  17. 支持打開各種圖片文件,支持本地音視頻文件拖曳播放。
  18. 視頻流通信方式可選tcp/udp,有些設備可能只提供了某一種協議通信比如tcp,需要指定該種協議方式打開。
  19. 可設置連接超時時間(視頻流探測用的超時時間)、讀取超時時間(採集過程中的超時時間)。
  20. 支持逐幀播放,提供上一幀/下一幀函數接口,可以逐幀查閱採集到的圖像。
  21. 音頻文件自動提取專輯信息比如標題、藝術家、專輯、專輯封面,自動顯示專輯封面。
  22. 視頻響應極低延遲0.2s左右,極速響應打開視頻流0.5s左右,專門做了優化處理。
  23. 支持H264/H265編碼(現在越來越多的監控攝像頭是H265視頻流格式)生成視頻文件,內部自動識別切換編碼格式。
  24. 支持用戶信息中包含特殊字符(比如用戶信息中包含+#@等字符)的視頻流播放,內置解析轉義處理。
  25. 支持濾鏡,各種水印及圖形效果,支持多個水印和圖像,可以將OSD標籤信息和各種圖形信息寫入到MP4文件。
  26. 支持視頻流中的各種音頻格式,AAC、PCM、G.726、G.711A、G.711Mu、G.711ulaw、G.711alaw、MP2L2等都支持,推薦選擇AAC兼容性跨平臺性最好。
  27. 內核ffmpeg採用純qt+ffmpeg解碼,非sdl等第三方繪製播放依賴,gpu繪製採用qopenglwidget,音頻播放採用qaudiooutput。
  28. 內核ffmpeg和內核mdk支持安卓,其中mdk支持安卓硬解碼,性能非常兇殘。
  29. 可以切換音視頻軌道,也就是節目通道,可能ts文件帶了多個音視頻節目流,可以分別設置要播放哪一個,可以播放前設置好和播放過程中動態設置。
  30. 可以設置視頻旋轉角度,可以播放前設置好和播放過程中動態改變。
  31. 視頻控件懸浮條自帶開始和停止錄像切換、聲音靜音切換、抓拍截圖、關閉視頻等功能。
  32. 音頻組件支持聲音波形值數據解析,可以根據該值繪製波形曲線和柱狀聲音條,默認提供了聲音振幅信號。
  33. 標籤和圖形信息支持三種繪製方式,繪製到遮罩層、繪製到圖片、源頭繪製(對應信息可以存儲到文件)。
  34. 通過傳入一個url地址,該地址可以帶上通信協議、分辨率、幀率等信息,無需其他設置。
  35. 保存視頻到文件支持三種策略,自動處理、僅限文件、全部轉碼,轉碼策略支持自動識別、轉264、轉265,編碼保存支持指定分辨率縮放或者等比例縮放。比如對保存文件體積有要求可以指定縮放後再存儲。
  36. 支持加密保存文件和解密播放文件,可以指定祕鑰文本。
  37. 提供的監控佈局類支持64通道同時顯示,還支持各種異型佈局,比如13通道,手機上6行2列布局。各種佈局可以自由定義。
  38. 支持電子放大,在懸浮條切換到電子放大模式,在畫面上選擇需要放大的區域,選取完畢後自動放大,再次切換放大模式可以復位。
  39. 各組件中極其詳細的打印信息提示,尤其是報錯信息提示,封裝的統一打印格式。針對現場複雜的設備環境測試極其方便有用,相當於精確定位到具體哪個通道哪個步驟出錯。
  40. 同時提供了簡單示例、視頻播放器、多畫面視頻監控、監控回放、逐幀播放、多屏渲染等單獨窗體示例,專門演示對應功能如何使用。
  41. 監控回放可選不同廠家類型、回放時間段、用戶信息、指定通道。支持切換回放進度。
  42. 可以從聲卡設備下拉框選擇聲卡播放聲音,提供對應的切換聲卡函數接口。
  43. 支持編譯到手機app使用,提供了專門的手機app佈局界面,可以作爲手機上的視頻監控使用。
  44. 代碼框架和結構優化到最優,性能強悍,註釋詳細,持續迭代更新升級。
  45. 源碼支持windows、linux、mac、android等,支持各種國產linux系統,包括但不限於統信UOS/中標麒麟/銀河麒麟等。還支持嵌入式linux。
  46. 源碼支持Qt4、Qt5、Qt6,兼容所有版本。

4.3. 視頻控件

  1. 可動態添加任意多個osd標籤信息,標籤信息包括名字、是否可見、字號大小、文本文字、文本顏色、背景顏色、標籤圖片、標籤座標、標籤格式(文本、日期、時間、日期時間、圖片)、標籤位置(左上角、左下角、右上角、右下角、居中、自定義座標)。
  2. 可動態添加任意多個圖形信息,這個非常有用,比如人工智能算法解析後的圖形區域信息直接發給視頻控件即可。圖形信息支持任意形狀,直接繪製在原始圖片上,採用絕對座標。
  3. 圖形信息包括名字、邊框大小、邊框顏色、背景顏色、矩形區域、路徑集合、點座標集合等。
  4. 每個圖形信息都可指定三種區域中的一種或者多種,指定了的都會繪製。
  5. 內置懸浮條控件,懸浮條位置支持頂部、底部、左側、右側。
  6. 懸浮條控件參數包括邊距、間距、背景透明度、背景顏色、文本顏色、按下顏色、位置、按鈕圖標代碼集合、按鈕名稱標識集合、按鈕提示信息集合。
  7. 懸浮條控件一排工具按鈕可自定義,通過結構體參數設置,圖標可選圖形字體還是自定義圖片。
  8. 懸浮條按鈕內部實現了錄像切換、抓拍截圖、靜音切換、關閉視頻等功能,也可以自行在源碼中增加自己對應的功能。
  9. 懸浮條按鈕對應實現了功能的按鈕,有對應圖標切換處理,比如錄像按鈕按下後會切換到正在錄像中的圖標,聲音按鈕切換後變成靜音圖標,再次切換還原。
  10. 懸浮條按鈕單擊後都用名稱唯一標識作爲信號發出,可以自行關聯響應處理。
  11. 懸浮條空白區域可以顯示提示信息,默認顯示當前視頻分辨率大小,可以增加幀率、碼流大小等信息。
  12. 視頻控件參數包括邊框大小、邊框顏色、焦點顏色、背景顏色(默認透明)、文字顏色(默認全局文字顏色)、填充顏色(視頻外的空白處填充黑色)、背景文字、背景圖片(如果設置了圖片優先取圖片)、是否拷貝圖片、縮放顯示模式(自動調整、等比縮放、拉伸填充)、視頻顯示模式(句柄、繪製、GPU)、啓用懸浮條、懸浮條尺寸(橫向爲高度、縱向爲寬度)、懸浮條位置(頂部、底部、左側、右側)。

五、相關代碼

void FFmpegSaveHelper::initBsfCtx(AVStream *stream, AVBSFContextx **bsfCtx, bool h264)
{
    if (!(*bsfCtx)) {
        const char *name = (h264 ? "h264_mp4toannexb" : "hevc_mp4toannexb");
#if (FFMPEG_VERSION_MAJOR > BSFVersion)
        av_bsf_alloc(av_bsf_get_by_name(name), bsfCtx);
        avcodec_parameters_copy((*bsfCtx)->par_in, stream->codecpar);
        av_bsf_init(*bsfCtx);
#else
        *bsfCtx = av_bitstream_filter_init(name);
#endif
    }
}

void FFmpegSaveHelper::freeBsfCtx(AVBSFContextx **bsfCtx)
{
    if (*bsfCtx) {
#if (FFMPEG_VERSION_MAJOR > BSFVersion)
        av_bsf_free(bsfCtx);
#else
        av_bitstream_filter_close(*bsfCtx);
#endif
        bsfCtx = NULL;
    }
}

void FFmpegSaveHelper::writeBsf(AVPacket *packet, AVStream *stream, AVBSFContextx *bsfCtx)
{
    if (bsfCtx) {
#if (FFMPEG_VERSION_MAJOR > BSFVersion)
        av_bsf_send_packet(bsfCtx, packet);
        av_bsf_receive_packet(bsfCtx, packet);
#else
        av_bitstream_filter_filter(bsfCtx, stream->codec, NULL, &packet->data, &packet->size, packet->data, packet->size, 0);
#endif
    }
}

//設置了需要封裝格式並且沒有重新編碼則需要加上/保存文件才需要/推流不需要
if (mp4ToAnnexB && !videoEncode && needVideo && saveMode == SaveMode_File) {
    FFmpegSaveHelper::initBsfCtx(videoStreamIn, &bsfCtx, videoCodecName == "h264");
    debug(0, "封裝格式", QString("格式: %1").arg(videoCodecName));
}

void FFmpegSave::writePacket2(AVPacket *packet)
{
    //非音視頻流不用處理
    int index = packet->stream_index;
    if (index != videoIndexOut && index != audioIndexOut) {
        return;
    }

    //轉發數據包(可以設置僅僅轉發數據包不用繼續)
    if (sendPacket) {
        emit receivePacket(FFmpegHelper::creatPacket(packet));
        if (onlySendPacket) {
            return;
        }
    }

    //封裝格式 https://blog.csdn.net/weixin_44520287/article/details/113435440 https://xilixili.net/2018/08/20/ffmpeg-got-raw-h264/
    if (index == videoIndexOut) {
        FFmpegSaveHelper::writeBsf(packet, videoStreamIn, bsfCtx);
    }

    if (saveVideoType == SaveVideoType_Stream) {
        //只需要寫入視頻數據
        if (index == videoIndexOut) {
            file.write((char *)packet->data, packet->size);
        }
    } else if (saveVideoType == SaveVideoType_Mp4) {
        //取出輸入輸出流的時間基
        AVStream *streamIn = (index == videoIndexOut ? videoStreamIn : audioStreamIn);
        AVStream *streamOut = formatCtx->streams[index];
        AVRational timeBaseIn = streamIn->time_base;
        AVRational timeBaseOut = streamOut->time_base;

        //轉換時間基準
        if (index == videoIndexOut) {
            FFmpegSaveHelper::rescalePacket(packet, timeBaseIn, videoCount, frameRate);
            FFmpegSaveHelper::rescalePacket(packet, timeBaseIn, timeBaseOut);
        } else if (index == audioIndexOut) {
            if (audioEncode) {
                FFmpegSaveHelper::rescalePacket(packet, audioDuration);
            } else {
                FFmpegSaveHelper::rescalePacket(packet, timeBaseIn, timeBaseOut, audioDuration);
            }
        }

        //打印對應的信息方便查看/videoIndexOut/audioIndexOut
        if (index == -1) {
            qDebug() << TIMEMS << flag << index << packet->pts << packet->dts << packet->duration;
        }

        //倍速調整時間戳
        if (encodeSpeed != 1) {
            packet->pts = packet->pts / encodeSpeed;
            packet->dts = packet->dts / encodeSpeed;
        }

        //寫入一幀數據
        int result = av_interleaved_write_frame(formatCtx, packet);
        if (result < 0) {
            errorCount++;
            debug(result, QString("寫%1包").arg(index == audioIndexOut ? "音頻" : "視頻"), "");
        } else {
            errorCount = 0;
        }

        //推流超過錯誤次數需要重連
        if (errorCount >= 5 && saveMode != SaveMode_File) {
            isOk = false;
            errorCount = 0;
            emit receiveSaveError(VideoError_Save);
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章