Node-Media-Server

Node-Media-Server (相對穩定可用性高)

主要應用Node.js 實現的RTSP(結合ffmpeg)/RTMP/HTTP/WebSocket/HLS/DASH流媒體服務器html

特性

  • 跨平臺支持 Windows/Linux/Unix
  • 支持的音視頻編碼 H.264/H.265/AAC/SPEEX/NELLYMOSER
  • 支持緩存最近一個關鍵幀間隔數據,實現RTMP協議秒開
  • 支持事件回調
  • 支持https/wss加密傳輸
  • 支持服務器和流媒體信息統計
  • 支持RTMP直播流轉HLS,DASH直播流
  • 支持RTMP直播流錄製爲MP4文件並開啓faststart
  • 支持RTMP/RTSP中繼(關鍵)
  • 支持多核集羣模式
  • 支持錄製爲MP4回放
  • 支持實時轉碼(關鍵)
  • 支持低延遲HLS/DASH
  • 支持on_connect/on_publish/on_play/on_done 事件回調

用法

git 版本

簡單運行方式前端

npm i
node app.js

多核模式運行vue

node cluster.js

npm 版本(推薦)

單核模式

mkdir nms
cd nms
npm install node-media-server
const { NodeMediaServer } = require('node-media-server');

const config = {
  rtmp: {
    port: 1935,
    chunk_size: 60000,
    gop_cache: true,
    ping: 60,
    ping_timeout: 30
  },
  http: {
    port: 8000,
    allow_origin: '*'
  }
};

var nms = new NodeMediaServer(config)
nms.run();

多核模式

mkdir nms
cd nms
npm install node-media-server
const { NodeMediaCluster } = require('node-media-server');
const numCPUs = require('os').cpus().length;
const config = {
  rtmp: {
    port: 1935,
    chunk_size: 60000,
    gop_cache: true,
    ping: 60,
    ping_timeout: 30
  },
  http: {
    port: 8000,
    allow_origin: '*'
  },
  cluster: {
    num: numCPUs
  }
};

var nmcs = new NodeMediaCluster(config)
nmcs.run();

鑑權驗證(更安全)

加密後的 URL 形式:

rtmp://hostname:port/appname/stream?sign=expires-HashValue
http://hostname:port/appname/stream.flv?sign=expires-HashValue
ws://hostname:port/appname/stream.flv?sign=expires-HashValuejava

1.原始推流或播放地址:node

rtmp://192.168.0.10/live/streamgit

2.配置驗證祕鑰爲: 'nodemedia2017privatekey',同時打開播放和發佈的鑑權開關github

const config = {
  rtmp: {
    port: 1935,
    chunk_size: 60000,
    gop_cache: true,
    ping: 60,
    ping_timeout: 30
  },
  http: {
    port: 8000,
    allow_origin: '*'
  },
  auth: {
    play: true,
    publish: true,
    secret: 'nodemedia2017privatekey'
  }
}

3.請求過時時間爲: 2017/8/23 11:25:21 ,則請求過時時間戳爲:web

1503458721docker

4.md5計算結合「完整流地址-失效時間-密鑰」的字符串:npm

HashValue = md5("/live/stream-1503458721-nodemedia2017privatekey」)
HashValue = 80c1d1ad2e0c2ab63eebb50eed64201a

5.最終請求地址爲

rtmp://192.168.0.10/live/stream?sign=1503458721-80c1d1ad2e0c2ab63eebb50eed64201a
注意:'sign' 關鍵字不能修改成其餘的

RTMP協議傳輸H.265視頻

H.265並無在Adobe的官方規範裏實現,這裏使用id 12做爲標識,也是國內絕大多數雲服務商使用的id號
PC轉碼推流: ffmpeg-hw-win32
純JavaScrip 直播播放器: NodePlayer.js

事件回調

......
nms.run();
nms.on('preConnect', (id, args) => {
  console.log('[NodeEvent on preConnect]', `id=${id} args=${JSON.stringify(args)}`);
  // let session = nms.getSession(id);
  // session.reject();
});

nms.on('postConnect', (id, args) => {
  console.log('[NodeEvent on postConnect]', `id=${id} args=${JSON.stringify(args)}`);
});

nms.on('doneConnect', (id, args) => {
  console.log('[NodeEvent on doneConnect]', `id=${id} args=${JSON.stringify(args)}`);
});

nms.on('prePublish', (id, StreamPath, args) => {
  console.log('[NodeEvent on prePublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
  // let session = nms.getSession(id);
  // session.reject();
});

nms.on('postPublish', (id, StreamPath, args) => {
  console.log('[NodeEvent on postPublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
});

nms.on('donePublish', (id, StreamPath, args) => {
  console.log('[NodeEvent on donePublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
});

nms.on('prePlay', (id, StreamPath, args) => {
  console.log('[NodeEvent on prePlay]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
  // let session = nms.getSession(id);
  // session.reject();
});

nms.on('postPlay', (id, StreamPath, args) => {
  console.log('[NodeEvent on postPlay]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
});

nms.on('donePlay', (id, StreamPath, args) => {
  console.log('[NodeEvent on donePlay]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
});

Https/Wss 視頻加密傳輸

生成證書

openssl genrsa -out privatekey.pem 1024
openssl req -new -key privatekey.pem -out certrequest.csr 
openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out certificate.pem

配置 https支持

const { NodeMediaServer } = require('node-media-server');

const config = {
  rtmp: {
    port: 1935,
    chunk_size: 60000,
    gop_cache: true,
    ping: 60,
    ping_timeout: 30
  },
  http: {
    port: 8000,
    allow_origin: '*'
  },
  https: {
    port: 8443,
    key:'./privatekey.pem',
    cert:'./certificate.pem',
  }
};


var nms = new NodeMediaServer(config)
nms.run();

播放加密傳輸視頻

https://localhost:8443/live/STREAM_NAME.flv
wss://localhost:8443/live/STREAM_NAME.flv

注意:Web瀏覽器播放自簽名的證書需先添加信任才能訪問

API

保護API

const config = {
 .......
   auth: {
    api : true,
    api_user: 'admin',
    api_pass: 'nms2018',
  },
 
 ......
}

注意:基於Basic auth提供驗證,請注意修改密碼,默認並未開啓。

服務器信息統計

http://localhost:8000/api/server

{
  "os": {
    "arch": "x64",
    "platform": "darwin",
    "release": "16.7.0"
  },
  "cpu": {
    "num": 8,
    "load": 12,
    "model": "Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz",
    "speed": 3592
  },
  "mem": {
    "totle": 8589934592,
    "free": 754126848
  },
  "net": {
    "inbytes": 6402345,
    "outbytes": 6901489
  },
  "nodejs": {
    "uptime": 109,
    "version": "v8.9.0",
    "mem": {
      "rss": 59998208,
      "heapTotal": 23478272,
      "heapUsed": 15818096,
      "external": 3556366
    }
  },
  "clients": {
    "accepted": 207,
    "active": 204,
    "idle": 0,
    "rtmp": 203,
    "http": 1,
    "ws": 0
  }
}

流信息統計

http://localhost:8000/api/streams

{
  "live": {
    "s": {
      "publisher": {
        "app": "live",
        "stream": "s",
        "clientId": "U3UYQ02P",
        "connectCreated": "2017-12-21T02:29:13.594Z",
        "bytes": 190279524,
        "ip": "::1",
        "audio": {
          "codec": "AAC",
          "profile": "LC",
          "samplerate": 48000,
          "channels": 6
        },
        "video": {
          "codec": "H264",
          "width": 1920,
          "height": 1080,
          "profile": "Main",
          "level": 4.1,
          "fps": 24
        }
      },
      "subscribers": [
        {
          "app": "live",
          "stream": "s",
          "clientId": "H227P4IR",
          "connectCreated": "2017-12-21T02:31:35.278Z",
          "bytes": 18591846,
          "ip": "::ffff:127.0.0.1",
          "protocol": "http"
        },
        {
          "app": "live",
          "stream": "s",
          "clientId": "ZNULPE9K",
          "connectCreated": "2017-12-21T02:31:45.394Z",
          "bytes": 8744478,
          "ip": "::ffff:127.0.0.1",
          "protocol": "ws"
        },
        {
          "app": "live",
          "stream": "s",
          "clientId": "C5G8NJ30",
          "connectCreated": "2017-12-21T02:31:51.736Z",
          "bytes": 2046073,
          "ip": "::ffff:192.168.0.91",
          "protocol": "rtmp"
        }
      ]
    },
    "stream": {
      "publisher": null,
      "subscribers": [
        {
          "app": "live",
          "stream": "stream",
          "clientId": "KBH4PCWB",
          "connectCreated": "2017-12-21T02:31:30.245Z",
          "bytes": 0,
          "ip": "::ffff:127.0.0.1",
          "protocol": "http"
        }
      ]
    }
  }
}

轉 HLS/DASH 直播流

const { NodeMediaServer } = require('node-media-server');

const config = {
  rtmp: {
    port: 1935,
    chunk_size: 60000,
    gop_cache: true,
    ping: 60,
    ping_timeout: 30
  },
  http: {
    port: 8000,
    mediaroot: './media',
    allow_origin: '*'
  },
  trans: {
    ffmpeg: '/usr/local/bin/ffmpeg',
    tasks: [
      {
        app: 'live',
        hls: true,
        hlsFlags: '[hls_time=2:hls_list_size=3:hls_flags=delete_segments]',
        dash: true,
        dashFlags: '[f=dash:window_size=3:extra_window_size=5]'
      }
    ]
  }
};

var nms = new NodeMediaServer(config)
nms.run();

直播錄製爲MP4文件

const { NodeMediaServer } = require('node-media-server');

const config = {
  rtmp: {
    port: 1935,
    chunk_size: 60000,
    gop_cache: true,
    ping: 60,
    ping_timeout: 30
  },
  http: {
    port: 8000,
    mediaroot: './media',
    allow_origin: '*'
  },
  trans: {
    ffmpeg: '/usr/local/bin/ffmpeg',
    tasks: [
      {
        app: 'vod',
        mp4: true,
        mp4Flags: '[movflags=faststart]',
      }
    ]
  }
};

var nms = new NodeMediaServer(config)
nms.run();

Rtsp/Rtmp 中繼

NodeMediaServer 使用ffmpeg實現RTMP/RTSP的中繼服務。(咱們主要應用這裏將rtsp轉爲rtmp)

靜態拉流

靜態拉流模式在服務啓動時執行,當發生錯誤時自動重連。能夠是一個直播流,也能夠是一個本地文件。理論上並不限制是RTSP或RTMP協議

relay: {
  ffmpeg: '/usr/local/bin/ffmpeg',
  tasks: [
    {
      app: 'cctv',
      mode: 'static',
      edge: 'rtsp://184.72.239.149/vod/mp4:BigBuckBunny_175k.mov',//rtsp
      name: 'BigBuckBunny'
      rtsp_transport : 'tcp' //['udp', 'tcp', 'udp_multicast', 'http']
    }, {
        app: 'iptv',
        mode: 'static',
        edge: 'rtmp://live.hkstv.hk.lxdns.com/live/hks',//rtmp
        name: 'hks'
      }, {
        app: 'mv',
        mode: 'static',
        edge: '/Volumes/ExtData/Movies/Dancing.Queen-SD.mp4',//本地文件
        name: 'dq'
      }
  ]
}

動態拉流

當本地服務器收到一個播放請求,若是這個流不存在,則從配置的邊緣服務器拉取這個流。當沒有客戶端播放這個流時,自動斷開。

relay: {
  ffmpeg: '/usr/local/bin/ffmpeg',
  tasks: [
    {
      app: 'live',
      mode: 'pull',
      edge: 'rtmp://192.168.0.20',
    }
  ]
}

動態推流

當本地服務器收到一個發佈請求,自動將這個流推送到邊緣服務器。

relay: {
  ffmpeg: '/usr/local/bin/ffmpeg',
  tasks: [
    {
      app: 'live',
      mode: 'push',
      edge: 'rtmp://192.168.0.10',
    }
  ]
}

安裝應用流程

安裝ffmpeg(版本要求4.0.0以上)

# 進入/usr/local/src目錄安裝yasm
cd /usr/local/src
wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz
tar zxvf yasm-1.3.0.tar.gz
cd yasm-1.3.0/
./configure 
make && make install
#  進入/usr/local/src目錄安裝
cd /usr/local/src
wget https://ffmpeg.org/releases/ffmpeg-4.0.tar.bz2
tar jxvf ffmpeg-4.0.tar.bz2 
cd ffmpeg-4.0/
./configure 
make && make install
# 查看是否安裝成功
cd /usr/local/bin
ll (存在ffmpeg可執行文件)

安裝Node-Media-Server

# 克隆工程到本地/usr/local/src目錄
cd /usr/local/src
git clone git@github.com:qq1126176532/Node-Media-Server.git

# 開始安裝npm
curl --silent --location https://rpm.nodesource.com/setup_10.x | bash -
yum install -y nodejs
npm install -g cnpm --registry=https://registry.npm.taobao.org

# 查看版本確認安裝成功
npm -v

#執行安裝
cd Node-Media-Server/
npm i 

#修改啓動腳本(這裏以簡單工做模式啓動爲例,多核心集羣模式結合中繼模式存在一些問題)
vim app.js:
---
const { NodeMediaServer } = require('./index');

const config = {
  rtmp: {
    port: 1935,
    chunk_size: 60000,
    gop_cache: true,
    ping: 60,
    ping_timeout: 30
  },
  http: {
    port: 8000,
    webroot: './public',
    mediaroot: './media',
    allow_origin: '*'
  },
  https: {
    port: 8443,
    key: './privatekey.pem',
    cert: './certificate.pem',
  },
  auth: {
    api: true,
    api_user: 'admin',
    api_pass: 'admin',
    play: false,
    publish: false,
    secret: 'nodemedia2017privatekey'
  },
  //引入中繼模式任務
  relay: {
    //指定ffmpeg可執行文件位置
    ffmpeg: '/usr/local/bin/ffmpeg',
    tasks: [
        {
            //應用名稱
            app: 'iptv',
            //工做模式 靜態便可
            mode: 'static',
            //中繼地址
            edge: 'rtsp://184.72.239.149/vod/mp4:BigBuckBunny_175k.mov',
            //訪問資源名稱
            name: 'rtsp',
            //傳輸協議
            rtsp_transport : 'tcp' //['udp', 'tcp', 'udp_multicast', 'http']
        }
    ]
  },
};


let nms = new NodeMediaServer(config)
nms.run();

nms.on('preConnect', (id, args) => {
  console.log('[NodeEvent on preConnect]', `id=${id} args=${JSON.stringify(args)}`);
  // let session = nms.getSession(id);
  // session.reject();
});

nms.on('postConnect', (id, args) => {
  console.log('[NodeEvent on postConnect]', `id=${id} args=${JSON.stringify(args)}`);
});

nms.on('doneConnect', (id, args) => {
  console.log('[NodeEvent on doneConnect]', `id=${id} args=${JSON.stringify(args)}`);
});

nms.on('prePublish', (id, StreamPath, args) => {
  console.log('[NodeEvent on prePublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
  // let session = nms.getSession(id);
  // session.reject();
});

nms.on('postPublish', (id, StreamPath, args) => {
  console.log('[NodeEvent on postPublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
});

nms.on('donePublish', (id, StreamPath, args) => {
  console.log('[NodeEvent on donePublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
});

nms.on('prePlay', (id, StreamPath, args) => {
  console.log('[NodeEvent on prePlay]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
  // let session = nms.getSession(id);
  // session.reject();
});

nms.on('postPlay', (id, StreamPath, args) => {
  console.log('[NodeEvent on postPlay]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
});

nms.on('donePlay', (id, StreamPath, args) => {
  console.log('[NodeEvent on donePlay]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
});
---
# 保存退出


# 啓動服務
nohup node app.js &

# 查看服務是否啓動成功
ps -ef | grep "app.js"
[root@localhost Node-Media-Server]# ps -ef | grep "app.js"
root      37745 127550  7 10:31 pts/1    00:00:00 node app.js
tail -f nohup.out(查看啓動日誌)
# 三個所用(1935 8000 8443)端口暴露
firewall-cmd --zone=public --add-port=1935/tcp --permanent
firewall-cmd --zone=public --add-port=8000/tcp --permanent
firewall-cmd --zone=public --add-port=8443/tcp --permanent

#(關閉端口 若是開錯了備用還原)
firewall-cmd --zone=public --remove-port=8080/tcp --permanent

# 從新加載配置
firewall-cmd --reload

# 重啓防火牆
systemctl restart firewalld.service

# 查看全部開放端口
firewall-cmd --zone=public --list-ports

客戶端

引用資料及思考:

感謝以下連接提供文章資源

前期準備和思考

  • 1.以前沒作高這方面的內容,起始渡鳥狗哥都找不到幫助太多的有價值的資料。有時候java搞起來不方便也能夠傾向於考慮一下其餘腳本語言或者方式的實現。
  • 2.ws-socket:此方案最終摒棄緣由以下:須要對接外網收費服務,免費服務不穩定,須要按期更新密鑰,鏈接數有限。docker服務地址官網
  • 3.Node-Media-Server方法切合實際需求,可離線內網使用,畫面問題需後期持續優化。
  • 4.應用Node.js基於Chrome V8 引擎的 JavaScript 運行環境,使用事件驅動、非阻塞式 I/O 的模型輕量高效。
  • 5.服務穩定,支持多協議方便後期擴展,服務單一避免冗餘和服務帶來的代碼臃腫,rtsp推流方式多樣,提供加密傳輸機制更加安全。
本站公眾號
   歡迎關注本站公眾號,獲取更多信息