網路監視器直播
            三月 10, 2019
            
  
        
        網路監視器直播
對於網絡攝像機做視頻預覽這塊, 本身其實是非常陌生的, 當時接到這個需求也是相當的頭疼(對於當時一年經驗不到的我來說).當時我們的應用場景是: 多路網絡攝像機通過局域網連接, PC 端能夠實時預覽監控畫面並且畫質達到720p, 延遲不能超過10秒, 多個攝像機能夠切換查看. 由於後端只提供一個RTSP 的直播協議, 所以所有的方案都是圍繞著RTSP這個關鍵詞.
整體想法
經過一波調研(google)之後, 知道瀏覽器對RTSP 協議並不友善, 也就是說我們必須要自己轉碼再提供給瀏覽器使用, 找到的解決思路大概是:
- 轉碼: FFmpeg是一個老牌的轉碼工具,非常強大
- Node.js 用作中轉站接收客戶端發來的攝像機信息及控制FFmpeg 推流
- 最終客戶端接收視頻流
準備
安裝FFmpeg 轉碼工具
- window 平台 
 下載FFmpeg解壓後應該是已經編譯好的文件- 將解壓好的文件放入C 盤根目錄(也可以自行放入其他盤符)下重命名為ffmpeg(方便以後找)
- 設置環境變量, 我的電腦-> 屬性-> 高級系統設置-> 環境變量-> 系統變量-> 新增, 路徑選擇剛剛C 盤下的ffmpeg文件夾中的bin 文件夾
- 註銷或重啟電腦讓環境變量生效
- 測試,在cmd中輸入ffmpeg -version,如果出現版本號之類的東西則成功.
 
- Mac平台 - Mac下可以直接通過Homebrew安裝最為簡單.
- 安裝Homebrew,在終端中輸入ruby -e “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
- 安裝FFmpeg,直接在終端中輸入brew install ffmpeg即可
- 測試,同樣在終端中輸入ffmpeg -version查看版本號
 
安裝Node.js
這個就不展開詳細說了,每個有在工作都會吧…
jsmpeg
這個庫還算比較不錯的了,也是通過websocket來轉發,看官方的例子是在終端中啟動ffmpeg -> websocket ->客戶端通過jsmpeg.min.js解碼在canvas中播放.因為這裡只是實現了播放,然後又去找了一個基於jsmpeg的庫node-rtsp-stream .這個庫只是做了一些封裝讓我們不用自己在終端中手動啟用ffmpeg,在此之上我再加上重啟就能滿足現在的需求.
改造 node-rtsp-stream
- 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);
- 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);
- 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;
- index.js引入- 1 
 2
 3
 4
 5- const Stream = require('./cameraStream'); 
 //cameraStream
 const streams = new Stream();
 streams.start();
- 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在瀏覽器端會出現編譯錯誤問題,不知道原因為何。
查看评论