天天看點

音視訊開發視訊和視訊幀:ffmpeg的RTMP推流

推薦視訊:RTSP/RTMP推流分析 推流架構分析/推流緩存隊列的設計 /FFmpeg函數阻塞問題分析​​https://www.bilibili.com/video/BV1ky4y177Jh​​

I. 推流簡介

筆者最初聽到“推流”時,内心想:“這是什麼高端玩意兒?”,迫于項目壓力,不得不頂着壓力調研和開發。經過一段時間的學習、開發和總結,筆者終于明白了推流,到底是個什麼高端玩意兒?

什麼是推流?

推流,指的是把采集階段封包好的内容傳輸到伺服器的過程。其實就是将現場的視訊信号傳到網絡的過程。

用大白話講,推流就是把本地音視訊資料通過網絡上傳到雲端/背景伺服器,所謂“采集階段封包好”,筆者認為是未解碼的H264的NALU。

音視訊開發視訊和視訊幀:ffmpeg的RTMP推流

上圖從“推流端”到“源站”(同上文所說到的“伺服器”),再到CDN分發節點,最後到“播放端”,整個過程的視音頻資料,都是壓縮的資料流。也就是說,對視訊資料來說,就是H264碼流。解碼工作是在播放端進行的。

推流的工作可想而知,最多的應用就是直播;而在大多數視訊門戶網站,筆者目前也很疑惑,是否存在步驟1,從效率上來說,視訊資料直接存放在“源站”,通過CDN根據用戶端請求下發,應該就可以了。至于具體做法,有待考證,筆者在這裡姑且記一筆吧。

了解了什麼是推流之後,下一個問題自然就出來了:應該怎麼推流呢?這其實是一個複雜的過程,而且還需要了解”源站“,也就是推流伺服器。本文僅涉及本地音視訊資料通過網絡上傳到雲端/背景伺服器的本地音視訊上傳階段,是以,先來了解本地的資料是怎麼上傳的,第一步就是了解推流的協定!

筆者了解到的幾個目前常用的推流協定有:RTMP,HLS,webRTC,HTTP-FLV。本文僅介紹RTMP,原因是:筆者目前隻接觸了RTMP協定的推流工作。

RTMP

RTMP是Real Time Messaging Protocol(實時消息傳輸協定)的首字母縮寫。是Adobe公司開發的一個基于TCP的應用層協定,也就是說,RTMP是和HTTP/HTTPS一樣,是應用層的一個協定族。RTMP在TCP通道上一般傳輸的是flv 格式流。請注意,RTMP是網絡傳輸協定,而flv則是視訊的封裝格式。flv封裝格式設計出來的目的是為了用于網絡傳輸使用的,是以RTMP+FLV可以說是”黃金搭檔“。

RTMP協定包括:基本協定及RTMPT/RTMPS/RTMPE等多種變種。RTMP協定家族有以下幾個點挺有趣,讀者們不妨看看:

  • RTMP工作在TCP之上,預設使用端口1935,這個是基本形态;
  • RTMPE在RTMP的基礎上增加了加密功能;
  • RTMPT封裝在HTTP請求之上,可穿透防火牆;
  • RTMPS類似RTMPT,增加了TLS/SSL的安全功能;
  • RTMFP使用UDP進行傳輸的RTMP;

RTMP就是專門針對多媒體資料流的實時通信設計出來的一種網絡資料傳輸協定,主要用來在Flash/AIR平台和支援RTMP協定的流媒體/互動伺服器之間進行音視訊和資料通信。現在Adobe公司已經不支援了,不過目前,該協定還在廣泛使用。

RTMP具體如何進行握手連接配接、傳輸資料,以及其封裝的資料包格式,讀者都可以通過各種資料了解到,本文不再贅述。(主要原因是:筆者還沒有深入了解這塊知識。捂臉)

II. ffmpeg的rtmp推流

下面将介紹2種ffmpeg推流的方式:指令行(cmd)和代碼(code)。

指令行(CMD)推流

ffmpeg cmd的參數實在太多,是以這裡隻介紹基礎的、以及筆者了解的跟讀者們做一個分享。

首先,來看一個将本地視訊檔案推流到伺服器的最基礎的指令:

ffmpeg -i ${input_video} -f flv rtmp://${server}/live/${streamName}      
  • -i:表示輸入視訊檔案,後跟視訊檔案路徑/URL。
  • -f:強制ffmpeg采用某種格式,後跟對應的格式。

上文有提到,RTMP一般用flv流資料,是以多設定-f flv。

接着,另一個基本需求,就是在推流的時候希望不要加上音頻,這個也好實作:

ffmpeg -i ${input_video} -vcodec copy -an -f flv rtmp://${server}/live/${streamName}      
  • -vcodec:指定視訊解碼器,v是視訊video,codec就是解碼器,後跟解碼器名稱,copy表示不作解碼;
  • -acodec:指定音頻解碼器,同理,a是audio,後跟解碼器名稱。an代表acodec none就是去掉音頻的意思。

關于a/v的寫法很多,除了上面介紹的,還有-c:v copy -c:a copy等。

再有其他的需求,讀者可自行Google。

代碼(code)推流

ffmpeg的c++代碼推流,網上也是一搜一大堆。網上也是一搜一大堆。筆者推薦雷神的最簡單的基于FFmpeg的推流器(以推送RTMP為例),能夠滿足基礎的推流需求。但是筆者在實際應用場景時遇到過幾個case,最後總結得到一份相對魯棒可用的code segment:

AVFormatContext *mp_ifmt_ctx = nullptr;
AVFormatContext *mp_ofmt_ctx = nullptr;
uint64_t start_timestamp; // 擷取得到第一幀的時間
// ...

int pushStreaming(AVPacket *pkt, int frm_cnt) {
    // 做篩選:因為實際源視訊檔案可能包括多個音視訊碼流,這裡隻選取一路視訊流m_vs_index和一路音頻流m_as_index。
    if (pkt->stream_index == m_vs_index || pkt->stream_index == m_as_index) {
        // 沒有pts的視訊資料,如未解碼的H.264裸流,需要重新計算其pts。
        if (pkt->pts == AV_NOPTS_VALUE) {
            AVRational time_base = mp_ifmt_ctx->streams[m_vs_index]->time_base;
            // Duration between 2 frames (us)
            int64_t calc_duration = (double)AV_TIME_BASE / 
                    av_q2d(mp_ifmt_ctx->streams[m_vs_index]->r_frame_rate);
            // Reset Parameters
            pkt->pts = (double)(frm_cnt * calc_duration) / 
                    (double)(av_q2d(time_base) * AV_TIME_BASE);
            pkt->dts = pkt->pts;
            pkt->duration = (double)calc_duration /
                            (double)(av_q2d(time_base) * AV_TIME_BASE);
        }
        
        // 筆者在這裡省去了delay操作,讀者可根據需求增加。該操作通過控制推流的速率來減輕推流伺服器的壓力。
        // if (pkt->stream_index == m_vs_index) {
        //     AVRational time_base = mp_ifmt_ctx->streams[m_vs_index]->time_base; 
        //     AVRational time_base_q = {1, AV_TIME_BASE}; 
        //     int64_t pts_time =  av_rescale_q(pkt->dts, time_base, time_base_q); 
        //     int64_t now_time = av_gettime() - start_timestamp;
        //     if (pts_time > now_time) {
        //         av_usleep((unsigned int)(pts_time - now_time));
        //     }
        // }

        //計算延時後,重新指定時間戳
        AVRational istream_base = mp_ifmt_ctx->streams[pkt->stream_index]->time_base;
        AVRational ostream_base = mp_ofmt_ctx->streams[pkt->stream_index]->time_base;
        pkt->pts = av_rescale_q_rnd(pkt->pts, istream_base, ostream_base,
                (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        pkt->dts = av_rescale_q_rnd(pkt->dts, istream_base, ostream_base,
                (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        pkt->pts = pkt->pts < 0 ? 0 : pkt->pts;
        pkt->dts = pkt->dts < 0 ? 0 : pkt->dts;
        pkt->duration = (int)av_rescale_q(pkt->duration, istream_base, ostream_base);
        pkt->pos = -1;
        if (pkt->pts < pkt->dts) {
            return 1;
        }
        // 向推流伺服器推送流資料
        int ret = av_interleaved_write_frame(mp_ofmt_ctx, pkt);
        if (ret < 0) {
            return ret;
        }
    }
    return 0;
}      

III. 推流遇到的坑

筆者遇到過2個推流失敗的case:

推一個RTSP攝像頭的流資料時,一旦打開該攝像頭的音頻軌道就會在av_interleaved_write_frame()函數處出錯。(傳回碼不記得了)

使用英飛拓某幾款攝像頭推流時,總是失敗,傳回碼顯示-33。

以上2個問題都成功定位問題所在:

操作人員在打開音頻軌道時總是同時打開2個音頻軌道,隻要選擇關閉其中1個音頻,馬上就可以推流了;

這幾款攝像頭同時包括多個視訊碼流,似乎他們會通過RTSP同時把多個碼流同時傳過來;最後在代碼中強行過濾視音頻碼流,隻保留一路視訊+一路音頻就可以了。

原因歸結為一個:flv格式至多隻能包括一個視訊流和一個音頻流。

至此,筆者原以為視訊封裝格式就隻是記錄了幾個無關緊要的參數的認知,完全崩塌。據筆者了解,MP4是可以同時包括多路碼流的。看來多媒體/流媒體技術的水還很深呢,還有很多很多地方需要筆者去學習、踩坑、總結呢!自勉自勉。

寫在後面

文章中有不嚴謹的地方,歡迎指摘。

​​​​

繼續閱讀