From d30c07e5e38d8c8a26f9fe472a7d6c58ced15048 Mon Sep 17 00:00:00 2001 From: ilim Date: Sun, 21 Jul 2019 02:54:09 +0900 Subject: [PATCH 01/11] use 3rd-party capture --- README.md | 21 ++++----------------- app.js | 5 +++++ package-lock.json | 5 +++++ package.json | 1 + rtmp-server.js | 39 +++++++++++++++++++++++++++++++++++++++ stream.js | 3 --- 6 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 rtmp-server.js diff --git a/README.md b/README.md index b7359fe..8bc5486 100644 --- a/README.md +++ b/README.md @@ -116,25 +116,14 @@ npm start ### Streaming -Linux の場合は自動で行われますが,それ以外のときエラーが発生します。 -その時の手動実行方法を書き置きます。 - -rtmp に向けストリーミングします。 -OBS などでも行えますがここでは ffmpeg の例を書きます。 - -**Use Video files** - -```bash= -ffmpeg -re -i example.mp4 -c copy -f flv rtmp://localhost/live/stream -``` +まず`rtmp://localhost:1935/live` に向けストリームキー`rtmp`をつけ OBS 等でストリーミングします。 +その後以下のコマンドを実行します **Use USB Camera on Ubuntu** ```bash= ffmpeg \ - -framerate 5 \ - -video_size 960x720 \ - -i /dev/video0 \ + -i rtmp://localhost:1935/live/rtmp \ -vcodec libx264 \ -preset veryfast \ -tune zerolatency \ @@ -150,9 +139,7 @@ ffmpeg \ ```bash= ffmpeg \ - -f avfoundation \ - -framerate 30 \ - -re -i 0 \ + -re -i rtmp://localhost:1935/live/rtmp \ -r 5 \ -vcodec libx264 \ -preset veryfast \ diff --git a/app.js b/app.js index 31a9bfd..f7bfd44 100644 --- a/app.js +++ b/app.js @@ -1,3 +1,4 @@ +const RtmpServer = require('./rtmp-server'); const ngrok = require('./ngrok'); const Server = require('./server'); const aws = require('./aws'); @@ -14,6 +15,10 @@ const config = { privateKey: process.env.LIVE_PRIVATE_KEY, }; +const liveServer = new RtmpServer(); +liveServer.on(); +liveServer.run(); + const disk = new Stream('bushitsuchan'); disk .run() diff --git a/package-lock.json b/package-lock.json index 3fba473..6e919cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2064,6 +2064,11 @@ "glob": "^7.1.3" } }, + "rtmp-server": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/rtmp-server/-/rtmp-server-0.2.0.tgz", + "integrity": "sha1-eZfv2SbTr6jNabNXl6TV8A4exFU=" + }, "run-async": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", diff --git a/package.json b/package.json index b2227e1..a9cc461 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "morgan": "^1.9.1", "ngrok": "^3.2.1", "node-ramdisk": "^1.2.2", + "rtmp-server": "^0.2.0", "util": "^0.12.1" }, "devDependencies": { diff --git a/rtmp-server.js b/rtmp-server.js new file mode 100644 index 0000000..e9ea09b --- /dev/null +++ b/rtmp-server.js @@ -0,0 +1,39 @@ +const RtmpServer = require('rtmp-server'); + +module.exports = class { + constructor() { + this.rtmpServer = new RtmpServer(); + } + + on() { + this.rtmpServer.on('error', (err) => { + throw err; + }); + + this.rtmpServer.on('client', (client) => { + // client.on('command', command => { + // console.log(command.cmd, command); + // }); + + client.on('connect', () => { + console.log('connect', client.app); + }); + + client.on('play', ({ streamName }) => { + console.log('PLAY', streamName); + }); + + client.on('publish', ({ streamName }) => { + console.log('PUBLISH', streamName); + }); + + client.on('stop', () => { + console.log('client disconnected'); + }); + }); + } + + run() { + this.rtmpServer.listen(1935); + } +}; diff --git a/stream.js b/stream.js index f822c5a..274ddca 100644 --- a/stream.js +++ b/stream.js @@ -32,9 +32,6 @@ module.exports = class { } })); console.log(`create ramdisk on ${this.mountPath}.`); - deamon( - `ffmpeg -framerate 5 -video_size 960x720 -i /dev/video0 -vcodec libx264 -preset veryfast -tune zerolatency -b 8M -vf "drawtext=fontfile=/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf: text="%{localtime:%T}": fontcolor=white@0.8: x=7: y=700" -hls_flags delete_segments -g 20 -f hls ${this.mountPath}/output.m3u8`, - ); return this.mountPath; } From f1b991d0c87f45bff0a3e8086a57387209a30959 Mon Sep 17 00:00:00 2001 From: ilim Date: Sun, 21 Jul 2019 16:08:06 +0900 Subject: [PATCH 02/11] add photo-viewer --- README.md | 6 +-- app.js | 26 ++++++++-- rtmp-server.js | 4 +- server.js | 104 ++++++++++++++++++++++++++-------------- stream.js | 9 +++- views/photo-viewer.html | 33 +++++++++++++ views/viewer.html | 4 +- 7 files changed, 138 insertions(+), 48 deletions(-) create mode 100644 views/photo-viewer.html diff --git a/README.md b/README.md index 8bc5486..8bcf6f5 100644 --- a/README.md +++ b/README.md @@ -116,14 +116,14 @@ npm start ### Streaming -まず`rtmp://localhost:1935/live` に向けストリームキー`rtmp`をつけ OBS 等でストリーミングします。 +まず`rtmp://localhost:1935/live` に向けストリームキー`bushitsuchan`をつけ OBS 等でストリーミングします。 その後以下のコマンドを実行します **Use USB Camera on Ubuntu** ```bash= ffmpeg \ - -i rtmp://localhost:1935/live/rtmp \ + -i rtmp://localhost:1935/live/bushitsuchan \ -vcodec libx264 \ -preset veryfast \ -tune zerolatency \ @@ -139,7 +139,7 @@ ffmpeg \ ```bash= ffmpeg \ - -re -i rtmp://localhost:1935/live/rtmp \ + -re -i rtmp://localhost:1935/live/bushitsuchan \ -r 5 \ -vcodec libx264 \ -preset veryfast \ diff --git a/app.js b/app.js index f7bfd44..80fe623 100644 --- a/app.js +++ b/app.js @@ -13,6 +13,8 @@ const config = { slackClientSecret: process.env.SLACK_CLIENT_SECRET, wsId: process.env.WORKSTATION_ID, privateKey: process.env.LIVE_PRIVATE_KEY, + debug: false, + isMac: false, }; const liveServer = new RtmpServer(); @@ -21,13 +23,27 @@ liveServer.run(); const disk = new Stream('bushitsuchan'); disk - .run() + .run(config.isMac) .then(async (mountPath) => { - const ngrokUrl = await ngrok.run(process.env.NGROK_TOKEN); - const awsUrl = await aws.run(config, ngrokUrl); - // console.log(`Remote URL: ${awsUrl}`); + let ngrokUrl; + let awsUrl; - const server = new Server(ngrokUrl, awsUrl, mountPath, config); + if (config.debug) { + ngrokUrl = null; + awsUrl = null; + } else { + ngrokUrl = await ngrok.run(process.env.NGROK_TOKEN); + awsUrl = await aws.run(config, ngrokUrl); + console.log(`Remote URL: ${awsUrl}`); + } + + const server = new Server( + ngrokUrl, + awsUrl, + mountPath, + config, + 'rtmp://localhost:1935/live/bushitsuchan', + ); server.run(); }) .catch((e) => { diff --git a/rtmp-server.js b/rtmp-server.js index e9ea09b..3838cc0 100644 --- a/rtmp-server.js +++ b/rtmp-server.js @@ -33,7 +33,7 @@ module.exports = class { }); } - run() { - this.rtmpServer.listen(1935); + run(port = 1935) { + this.rtmpServer.listen(port); } }; diff --git a/server.js b/server.js index 384aa34..032ac28 100644 --- a/server.js +++ b/server.js @@ -5,6 +5,7 @@ const cors = require('cors'); const morgan = require('morgan'); const cookieSession = require('cookie-session'); const helmet = require('helmet'); +const childProcess = require('child_process'); const getToken = async (code, clientId, clientSecret) => { if (code === undefined) { @@ -69,6 +70,8 @@ module.exports = class { this.app.use(morgan('short')); this.app.use(cors()); this.routing(); + + this.wCap = null; } routing() { @@ -114,66 +117,97 @@ module.exports = class { res.send('You have successfully logged out'); }); - this.app.use(['/auth', '/viewer'], (req, res, next) => { + if (!this.config.debug) { + this.app.use(['/auth', '/viewer', '/photo-viewer', '/stream', '/photo'], (req, res, next) => { + const { token } = req.session; + if (token) { + next(); + return; + } + res.redirect('login'); + }); + } + this.app.get('/auth', (req, res) => { const { token } = req.session; - if (token) { - next(); + if (!this.config.debug) { + res.json({ + hlsAddress: 'stream/output.m3u8', + photoAddress: 'photo', + }); return; } - res.redirect('login'); - }); - - this.app.get('/auth', (req, res) => { - const { token } = req.session; authorize(token, this.config.wsId) .then(() => { req.session.lastAutedTime = Date.now(); res.json({ - address: 'stream/output.m3u8', + hlsAddress: 'stream/output.m3u8', + photoAddress: 'photo', }); }) .catch((e) => { if (e instanceof TypeError) { - res.status(401).end(); // 再login + res.sendStatus(401).end(); // 再login } else { - res.status(403).end(); // 権限をもっていない + res.sendStatus(403).end(); // 権限をもっていない } }); }); - this.app.get('/viewer', (req, res) => { - res.sendFile('./views/viewer.html', { root: __dirname }); + this.app.get(['/viewer', '/photo-viewer'], (req, res) => { + res.sendFile(`./views/${req.url}.html`, { root: __dirname }); }); const expireTime = 60 * 1000; - this.app.use('/stream', (req, res, next) => { - const { lastAutedTime, token } = req.session; - if (!token) { - res.status(401).end(); - } - if (!lastAutedTime || lastAutedTime + expireTime < Date.now()) { - authorize(token, this.config.wsId) - .then(() => { - req.session.lastAutedTime = Date.now(); - next(); - }) - .catch(() => res.send(403).end()); - } else { + if (!this.config.debug) { + this.app.use(['/stream', '/photo'], (req, res, next) => { + const { lastAutedTime, token } = req.session; + if (!token) { + res.sendStatus(401).end(); + } + if (!lastAutedTime || lastAutedTime + expireTime < Date.now()) { + authorize(token, this.config.wsId) + .then(() => { + req.session.lastAutedTime = Date.now(); + next(); + }) + .catch(() => res.send(403).end()); + } else { + next(); + } + }); + + this.app.use(['/stream', '/photo'], (req, res, next) => { + const { lastAutedTime } = req.session; + if (lastAutedTime > Date.now()) { + req.session = null; + res.sendStatus(403).end(); + } next(); - } - }); + }); + } + + this.app.use('/stream', express.static(this.mountPath)); - this.app.use('/stream', (req, res, next) => { - const { lastAutedTime } = req.session; - if (lastAutedTime > Date.now()) { - req.session = null; - res.status(403).end(); + this.app.get('/photo', (req, res) => { + let ffmpeg; + if (this.config.isMac) { + // Mac + ffmpeg = childProcess.spawn( + 'ffmpeg -f avfoundation -framerate 30 -i 0 -vframes 1 -f image2 pipe:1', + ); + } else { + // Ubuntu + ffmpeg = childProcess.spawn('ffmpeg -i /dev/video0 -vframes 1 -f image2 pipe:1'); } - next(); + + const ext = 'jpeg'; + + res.contentType(`image/${ext}`); + ffmpeg.stdout.pipe(res); }); - this.app.use('/stream', express.static(this.mountPath)); + // this.app.use('/photo', express.static(`${__dirname}/photos`)); } run(port = 3000) { diff --git a/stream.js b/stream.js index 274ddca..90afaa2 100644 --- a/stream.js +++ b/stream.js @@ -23,7 +23,7 @@ module.exports = class { this.mountPath = null; } - async run(size = 50) { + async run(size = 50, isMac = false) { this.mountPath = await new Promise((resolve, reject) => this.disk.create(size, (err, mount) => { if (err) { reject(err); @@ -32,6 +32,13 @@ module.exports = class { } })); console.log(`create ramdisk on ${this.mountPath}.`); + if (isMac) { + // Mac + // deamon(''); + } else { + // Ubuntu + // deamon(''); + } return this.mountPath; } diff --git a/views/photo-viewer.html b/views/photo-viewer.html new file mode 100644 index 0000000..481e8ec --- /dev/null +++ b/views/photo-viewer.html @@ -0,0 +1,33 @@ + + + + + Photo + + + このページのUIを担当してくれるデザイナー募集中 +
+ +
+ + + + + diff --git a/views/viewer.html b/views/viewer.html index c79b5f4..a04ad44 100644 --- a/views/viewer.html +++ b/views/viewer.html @@ -30,13 +30,13 @@ .then(res => { if (Hls.isSupported()) { var hls = new Hls(); - hls.loadSource(res.address); + hls.loadSource(res.hlsAddress); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, function() { video.play(); }); } else if (video.canPlayType("application/vnd.apple.mpegurl")) { - video.src = res.address; + video.src = res.hlsAddress; video.addEventListener("loadedmetadata", function() { video.play(); }); From fe8447fafc229f5f3d4ccef66bf33c459ee32dd4 Mon Sep 17 00:00:00 2001 From: ilim Date: Sun, 21 Jul 2019 17:20:12 +0900 Subject: [PATCH 03/11] RAM Disk: bugfix & support Ubuntu --- README.md | 13 +++++++++++++ app.js | 7 ++++--- package-lock.json | 5 +++++ package.json | 1 + stream.js | 27 +++++++++++++++++---------- 5 files changed, 40 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8bcf6f5..5c11240 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,19 @@ brew install ffmpeg sudo apt-get install ffmpeg ``` +### RAM Disk + +自動で行われますが,Macの場合 +Mac の場合は手動でやる場合以下のように実行してください。 +Ubuntu は自動かつ失敗しません。 + +```bash= +hdiutil attach -nomount ram://204800 +newfs_hfs /dev/disk2 +mkdir -p /path/to/bushitsuchan-PC/hls +mount -t hfs /dev/disk2 /path/to/bushitsuchan-PC/hls +``` + ## Run ```bash= diff --git a/app.js b/app.js index 80fe623..a025a52 100644 --- a/app.js +++ b/app.js @@ -13,15 +13,15 @@ const config = { slackClientSecret: process.env.SLACK_CLIENT_SECRET, wsId: process.env.WORKSTATION_ID, privateKey: process.env.LIVE_PRIVATE_KEY, - debug: false, - isMac: false, + debug: Boolean(process.env.DEBUG), + isMac: Boolean(process.env.IS_MAC), }; const liveServer = new RtmpServer(); liveServer.on(); liveServer.run(); -const disk = new Stream('bushitsuchan'); +const disk = new Stream('hls-ramdisk'); disk .run(config.isMac) .then(async (mountPath) => { @@ -47,6 +47,7 @@ disk server.run(); }) .catch((e) => { + disk.mountPath = disk.mountPath || '/tmp/hls-ramdisk'; disk.close(); console.error(e); }); diff --git a/package-lock.json b/package-lock.json index 6e919cf..4491e67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1030,6 +1030,11 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", diff --git a/package.json b/package.json index a9cc461..df4e79d 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "cookie-session": "^1.3.3", "cors": "^2.8.5", "express": "^4.17.1", + "fs": "0.0.1-security", "helmet": "^3.18.0", "morgan": "^1.9.1", "ngrok": "^3.2.1", diff --git a/stream.js b/stream.js index 90afaa2..b02dd58 100644 --- a/stream.js +++ b/stream.js @@ -1,6 +1,7 @@ const ramdisk = require('node-ramdisk'); const childProcess = require('child_process'); const util = require('util'); +const fs = require('fs'); const exec = util.promisify(childProcess.exec); @@ -18,12 +19,21 @@ const deamon = async (command) => { }; module.exports = class { - constructor(volume) { + constructor(volume, folder = 'bushitsuchan', isMac = false) { this.disk = ramdisk(volume); this.mountPath = null; + this.folder = folder; + this.isMac = isMac; } - async run(size = 50, isMac = false) { + async run(size = 50) { + if (!this.isMac) { + this.mountPath = '/dev/shm'; + fs.rmdirSync(`${this.mountPath}/${this.folder}`); + fs.mkdirSync(`${this.mountPath}/${this.folder}`, { recursive: true }); + // deamon(''); + return `${this.mountPath}/${this.folder}`; + } this.mountPath = await new Promise((resolve, reject) => this.disk.create(size, (err, mount) => { if (err) { reject(err); @@ -32,17 +42,14 @@ module.exports = class { } })); console.log(`create ramdisk on ${this.mountPath}.`); - if (isMac) { - // Mac - // deamon(''); - } else { - // Ubuntu - // deamon(''); - } - return this.mountPath; + // deamon(''); + return `${this.mountPath}/${this.folder}`; } async close() { + if (!this.isMac) { + return Promise.resolve(); + } return new Promise((resolve, reject) => { this.disk.delete(this.mountPath, (err) => { if (err) { From eda46e384a39a25c4dab1715dee6ccad45c29450 Mon Sep 17 00:00:00 2001 From: ilim Date: Sun, 21 Jul 2019 18:03:02 +0900 Subject: [PATCH 04/11] bugfix --- stream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream.js b/stream.js index b02dd58..9362025 100644 --- a/stream.js +++ b/stream.js @@ -29,7 +29,6 @@ module.exports = class { async run(size = 50) { if (!this.isMac) { this.mountPath = '/dev/shm'; - fs.rmdirSync(`${this.mountPath}/${this.folder}`); fs.mkdirSync(`${this.mountPath}/${this.folder}`, { recursive: true }); // deamon(''); return `${this.mountPath}/${this.folder}`; @@ -48,6 +47,7 @@ module.exports = class { async close() { if (!this.isMac) { + fs.rmdirSync(`${this.mountPath}/${this.folder}`); return Promise.resolve(); } return new Promise((resolve, reject) => { From c7db137d25ab2b8cf39af8a6f79f560155a90fed Mon Sep 17 00:00:00 2001 From: ilim Date: Sun, 21 Jul 2019 18:43:11 +0900 Subject: [PATCH 05/11] Manually create RAM disk on Mac --- README.md | 12 ++++++-- app.js | 17 ++++++++--- ngrok.js | 2 -- package-lock.json | 18 ----------- package.json | 1 - rtmp-server.js | 4 +-- stream.js | 76 +++++++++++++++++------------------------------ 7 files changed, 52 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 5c11240..13d2b36 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,13 @@ OSK の部室の様子を様子をオンラインで確認できるプロジェクト 部室ちゃん その部室に置いてある PC 側で動かすプログラム +# サポート + +以下の OS をサポートします + +- Ubuntu 18.04 +- macOS 10.14 + ## Setup ### ngrok @@ -110,9 +117,8 @@ sudo apt-get install ffmpeg ### RAM Disk -自動で行われますが,Macの場合 -Mac の場合は手動でやる場合以下のように実行してください。 -Ubuntu は自動かつ失敗しません。 +Ubuntu では自動で行われますが,Mac の場合 OS 起動の度に手動で行う必要があります。 +以下のように実行してください。 ```bash= hdiutil attach -nomount ram://204800 diff --git a/app.js b/app.js index a025a52..cc251b7 100644 --- a/app.js +++ b/app.js @@ -9,6 +9,7 @@ const config = { viewerResourceId: process.env.VIEWER_RESOURCE_ID, oauthResourceId: process.env.OAUTH_RESOURCE_ID, httpMethod: 'GET', + ngrokToken: process.env.NGROK_TOKEN, slackClientId: process.env.SLACK_CLIENT_ID, slackClientSecret: process.env.SLACK_CLIENT_SECRET, wsId: process.env.WORKSTATION_ID, @@ -21,10 +22,17 @@ const liveServer = new RtmpServer(); liveServer.on(); liveServer.run(); -const disk = new Stream('hls-ramdisk'); +const disk = new Stream( + config.isMac ? `${__dirname}/hls` : '/dev/shm', + 'bushitsuchan', + config.isMac, +); +console.log(`Regarding directory ${disk.mountPath} as RAM DISK`); + disk .run(config.isMac) .then(async (mountPath) => { + console.log(`Please put HLS files in ${mountPath}`); let ngrokUrl; let awsUrl; @@ -32,7 +40,9 @@ disk ngrokUrl = null; awsUrl = null; } else { - ngrokUrl = await ngrok.run(process.env.NGROK_TOKEN); + ngrokUrl = await ngrok.run(config.ngrokToken, 3000); + console.log(`Forwarding ${ngrokUrl} -> localhost:${3000}`); + awsUrl = await aws.run(config, ngrokUrl); console.log(`Remote URL: ${awsUrl}`); } @@ -47,7 +57,6 @@ disk server.run(); }) .catch((e) => { - disk.mountPath = disk.mountPath || '/tmp/hls-ramdisk'; - disk.close(); console.error(e); + disk.close(); }); diff --git a/ngrok.js b/ngrok.js index 97e60ab..8d02642 100644 --- a/ngrok.js +++ b/ngrok.js @@ -3,7 +3,5 @@ const ngrok = require('ngrok'); module.exports.run = async (token, port = 3000) => { await ngrok.authtoken(token); const url = await ngrok.connect(port); - - console.log(`Forwarding ${url} -> localhost:${port}`); return url; }; diff --git a/package-lock.json b/package-lock.json index 4491e67..1c46172 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1625,24 +1625,6 @@ "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz", "integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==" }, - "node-ramdisk": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/node-ramdisk/-/node-ramdisk-1.2.2.tgz", - "integrity": "sha1-XRKmCoBVChy2eeNyBEATJR9htcI=", - "requires": { - "debug": "^2.2.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - } - } - }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", diff --git a/package.json b/package.json index df4e79d..6c5e4fd 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "helmet": "^3.18.0", "morgan": "^1.9.1", "ngrok": "^3.2.1", - "node-ramdisk": "^1.2.2", "rtmp-server": "^0.2.0", "util": "^0.12.1" }, diff --git a/rtmp-server.js b/rtmp-server.js index 3838cc0..b4d9cc4 100644 --- a/rtmp-server.js +++ b/rtmp-server.js @@ -33,7 +33,7 @@ module.exports = class { }); } - run(port = 1935) { - this.rtmpServer.listen(port); + async run(port = 1935) { + await new Promise(resolve => this.rtmpServer.listen(port, () => resolve())); } }; diff --git a/stream.js b/stream.js index 9362025..511580c 100644 --- a/stream.js +++ b/stream.js @@ -1,63 +1,43 @@ -const ramdisk = require('node-ramdisk'); -const childProcess = require('child_process'); -const util = require('util'); const fs = require('fs'); +// const childProcess = require('child_process'); +// const util = require('util'); -const exec = util.promisify(childProcess.exec); +// const exec = util.promisify(childProcess.exec); -const deamon = async (command) => { - console.log(`start command ${command.slice(0, 20)}...`); - // eslint-disable-next-line no-constant-condition - while (true) { - try { - // eslint-disable-next-line no-await-in-loop - await exec(command); - } catch (e) { - console.error(e); - } - } -}; +// const deamon = async (command) => { +// console.log(`start command ${command.slice(0, 20)}...`); +// // eslint-disable-next-line no-constant-condition +// while (true) { +// try { +// // eslint-disable-next-line no-await-in-loop +// await exec(command); +// } catch (e) { +// console.error(e); +// } +// } +// }; module.exports = class { - constructor(volume, folder = 'bushitsuchan', isMac = false) { - this.disk = ramdisk(volume); - this.mountPath = null; + constructor(mountPath, folder = 'bushitsuchan') { + this.mountPath = mountPath; this.folder = folder; - this.isMac = isMac; } - async run(size = 50) { - if (!this.isMac) { - this.mountPath = '/dev/shm'; - fs.mkdirSync(`${this.mountPath}/${this.folder}`, { recursive: true }); - // deamon(''); - return `${this.mountPath}/${this.folder}`; - } - this.mountPath = await new Promise((resolve, reject) => this.disk.create(size, (err, mount) => { - if (err) { - reject(err); - } else { - resolve(mount); - } - })); - console.log(`create ramdisk on ${this.mountPath}.`); + async run() { + await new Promise((resolve, reject) => { + fs.mkdir(`${this.mountPath}/${this.folder}`, (err) => { + if (err && err.code !== 'EEXIST') { + reject(err); + return; + } + resolve(); + }); + }); // deamon(''); return `${this.mountPath}/${this.folder}`; } async close() { - if (!this.isMac) { - fs.rmdirSync(`${this.mountPath}/${this.folder}`); - return Promise.resolve(); - } - return new Promise((resolve, reject) => { - this.disk.delete(this.mountPath, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); + return fs.rmdir(`${this.mountPath}/${this.folder}`); } }; From 7b8c1d9ad7e0b8034b1a0ef8605378d146061a02 Mon Sep 17 00:00:00 2001 From: ilim Date: Sun, 21 Jul 2019 18:47:38 +0900 Subject: [PATCH 06/11] Update README --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 13d2b36..5f7ba16 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ OSK の部室の様子を様子をオンラインで確認できるプロジェクト 部室ちゃん その部室に置いてある PC 側で動かすプログラム -# サポート +# Support 以下の OS をサポートします @@ -50,6 +50,10 @@ ngrok で得られる URL はは変動するので,[API Gateway](https://aws.a │ └─ GET ├─ /oauth-redirect │ └─ GET + ├─ /photo + │ └─ GET + ├─ /photo-viewer + │ └─ GET ├─ /stream │ ├─ GET │ └ /{file+} From dd9dda6788444626a41b4893ee4ee50b3fcc2069 Mon Sep 17 00:00:00 2001 From: ilim Date: Sun, 21 Jul 2019 22:38:06 +0900 Subject: [PATCH 07/11] update rtmp server --- README.md | 45 ++++++++++++++------------- app.js | 9 +++--- package-lock.json | 54 ++++++++++++++++++++++++-------- package.json | 1 + rtmp-server.js | 46 +++++++++------------------ server.js | 69 ++++++++++++++++++++++------------------- stream.js | 2 +- views/photo-viewer.html | 1 + views/viewer.html | 3 +- 9 files changed, 124 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 5f7ba16..138feb9 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,11 @@ ngrok で得られる URL はは変動するので,[API Gateway](https://aws.a │ └─ GET ├─ /login │ └─ GET + ├─ /logout + │ └─ GET ├─ /oauth-redirect │ └─ GET - ├─ /photo + ├─ /photo.jpeg │ └─ GET ├─ /photo-viewer │ └─ GET @@ -96,7 +98,7 @@ export AWS_REST_API_ID="h7c..." export SLACK_CLIENT_ID="179..." export SLACK_CLIENT_SECRET="38b..." -export LIVE_PRIVATE_KEY="presetprivatekey" +export PRIVATE_KEY="presetprivatekey" export WORKSTATION_ID="VOW38CP2D" ``` @@ -137,38 +139,37 @@ mount -t hfs /dev/disk2 /path/to/bushitsuchan-PC/hls npm start ``` -### Streaming +## Streaming + +以下のコマンドを実行してください。 -まず`rtmp://localhost:1935/live` に向けストリームキー`bushitsuchan`をつけ OBS 等でストリーミングします。 +### Step.1 Streaming to RTMP server -その後以下のコマンドを実行します **Use USB Camera on Ubuntu** ```bash= ffmpeg \ - -i rtmp://localhost:1935/live/bushitsuchan \ - -vcodec libx264 \ - -preset veryfast \ - -tune zerolatency \ - -b 8M \ - -vf "drawtext=fontfile=/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf: \ - text='%{localtime\:%T}': fontcolor=white@0.8: x=7: y=700" \ - -hls_flags delete_segments \ - -g 20 \ - -f hls [Directory of ramdisk]/output.m3u8 + -i /dev/video0 \ + -vf "drawtext=text='%{localtime\:%T}': fontcolor=white@0.8: x=7: y=700" \ + -f flv rtmp://localhost:1935/live/bushitsuchan ``` **Use USB Camera on macOS** ```bash= ffmpeg \ - -re -i rtmp://localhost:1935/live/bushitsuchan \ - -r 5 \ - -vcodec libx264 \ - -preset veryfast \ - -tune zerolatency \ - -vf "drawtext=fontfile=/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf: \ - text='%{localtime\:%T}': fontcolor=white@0.8: x=7: y=700" \ + -f avfoundation \ + -framerate 30 \ + -i 0 \ + -vf "drawtext=text='%{localtime\:%T}': fontcolor=white@0.8: x=7: y=700" \ + -f flv rtmp://localhost:1935/live/bushitsuchan +``` + +### Step.2 Conversioning into HLS + +```bash= +ffmpeg \ + -i rtmp://localhost:1935/live/bushitsuchan \ -hls_flags delete_segments \ -g 20 \ -f hls [Directory of ramdisk]/output.m3u8 diff --git a/app.js b/app.js index cc251b7..32a5cad 100644 --- a/app.js +++ b/app.js @@ -13,13 +13,12 @@ const config = { slackClientId: process.env.SLACK_CLIENT_ID, slackClientSecret: process.env.SLACK_CLIENT_SECRET, wsId: process.env.WORKSTATION_ID, - privateKey: process.env.LIVE_PRIVATE_KEY, + privateKey: process.env.PRIVATE_KEY, debug: Boolean(process.env.DEBUG), isMac: Boolean(process.env.IS_MAC), }; -const liveServer = new RtmpServer(); -liveServer.on(); +const liveServer = new RtmpServer(1935); liveServer.run(); const disk = new Stream( @@ -52,9 +51,9 @@ disk awsUrl, mountPath, config, - 'rtmp://localhost:1935/live/bushitsuchan', + `rtmp://localhost:1935/live/${'bushitsuchan'}`, ); - server.run(); + server.run(3000).then(() => console.log(`Express app listening on port ${3000}`)); }) .catch((e) => { console.error(e); diff --git a/package-lock.json b/package-lock.json index 1c46172..fce73ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,7 +82,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -130,6 +129,11 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -168,6 +172,11 @@ "safe-buffer": "5.1.2" } }, + "basic-auth-connect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz", + "integrity": "sha1-/bC0OWLKe0BFanwrtI/hc9otISI=" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -260,7 +269,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -297,7 +305,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -305,8 +312,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "combined-stream": { "version": "1.0.8", @@ -431,6 +437,11 @@ "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=" }, + "dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" + }, "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", @@ -566,8 +577,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { "version": "5.16.0", @@ -1116,8 +1126,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.0", @@ -1541,14 +1550,12 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" } @@ -1625,6 +1632,20 @@ "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz", "integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==" }, + "node-media-server": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/node-media-server/-/node-media-server-2.1.2.tgz", + "integrity": "sha512-r3X0rXHxPO5e2krZFew1fARWuWuwQaunzQm25b6/081fTeW/WXvMTThQ3t3YJ3C9RpPbt1ys02LzHvFRfgjVZw==", + "requires": { + "basic-auth-connect": "^1.0.0", + "chalk": "^2.4.2", + "dateformat": "^3.0.3", + "express": "^4.16.4", + "lodash": "^4.17.11", + "mkdirp": "^0.5.1", + "ws": "^5.2.2" + } + }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", @@ -2284,7 +2305,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -2521,6 +2541,14 @@ "mkdirp": "^0.5.1" } }, + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "requires": { + "async-limiter": "~1.0.0" + } + }, "x-xss-protection": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.1.0.tgz", diff --git a/package.json b/package.json index 6c5e4fd..a49b6d0 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "helmet": "^3.18.0", "morgan": "^1.9.1", "ngrok": "^3.2.1", + "node-media-server": "^2.1.2", "rtmp-server": "^0.2.0", "util": "^0.12.1" }, diff --git a/rtmp-server.js b/rtmp-server.js index b4d9cc4..56bb728 100644 --- a/rtmp-server.js +++ b/rtmp-server.js @@ -1,39 +1,21 @@ -const RtmpServer = require('rtmp-server'); +const NodeMediaServer = require('node-media-server'); module.exports = class { - constructor() { - this.rtmpServer = new RtmpServer(); - } - - on() { - this.rtmpServer.on('error', (err) => { - throw err; - }); - - this.rtmpServer.on('client', (client) => { - // client.on('command', command => { - // console.log(command.cmd, command); - // }); - - client.on('connect', () => { - console.log('connect', client.app); - }); - - client.on('play', ({ streamName }) => { - console.log('PLAY', streamName); - }); - - client.on('publish', ({ streamName }) => { - console.log('PUBLISH', streamName); - }); - - client.on('stop', () => { - console.log('client disconnected'); - }); + constructor(port = 1935) { + this.nms = new NodeMediaServer({ + logType: 1, + + rtmp: { + port, + chunk_size: 60000, + gop_cache: true, + ping: 30, + ping_timeout: 60, + }, }); } - async run(port = 1935) { - await new Promise(resolve => this.rtmpServer.listen(port, () => resolve())); + run() { + this.nms.run(); } }; diff --git a/server.js b/server.js index 032ac28..7ea638d 100644 --- a/server.js +++ b/server.js @@ -58,20 +58,23 @@ const authorize = async (token, workstationId) => { }; module.exports = class { - constructor(ngrokUrl, awsUrl, mountPath, config) { + constructor(ngrokUrl, awsUrl, mountPath, config, rtmpAddress) { this.ngrokUrl = ngrokUrl; this.awsUrl = awsUrl; this.mountPath = mountPath; this.config = config; + this.rtmpAddress = rtmpAddress; this.app = express(); this.app.set('trust proxy', 1); this.app.use(helmet()); - this.app.use(morgan('short')); + this.app.use( + morgan('common', { + skip: (req, res) => ['.ts', '.m3u8', '.jpg'].some(element => req.path.endsWith(element)), + }), + ); this.app.use(cors()); this.routing(); - - this.wCap = null; } routing() { @@ -118,21 +121,24 @@ module.exports = class { }); if (!this.config.debug) { - this.app.use(['/auth', '/viewer', '/photo-viewer', '/stream', '/photo'], (req, res, next) => { - const { token } = req.session; - if (token) { - next(); - return; - } - res.redirect('login'); - }); + this.app.use( + ['/auth', '/viewer', '/photo-viewer', '/stream', '/photo.jpg'], + (req, res, next) => { + const { token } = req.session; + if (token) { + next(); + return; + } + res.redirect('login'); + }, + ); } this.app.get('/auth', (req, res) => { const { token } = req.session; if (!this.config.debug) { res.json({ hlsAddress: 'stream/output.m3u8', - photoAddress: 'photo', + photoAddress: 'photo.jpg', }); return; } @@ -141,7 +147,7 @@ module.exports = class { req.session.lastAutedTime = Date.now(); res.json({ hlsAddress: 'stream/output.m3u8', - photoAddress: 'photo', + photoAddress: 'photo.jpg', }); }) .catch((e) => { @@ -160,7 +166,7 @@ module.exports = class { const expireTime = 60 * 1000; if (!this.config.debug) { - this.app.use(['/stream', '/photo'], (req, res, next) => { + this.app.use(['/stream', '/photo.jpg'], (req, res, next) => { const { lastAutedTime, token } = req.session; if (!token) { res.sendStatus(401).end(); @@ -177,7 +183,7 @@ module.exports = class { } }); - this.app.use(['/stream', '/photo'], (req, res, next) => { + this.app.use(['/stream', '/photo.jpg'], (req, res, next) => { const { lastAutedTime } = req.session; if (lastAutedTime > Date.now()) { req.session = null; @@ -189,28 +195,27 @@ module.exports = class { this.app.use('/stream', express.static(this.mountPath)); - this.app.get('/photo', (req, res) => { - let ffmpeg; - if (this.config.isMac) { - // Mac - ffmpeg = childProcess.spawn( - 'ffmpeg -f avfoundation -framerate 30 -i 0 -vframes 1 -f image2 pipe:1', - ); - } else { - // Ubuntu - ffmpeg = childProcess.spawn('ffmpeg -i /dev/video0 -vframes 1 -f image2 pipe:1'); - } - + this.app.get('/photo.jpg', async (req, res) => { const ext = 'jpeg'; + const ffmpeg = childProcess.spawn('ffmpeg', [ + '-i', + `${this.rtmpAddress}`, + '-ss', + '0.7', + '-vframes', + '1', + '-f', + 'image2', + 'pipe:1', + ]); + res.contentType(`image/${ext}`); ffmpeg.stdout.pipe(res); }); - - // this.app.use('/photo', express.static(`${__dirname}/photos`)); } - run(port = 3000) { - return new Promise(resolve => this.app.listen(port, () => resolve(port))); + async run(port = 3000) { + await new Promise(resolve => this.app.listen(port, () => resolve())); } }; diff --git a/stream.js b/stream.js index 511580c..b156c54 100644 --- a/stream.js +++ b/stream.js @@ -38,6 +38,6 @@ module.exports = class { } async close() { - return fs.rmdir(`${this.mountPath}/${this.folder}`); + return fs.rmdirSync(`${this.mountPath}/${this.folder}`); } }; diff --git a/views/photo-viewer.html b/views/photo-viewer.html index 481e8ec..0215caa 100644 --- a/views/photo-viewer.html +++ b/views/photo-viewer.html @@ -10,6 +10,7 @@ + 動画版 diff --git a/views/viewer.html b/views/viewer.html index a04ad44..67fdab8 100644 --- a/views/viewer.html +++ b/views/viewer.html @@ -12,12 +12,13 @@ + 画像版