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

內容概要:php

攝像頭 => FFmpeg => Nginx服務器 => 瀏覽器html

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

 

1.開發流程前端

1.1 經過FFmpeg視頻採集和轉碼html5

  在音視頻處理領域,FFmpeg基本是一種通用的解決方案。雖然做爲測試咱們也能夠藉助OBS等其餘工具,可是爲了更接近項目實戰咱們採用前者。這裏不會專門介紹如何使用FFmpeg,只提供演示代碼。不熟悉FFmpeg的同窗能夠跳過這個部分直接使用工具推流,網上的資料不少請自行查閱。nginx

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

char errorbuf[1024] = { 0 }; // 異常信息
int errorcode = 0; // 異常代碼
AVFormatContext *ic = NULL; // 輸入封裝上下文
AVFormatContext *oc = NULL; // 輸出封裝上下文

char *inUrl = "rtsp://admin:SYhr_5000@192.168.8.107: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。可是在實際運用中因爲網絡傳輸和帶寬的限制,咱們可能會對原始視頻流作降率處理,這樣就必需要加入解碼編碼的過程。git

1.2 推流服務器配置github

  開源的直播軟件解決方案有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下載的官方網址我也直接提供了吧。npm

  下面跳過安裝直接配置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

相關文章
相關標籤/搜索