监控视频采集与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: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。但是在实际运用中由于网络传输和带宽的限制,我们可能会对原始视频流做降率处理,这样就必须要加入解码编码的过程。

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

posted @ 2019-01-12 11:51  冷豪  阅读(5601)  评论(0编辑  收藏  举报