文章11
标签16
分类0
低延迟直播方案踩坑记录

低延迟直播方案踩坑记录

学校小学期要整带入侵检测的视频监控,其中难点之一就是从摄像头拿流在前端播放。摄像头是海康的,拿的是rstp流,有点小麻烦。网上冲浪了一会,现成的方案大概有那么几种:

  1. 转成rtmp协议流,前端需要有flash。哈哈,flash爪巴;
  2. 依靠浏览器插件或者本地插件。比如海康自己就整了个本地插件,播放直播的时候用插件播放界面覆盖在一片黑屏上。浏览器插件的话也就vlc啥的,不仅过时,而且不太优雅;
  3. 转成hls流,直接塞进video标签,就是延迟堪忧;
  4. 转成flv流,然后用flvjs或者基于此的mpegts播放,也就是通过mse接口做一些必要的操作,然后把数据塞进video标签;
  5. 自己实现websocket流喂给video标签,或者简单粗暴放一个img标签(之前折腾opencv+flask时用的方法,太暴力了);
  6. 用WebRTC技术弄类似视频聊天室这样的东西,当然也有现成的实现;

当然,一开始让我整转流我是拒绝的,这意味着要加服务器,或者增加原本做图形计算的后端的压力,所以最开始想的是直接前端收rtsp流然后转码。

ffmpeg.wasm纯前端

见https://github.com/ffmpegwasm/ffmpeg.wasm

ffmpeg.js目前只有webm和mp4两类build,只能放弃。但是ffmpeg.wasm不一样,ffmpeg.wasm顾名思义是一个用了WebAssembly技术的好东西,性能非常好,功能非常多,于是我赶紧试了一下:

const VideoBlock: React.FC<{
  source: string;
}> = ({source})  =>{
  return (
    <div>
      <video id="player" controls src={source}></video>
    </div>
  )
}

const LiveStream = (): React.ReactNode => {
  const transcodeRtspToPlay = async () => {
    try{
      const { createFFmpeg } = FFmpeg;
      const ffmpeg = createFFmpeg({
        log: true,
        corePath: 'https://unpkg.com/@ffmpeg/core@0.10.0/dist/ffmpeg-core.js',
      });
      await ffmpeg.load();
      await ffmpeg.run(
        '-re',
        '-i',
        'rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov',
        '-vcodec',
        'copy',
        '-acodec',
        'copy',
        '-f',
        'flv',
        'test.flv',
      );
      const data = ffmpeg.FS('readFile', 'test.flv');
      const videoUrl = URL.createObjectURL(new Blob([data.buffer], { type: 'video/flv' }));
      ReactDOM.render(
        <VideoBlock source={videoUrl}></VideoBlock>,
        document.getElementById('video-block-container')
      );
    }catch (error){
      message.error(error.message)
    }
  };

笑死,根本不能用。

翻了一下issue,说是不能接受rstp/rtmp流输入:

https://github.com/ffmpegwasm/ffmpeg.wasm/issues/67#issuecomment-721452717

Right now rtsp/rtmp is not supported due to the external libraries is unable to integrate with ffmpeg.wasm, I am still looking for possibilities.

不过这个功能呼声还挺高的(https://github.com/ffmpegwasm/ffmpeg.wasm/issues/61#issuecomment-620948390),希望未来可以加入吧。

(同时还发现了一个挺有意思的东西,JSMpeg – MPEG1 Video & MP2 Audio Decoder in JavaScript,在这里马克一下,有空再看吧)

ffmpeg转推HLS流+video标签

hls本质上就是一堆切片加一个不断更新的播放列表文件。前端没啥好说的,收一个m3u8就行,video标签直接就可以放,后端用python直接启动ffmpeg然后进行一波流的推,搭一个nginx反代就完事了。延迟很高,20s左右,优化了一波ffmpeg的参数(i帧间隔啊、切片间隔啊、缓存数啊啥的)可以到6s左右。这里需要看一下ffmpeg的文档,稍微了解一下h264有关视频i帧p帧b帧和gop之类的知识。

因为延迟太高,放弃。

ffmpeg转推FLV流+mpegts.js

转推FLV流的过程和HLS类似,nginx有个模块叫http-flv-module,非常好用,但是需要自己编译进去。linux下倒是好说,问题是小组内全是win,我的机器虽然有kubuntu但是是双系统,不想折腾wsl。阿里云的服务器也早就白嫖干净了。本着能省就省的精神,我毅然决然的走上了用win编译win版nginx的不归路。

前略,中略,后略,总之就是非常蛋疼,浪费了好几个钟头的人生…

这里放一下nginx的配置:


#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;
}


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 8080;

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        location /live {
            flv_live on; #打开 HTTP 播放 FLV 直播流功能
            chunked_transfer_encoding on; #支持 'Transfer-Encoding: chunked' 方式回复

            add_header 'Access-Control-Allow-Origin' '*'; #添加额外的 HTTP 头
            add_header 'Access-Control-Allow-Credentials' 'true'; #添加额外的 HTTP 头
        }
    }
}

rtmp_auto_push on;
rtmp_auto_push_reconnect 1s;
rtmp_socket_dir /tmp;

rtmp {

    out_queue           4096;
    out_cork            8;
    max_streams         128;
    timeout             10s;
    drop_idle_publisher 10s;

    log_interval 5s; #log 模块在 access.log 中记录日志的间隔时间,对调试非常有用
    log_size     1m; #log 模块用来记录日志的缓冲区大小

    server {
        listen 1935; #默认的 1935 端口

        application flv {
            live on;
            gop_cache on; #打开 GOP 缓存,减少首屏等待时间
        }

        # application hls {
        #     live on;
        #     hls on;
        #     hls_path /tmp/hls;
        # }

        # application dash {
        #     live on;
        #     dash on;
        #     dash_path /tmp/dash;
        # }
    }
}

其实也就是按照http-flv-module给的文档xjb配(注意跨域头),然后开ffmpeg往nginx给的地址端口推由rtsp流转换的flv流即可。

前端部分一开始用的flvjs,延迟还是不太理想,大概2s左右。后来翻到了mpegts.js,延迟就给干到了500ms,舒服。

这里直接贴一手播放器组件的完整代码,没用函数式用了类式组件:

import Mpegts from 'mpegts.js';
import React from 'react';

interface MpegtsVideoProps {
  mediaDataSource: Mpegts.MediaDataSource;
  config?: Mpegts.Config;
  autoPlay?: boolean;
  controls?: boolean;
  onProgress?: (player?: Mpegts.Player, video?: React.RefObject<HTMLVideoElement>) => void;
  onPlay?: (player?: Mpegts.Player, video?: React.RefObject<HTMLVideoElement>) => void;
  onPause?: (player?: Mpegts.Player, video?: React.RefObject<HTMLVideoElement>) => void;
  onPlayerLoadingComplete?: (player?: Mpegts.Player) => void;
  className?: string;
}

class MpegtsVideo extends React.Component<MpegtsVideoProps> {
  videoRef = React.createRef<HTMLVideoElement>();
  player = Mpegts.createPlayer(this.props.mediaDataSource, this.props.config);
  isPlayerInit = false;

  componentDidMount() {
    if (this.videoRef.current && Mpegts.isSupported() && !this.isPlayerInit) {
      this.player.attachMediaElement(this.videoRef.current);
      this.player.load();
      this.player.play();
      this.player.on(Mpegts.Events.ERROR, () => {
        this.isPlayerInit = false;
      });
      this.player.on(Mpegts.Events.LOADING_COMPLETE, () => {
        this.isPlayerInit = true;
        if (this.props.onPlayerLoadingComplete) this.props.onPlayerLoadingComplete(this.player);
      });
    }
  }

  componentWillUnmount() {
    if (this.isPlayerInit) {
      this.player.detachMediaElement();
      this.player.pause();
      this.player.unload();
      this.player.destroy();
      this.isPlayerInit = false;
    }
  }

  render() {
    return (
      <div>
        <video
          className={this.props.className}
          ref={this.videoRef}
          onProgress={() => {
            if (this.props.onProgress) this.props.onProgress(this.player, this.videoRef);
          }}
          autoPlay={this.props.autoPlay}
          controls={this.props.controls}
          onPlay={() => {
            if (this.props.onPlay) this.props.onPlay(this.player, this.videoRef);
          }}
          onPause={() => {
            if (this.props.onPause) this.props.onPause(this.player, this.videoRef);
          }}
        ></video>
      </div>
    );
  }
}

export default MpegtsVideo;

什么?你问我为什么不直接按照mpegts给的播放器api来?因为懒

到这里其实已经可以实现想要的效果了,因为小学期一共就两周,而且这之前完全没碰过react,自己慢慢摸索React的函数式组件和各种生命周期hook就花了不少时间,所以也没时间探索其他方案了…个人比较在意webrtc的实现,也翻了不少方案,不过还是等有空再说吧

本文作者:.torrent
本文链接:https://blog.hitachimako.top/2021/fuck-live-stream/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可