網路監視器直播

網路監視器直播

網路監視器直播

對於網絡攝像機做視頻預覽這塊, 本身其實是非常陌生的, 當時接到這個需求也是相當的頭疼(對於當時一年經驗不到的我來說).當時我們的應用場景是: 多路網絡攝像機通過局域網連接, PC 端能夠實時預覽監控畫面並且畫質達到720p, 延遲不能超過10秒, 多個攝像機能夠切換查看. 由於後端只提供一個RTSP 的直播協議, 所以所有的方案都是圍繞著RTSP這個關鍵詞.

整體想法

經過一波調研(google)之後, 知道瀏覽器對RTSP 協議並不友善, 也就是說我們必須要自己轉碼再提供給瀏覽器使用, 找到的解決思路大概是:

  1. 轉碼: FFmpeg是一個老牌的轉碼工具,非常強大
  2. Node.js 用作中轉站接收客戶端發來的攝像機信息及控制FFmpeg 推流
  3. 最終客戶端接收視頻流

準備

安裝FFmpeg 轉碼工具

  • window 平台
    下載FFmpeg解壓後應該是已經編譯好的文件

    1. 將解壓好的文件放入C 盤根目錄(也可以自行放入其他盤符)下重命名為ffmpeg(方便以後找)
    2. 設置環境變量, 我的電腦-> 屬性-> 高級系統設置-> 環境變量-> 系統變量-> 新增, 路徑選擇剛剛C 盤下的ffmpeg文件夾中的bin 文件夾
    3. 註銷或重啟電腦讓環境變量生效
    4. 測試,在cmd中輸入ffmpeg -version,如果出現版本號之類的東西則成功.
  • Mac平台

    1. Mac下可以直接通過Homebrew安裝最為簡單.
    2. 安裝Homebrew,在終端中輸入ruby -e “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
    3. 安裝FFmpeg,直接在終端中輸入brew install ffmpeg即可
    4. 測試,同樣在終端中輸入ffmpeg -version查看版本號

安裝Node.js

這個就不展開詳細說了,每個有在工作都會吧…

jsmpeg

這個庫還算比較不錯的了,也是通過websocket來轉發,看官方的例子是在終端中啟動ffmpeg -> websocket ->客戶端通過jsmpeg.min.js解碼在canvas中播放.因為這裡只是實現了播放,然後又去找了一個基於jsmpeg的庫node-rtsp-stream .這個庫只是做了一些封裝讓我們不用自己在終端中手動啟用ffmpeg,在此之上我再加上重啟就能滿足現在的需求.

改造 node-rtsp-stream

  1. node-rtsp-stream/mpeg1muxer.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
      // Generated by CoffeeScript 2.3.2
    (function() {
    var Mpeg1Muxer, child_process, events, util;

    child_process = require('child_process');

    util = require('util');

    events = require('events');

    Mpeg1Muxer = function(options) {
    var key, self;
    self = this;
    this.url = options.url;
    this.ffmpegOptions = options.ffmpegOptions;
    this.additionalFlags = [];
    if (this.ffmpegOptions) {
    for (key in this.ffmpegOptions) {
    this.additionalFlags.push(key, String(this.ffmpegOptions[key]));
    }
    }
    this.spawnOptions = [
    "-rtsp_transport",
    "tcp",
    "-i",
    this.url,
    '-f',
    'mpeg1video',
    // additional ffmpeg options go here
    ...this.additionalFlags,
    '-'
    ];
    this.stream = child_process.spawn("ffmpeg", this.spawnOptions, {
    detached: false
    });
    this.inputStreamStarted = true;
    this.stream.stdout.on('data', function(data) {
    return self.emit('mpeg1data', data);
    });
    this.stream.stderr.on('data', function(data) {
    return self.emit('ffmpegError', data);
    });
    this.stream.stop = function() {
    // console.log(self.stream.pid)
    self.stream.stdin.pause();
    self.stream.kill()
    console.log('ffmpeg is be kill')
    };
    this.stream.on('exit', function(code) {
    return self.emit('ffmpegClose', code)
    });
    return this;
    };

    util.inherits(Mpeg1Muxer, events.EventEmitter);

    module.exports = Mpeg1Muxer;

    }).call(this);
  2. node-rtsp-stream/videoStream.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
          
    // Generated by CoffeeScript 2.3.2
    (function() {
    var Mpeg1Muxer, STREAM_MAGIC_BYTES, VideoStream, events, util, ws;

    ws = require('ws');

    util = require('util');

    events = require('events');

    Mpeg1Muxer = require('./mpeg1muxer');

    STREAM_MAGIC_BYTES = "jsmp"; // Must be 4 bytes

    VideoStream = function(options) {
    this.options = options;
    this.name = options.name;
    this.streamUrl = options.streamUrl;
    this.width = options.width;
    this.height = options.height;
    this.wsPort = options.wsPort;
    this.inputStreamStarted = false;
    this.stream = void 0;
    this.startMpeg1Stream();
    this.pipeStreamToSocketServer();
    return this;
    };

    util.inherits(VideoStream, events.EventEmitter);

    VideoStream.prototype.stop = function() {
    this.wsServer.close();
    this.stream.kill();
    this.inputStreamStarted = false;
    return this;
    };
    // 重新連線
    VideoStream.prototype.restart = function() {
    if (this.mpeg1Muxer) {
    this.mpeg1Muxer.stream.stop()
    console.log('ffmpeg is restart')
    this.inputStreamStarted = false;
    this.stream = void 0;
    this.startMpeg1Stream();
    // 檢查 ffmpeg 是否壞掉或關閉
    this.mpeg1Muxer.on('ffmpegClose', function(code) {
    console.log('ffmpeg closed on ' + code)

    })
    }
    }

    VideoStream.prototype.startMpeg1Stream = function() {
    var gettingInputData, gettingOutputData, inputData, outputData, self;
    this.mpeg1Muxer = new Mpeg1Muxer({
    ffmpegOptions: this.options.ffmpegOptions,
    url: this.streamUrl
    });
    this.stream = this.mpeg1Muxer.stream;
    self = this;
    if (this.inputStreamStarted) {
    return;
    }
    this.mpeg1Muxer.on('mpeg1data', function(data) {
    return self.emit('camdata', data);
    });
    gettingInputData = false;
    inputData = [];
    gettingOutputData = false;
    outputData = [];
    this.mpeg1Muxer.on('ffmpegError', function(data) {
    var size;
    data = data.toString();
    if (data.indexOf('Input #') !== -1) {
    gettingInputData = true;
    }
    if (data.indexOf('Output #') !== -1) {
    gettingInputData = false;
    gettingOutputData = true;
    }
    if (data.indexOf('frame') === 0) {
    gettingOutputData = false;
    }
    if (gettingInputData) {
    inputData.push(data.toString());
    size = data.match(/\d+x\d+/);
    if (size != null) {
    size = size[0].split('x');
    if (self.width == null) {
    self.width = parseInt(size[0], 10);
    }
    if (self.height == null) {
    return self.height = parseInt(size[1], 10);
    }
    }
    }
    });
    this.mpeg1Muxer.on('ffmpegError', function(data) {
    return global.process.stderr.write(data);
    });
    return this;
    };

    VideoStream.prototype.pipeStreamToSocketServer = function() {
    var self;
    self = this;
    this.wsServer = new ws.Server({
    port: this.wsPort
    });
    this.wsServer.on("connection", function(socket) {
    return self.onSocketConnect(socket);
    });
    this.wsServer.broadcast = function(data, opts) {
    var i, results;
    results = [];
    for (i in this.clients) {
    if (this.clients[i].readyState === 1) {
    results.push(this.clients[i].send(data, opts));
    } else {
    results.push(console.log("Error: Client (" + i + ") not connected."));
    }
    }
    return results;
    };
    return this.on('camdata', function(data) {
    return self.wsServer.broadcast(data);
    });
    };

    VideoStream.prototype.onSocketConnect = function(socket) {
    var self, streamHeader;
    // Send magic bytes and video size to the newly connected socket
    // struct { char magic[4]; unsigned short width, height;}
    self = this;
    streamHeader = new Buffer(8);
    streamHeader.write(STREAM_MAGIC_BYTES);
    streamHeader.writeUInt16BE(this.width, 4);
    streamHeader.writeUInt16BE(this.height, 6);
    socket.send(streamHeader, {
    binary: true
    });
    console.log(`${this.name}: New WebSocket Connection (` + this.wsServer.clients.length + " total)");
    return socket.on("close", function(code, message) {
    return console.log(`${this.name}: Disconnected WebSocket (` + self.wsServer.clients.length + " total)");
    });
    };


    module.exports = VideoStream;

    }).call(this);
  3. node 端使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    Stream = require('node-rtsp-stream');
    class streams {
    constructor() {
    this.stream;
    this.restartStream.bind(this)
    }
    start() {
    this.stream = new Stream({
    name: 'cameraStream',
    streamUrl: 'rtsp://192.168.2.236/user=admin&password=&channel=1&stream=0.sdp',
    wsPort: 9999,
    reconnect: true,
    ffmpegOptions: {
    '-reconnect' :1,
    '-reconnect_at_eof':1,
    '-reconnect_streamed' : 1,
    //'-hide_banner': '',
    //'-loglevel': 'panic'
    }
    });
    this.stream.mpeg1Muxer.on('ffmpegClose', (code) => {
    console.log('ffmpeg closed on need restart ' + code);
    this.restartStream();
    })
    }
    restartStream () {
    console.log('restart stream')
    this.stream.restart()
    }
    }
    module.exports = streams; // stream;
  4. index.js 引入

    1
    2
    3
    4
    5
    const Stream = require('./cameraStream');

    //cameraStream
    const streams = new Stream();
    streams.start();
  5. Client端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <canvas id='can'></canvas>
    <script src='jsmpeg.min.js'></script>
    <script>
    let canvas = document.querySelector('#can')
    let ws = new WebSocket('ws://127.0.0.1:11111')
    let player = new jsmpeg(ws, {
    canvas: canvas,
    autoplay: true
    })
    </script>

結論

目前這方法沒出啥問題,不過有時候FFMPEG在瀏覽器端會出現編譯錯誤問題,不知道原因為何。