1. 程式人生 > >監控視訊採集與Web直播開發全流程分析

監控視訊採集與Web直播開發全流程分析

內容概要:

攝像頭 => FFmpeg => Nginx伺服器 => 瀏覽器

  • 從攝像頭拉取rtsp流
  • 轉碼成rtmp流向推流伺服器寫入
  • 利用html5播放

 

1.開發流程

1.1 通過FFmpeg視訊採集和轉碼

  在音視訊處理領域,FFmpeg基本是一種通用的解決方案。雖然作為測試我們也可以藉助OBS等其他工具,但是為了更接近專案實戰我們採用前者。這裡不會專門介紹如何使用FFmpeg,只提供演示程式碼。不熟悉FFmpeg的同學可以跳過這個部分直接使用工具推流,網上的資料很多請自行查閱。

// 註冊解碼器和初始化網路模組
av_register_all();
avformat_network_init();

char errorbuf[1024] = { 0 }; // 異常資訊 int errorcode = 0; // 異常程式碼 AVFormatContext *ic = NULL; // 輸入封裝上下文 AVFormatContext *oc = NULL; // 輸出封裝上下文 char *inUrl = "rtsp://admin:[email protected]:554/H264"; // rtsp輸入URL char *outUrl = "rtmp://192.168.1.118/rtmp_live/1"; // rtmp輸出URL AVDictionary *opts = NULL; av_dict_set(
&opts, "max_delay", "500", 0); av_dict_set(&opts, "rtsp_transport", "tcp", 0); errorcode = avformat_open_input(&ic, inUrl, NULL, &opts); if (errorcode != 0) { av_strerror(errorcode, errorbuf, sizeof(errorbuf)); cout << errorbuf << endl; return -1; } errorcode = avformat_find_stream_info(ic, NULL);
if (errorcode < 0) { av_strerror(errorcode, errorbuf, sizeof(errorbuf)); cout << errorbuf << endl; return -1; } av_dump_format(ic, 0, inUrl, 0); // 定義輸出封裝格式為FLV errorcode = avformat_alloc_output_context2(&oc, NULL, "flv", outUrl); if (!oc) { av_strerror(errorcode, errorbuf, sizeof(errorbuf)); cout << errorbuf << endl; return -1; } // 遍歷流資訊初始化輸出流 for (int i = 0; i < ic->nb_streams; ++i) { AVStream *os = avformat_new_stream(oc, ic->streams[i]->codec->codec); if (!os) { av_strerror(errorcode, errorbuf, sizeof(errorbuf)); cout << errorbuf << endl; return -1; } errorcode = avcodec_parameters_copy(os->codecpar, ic->streams[i]->codecpar); if (errorcode != 0) { av_strerror(errorcode, errorbuf, sizeof(errorbuf)); cout << errorbuf << endl; return -1; } os->codec->codec_tag = 0; } av_dump_format(oc, 0, outUrl, 1); errorcode = avio_open(&oc->pb, outUrl, AVIO_FLAG_WRITE); if (errorcode < 0) { av_strerror(errorcode, errorbuf, sizeof(errorbuf)); cout << errorbuf << endl; return -1; } errorcode = avformat_write_header(oc, NULL); if (errorcode < 0) { av_strerror(errorcode, errorbuf, sizeof(errorbuf)); cout << errorbuf << endl; return -1; } AVPacket pkt; // 獲取時間基數 AVRational itb = ic->streams[0]->time_base; AVRational otb = oc->streams[0]->time_base; while (true) { errorcode = av_read_frame(ic, &pkt); if (pkt.size <= 0) { continue; } // 重新計算AVPacket的時間基數 pkt.pts = av_rescale_q_rnd(pkt.pts, itb, otb, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)); pkt.dts = av_rescale_q_rnd(pkt.dts, itb, otb, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)); pkt.duration = av_rescale_q_rnd(pkt.duration, itb, otb, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)); pkt.pos = -1; errorcode = av_interleaved_write_frame(oc, &pkt); if (errorcode < 0) { av_strerror(errorcode, errorbuf, sizeof(errorbuf)); cout << errorbuf << endl; continue; } }

  程式碼中的輸入和輸出URL替換為實際地址,上面的程式碼並沒有做任何編碼和解碼的操作,只是把從攝像頭讀取到的AVPacket做了一次轉封裝並根據time_base重新計算了一下pts和dts。但是在實際運用中由於網路傳輸和頻寬的限制,我們可能會對原始視訊流做降率處理,這樣就必須要加入解碼編碼的過程。

1.2 推流伺服器配置

  開源的直播軟體解決方案有SRS(Simple-RTMP-Server)和nginx-rtmp-module,前者是國人發起的一個優秀的開源專案,目前國內很多公司都使用它作為直播解決方案,由C++編寫;後者依賴Nginx,以第三方模組的方式提供直播功能,由C編寫。資料顯示SRS的負載效率和直播效果優於nginx-rtmp-module,並且後者已經有一年沒有做任何更新了。不過考慮到實際需求我還是決定使用nginx-rtmp-module,並且為了方便後期與Web整合,我們使用基於它開發的nginx-http-flv-module。關於nginx-http-flv-module的內容大家可以訪問《基於nginx-rtmp-module模組實現的HTTP-FLV直播模組nginx-http-flv-module》,安裝和配置說明訪問他的GitHub中文說明,與nginx-rtmp-module有關的配置說明推薦訪問官方wiki,當然Nginx下載的官方網址我也直接提供了吧。

  下面跳過安裝直接配置nginx.conf

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}
rtmp_auto_push on;
rtmp_auto_push_reconnect 1s;
rtmp_socket_dir /tmp;

rtmp {
    timeout 10s;
    out_queue 4096;
    out_cork 8;

    log_interval 5s;
    log_size 1m;

    server {
        listen 1935;
        chunk_size 4096;
        application rtmp_live {
            live on;
        gop_cache on;
        }
    }
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }

    location /http_live {
        flv_live on;
        chunked_transfer_encoding on;
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Credentials' 'true';
    }
       
        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}
nginx.conf

  我們要關注的重點是gop_cache,具體後面會解釋。完成以後如果沒有其他問題,我們推流伺服器就可以使用了。

1.3 Web框架

  這裡我採用了Angular和flv.js的整合方案,具體的使用其實也很簡單。通過npm引入,然後直接在ts檔案中宣告一下即可。如果你對Angular不熟悉也可以選擇其他前端框架。下面是html的內容以及ts程式碼:

<div class="camera" nz-row>
  <div nz-col [nzSpan]="20">
    <div nz-col [nzSpan]="12" class="camera_screen">
      <video class="videoElement" controls="controls"></video>
    </div>
    <div nz-col [nzSpan]="12" class="camera_screen">
      <video class="videoElement" controls="controls"></video>
    </div>
    <div nz-col [nzSpan]="12" class="camera_screen">
      <video class="videoElement" controls="controls"></video>
    </div>
    <div nz-col [nzSpan]="12" class="camera_screen">
      <video class="videoElement" controls="controls"></video>
    </div>
  </div>
  <div class="camera_stand" nz-col [nzSpan]="4"></div>
</div>
loadVideo(httpUrl: string, index: number): void {
    this.player = document.getElementsByClassName('videoElement').item(index);
    if (flvjs.default.isSupported()) {
      // 建立flvjs物件
      this.flvPlayer = flvjs.default.createPlayer({
        type: 'flv',        // 指定視訊型別
        isLive: true,       // 開啟直播
        hasAudio: false,    // 關閉聲音
        cors: true,         // 開啟跨域訪問
        url: httpUrl,       // 指定流連結
      },
      {
        enableStashBuffer: false,
        lazyLoad: true,
        lazyLoadMaxDuration: 1,
        lazyLoadRecoverDuration: 1,
        deferLoadAfterSourceOpen: false,
        statisticsInfoReportInterval: 1,
        fixAudioTimestampGap: false,
        autoCleanupSourceBuffer: true,
        autoCleanupMaxBackwardDuration: 5,
        autoCleanupMinBackwardDuration: 2,
      });

      // 將flvjs物件和DOM物件繫結
      this.flvPlayer.attachMediaElement(this.player);
      // 載入視訊
      this.flvPlayer.load();
      // 播放視訊
      this.flvPlayer.play();
      this.player.addEventListener('progress', function() {
        const len = this.buffered.length ;
        const buftime = this.buffered.end(len - 1) - this.currentTime;
        if (buftime >= 0.5) {
          this.currentTime = this.buffered.end(len - 1);
        }
      });
    }
  }

  有關flv的引數配置與事件監聽器後面會專門解釋,先展示一下直播的效果:

  這裡模擬了四路視訊的情況,效果還是很理想的。

 

2. 直播延遲分析及解決方案

2.1 網路因素

  目前使用在直播領域比較常用的網路協議有rtmp和http_flv。hls是蘋果公司開發的直播協議,多用在蘋果自己的裝置上,延遲比較明顯。此外從播放器的角度來看,有一個因素也是需要考慮的。我們知道視訊傳輸分為關鍵幀(I)和非關鍵幀(P/B),播放器對畫面進行解碼的起始幀必須是關鍵幀。但是受到直播條件的約束,使用者開啟播放的時候接收到的第一幀視訊幀不會剛剛好是關鍵幀。根據我在接收端對於海康攝像機的測試,每兩個關鍵幀之間大約有50幀非關鍵幀,而裝置的fps值是25,即每秒25幀畫面。也就是說,大概每2每秒才會有一幀關鍵幀。那麼假設使用者在網路傳輸的第1秒開始播放,推流伺服器就面臨兩個選擇:讓播放端黑屏1秒等到下一個關鍵幀才開始播放從上一個關鍵幀開始傳送出去讓使用者端有1秒的畫面延遲。實際上,無論怎麼選擇都是一個魚與熊掌的故事,要想直播沒有延遲就得忍受黑屏,要想使用者體驗好就會有畫面延遲。

  這裡我們選擇後者,先保證使用者體驗,後面我會用其他手段來彌補畫面延遲的缺點。所以在nginx的配置選項中開啟gop_cache。

2.2 播放器緩衝

  無論是在C端還是在B端,從伺服器讀取到的資料流都不會被立刻播放而是首先被緩衝起來。由於我們的網路協議採用TCP連線,資料包有可能在客戶端不斷累積,造成播放延遲。回到上面的loadVideo方法重點看addEventListener。HTML5提供了與音視訊播放相關的事件監聽器,this.buffered.end(len - 1)返回最後一個緩衝區的結束時間。我們可以利用這個緩衝時間與當前時間進行比較,當大於某一閾值的時候就直接向後跳幀。要注意這個閾值的設定時間越短,網路抖動越有可能影響收看效果。所以我們需要根據實際業務需求來設定。同時通過在播放端動態調整緩衝進度既保證了使用者在開啟瀏覽器的第一時間就看到畫面又降低了直播延遲。

2.3 傳輸延遲

  以上考慮的情況都是在區域網內部進行,網路延遲基本忽略不計。但是如果您的應用要部署到公網上,傳輸延遲就必須要考慮了。

 

3.總結

  本文的重點是如何在保證使用者體驗的基礎上儘量提升直播效果,這類需求一般適用於企業內部監控系統的實施和異地辦公地點舉行視訊會議。傳統的直接使用rtsp網路攝像機所提供的C端解決方案也能夠達到極小的延遲和較高的視訊效果。但是部署起來要比B端複雜。

  最後號外一下我的QQ討論群:960652410