diff --git a/.gitignore b/.gitignore index 42d866a..0307a32 100644 --- a/.gitignore +++ b/.gitignore @@ -97,4 +97,5 @@ typings/ .envrc *.png *.jpeg -*.jpg \ No newline at end of file +*.jpg +hls/ \ No newline at end of file diff --git a/README.md b/README.md index b7359fe..e464ac0 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,13 @@ OSK の部室の様子を様子をオンラインで確認できるプロジェクト 部室ちゃん その部室に置いてある PC 側で動かすプログラム +## Support + +以下の OS をサポートします + +- Ubuntu 18.04 +- macOS 10.14 + ## Setup ### ngrok @@ -41,8 +48,14 @@ ngrok で得られる URL はは変動するので,[API Gateway](https://aws.a │ └─ GET ├─ /login │ └─ GET + ├─ /logout + │ └─ GET ├─ /oauth-redirect │ └─ GET + ├─ /photo.jpeg + │ └─ GET + ├─ /photo-viewer + │ └─ GET ├─ /stream │ ├─ GET │ └ /{file+} @@ -85,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" ``` @@ -108,60 +121,22 @@ brew install ffmpeg sudo apt-get install ffmpeg ``` -## Run - -```bash= -npm start -``` +### RAM Disk -### Streaming - -Linux の場合は自動で行われますが,それ以外のときエラーが発生します。 -その時の手動実行方法を書き置きます。 - -rtmp に向けストリーミングします。 -OBS などでも行えますがここでは ffmpeg の例を書きます。 - -**Use Video files** - -```bash= -ffmpeg -re -i example.mp4 -c copy -f flv rtmp://localhost/live/stream -``` - -**Use USB Camera on Ubuntu** +Ubuntu では自動で行われますが,Mac の場合 OS 起動の度に手動で行う必要があります。 +以下のように実行してください。 ```bash= -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 [Directory of ramdisk]/output.m3u8 +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 ``` -**Use USB Camera on macOS** +## Run ```bash= -ffmpeg \ - -f avfoundation \ - -framerate 30 \ - -re -i 0 \ - -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" \ - -hls_flags delete_segments \ - -g 20 \ - -f hls [Directory of ramdisk]/output.m3u8 +npm start ``` ## Usage @@ -184,5 +159,3 @@ Remote URL: https://[AWS_REST_API_ID].execute-api.[REGION].amazonaws.com/prod すると,初回実行時(過去に Slack で認証をしていなければ) Slack の認証ページへリダイレクトされます。 Sign in すると自動的に配信再生ページへ移動します。 - -> 再生が開始されないことがあるので,静止画で止まったままのときはサイトをリロードしてください。 diff --git a/app.js b/app.js index 31a9bfd..1308732 100644 --- a/app.js +++ b/app.js @@ -1,31 +1,77 @@ +const RtmpServer = require('./rtmp-server'); const ngrok = require('./ngrok'); const Server = require('./server'); const aws = require('./aws'); const Stream = require('./stream'); +const { daemon } = require('./utils'); const config = { restApiId: process.env.AWS_REST_API_ID, 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, - privateKey: process.env.LIVE_PRIVATE_KEY, + privateKey: process.env.PRIVATE_KEY, + debug: Boolean(process.env.DEBUG), + isMac: Boolean(process.env.IS_MAC), }; -const disk = new Stream('bushitsuchan'); +const liveServer = new RtmpServer(1935); +liveServer.run(); +console.log(`RTMP server listening on port ${1935}`); + +const disk = new Stream( + config.isMac ? `${__dirname}/hls` : '/dev/shm', + 'bushitsuchan', + config.isMac, +); +console.log(`Regarding directory ${disk.mountPath} as RAM DISK`); + 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}`); + console.log(`Please put HLS files in ${mountPath}`); + let input; + if (config.isMac) { + input = 'ffmpeg -f avfoundation -framerate 30 -re -i 0 -r 10'; + } else { + input = 'ffmpeg -i /dev/video0'; + } + daemon( + `${input} -vcodec libx264 -pix_fmt yuv420p -preset veryfast -tune zerolatency,stillimage,film -vb 2500k -vf "drawtext=text='%{localtime}':fontcolor=white@0.8:x=0:y=h-lh*1.2:fontsize=24" -f flv rtmp://localhost:${1935}/live/bushitsuchan`, + ); + + daemon( + `ffmpeg -i rtmp://localhost:1935/live/bushitsuchan -hls_flags delete_segments -codec:v copy -g 40 -f hls ${mountPath}/output.m3u8`, + ); + + let ngrokUrl; + let awsUrl; - const server = new Server(ngrokUrl, awsUrl, mountPath, config); - server.run(); + if (config.debug) { + ngrokUrl = null; + awsUrl = null; + } else { + ngrokUrl = await ngrok.run(config.ngrokToken, 3000); + console.log(`Forwarding ${ngrokUrl} -> localhost:${3000}`); + + 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(3000).then(() => console.log(`Express app listening on port ${3000}`)); }) .catch((e) => { - 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 3fba473..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", @@ -1030,6 +1040,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", @@ -1111,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", @@ -1536,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" } @@ -1620,22 +1632,18 @@ "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=", + "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": { - "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" - } - } + "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": { @@ -2064,6 +2072,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", @@ -2292,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" } @@ -2529,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 b2227e1..a49b6d0 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,12 @@ "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", - "node-ramdisk": "^1.2.2", + "node-media-server": "^2.1.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..56bb728 --- /dev/null +++ b/rtmp-server.js @@ -0,0 +1,21 @@ +const NodeMediaServer = require('node-media-server'); + +module.exports = class { + constructor(port = 1935) { + this.nms = new NodeMediaServer({ + logType: 1, + + rtmp: { + port, + chunk_size: 60000, + gop_cache: true, + ping: 30, + ping_timeout: 60, + }, + }); + } + + run() { + this.nms.run(); + } +}; diff --git a/server.js b/server.js index 384aa34..3c8f9fe 100644 --- a/server.js +++ b/server.js @@ -5,6 +5,8 @@ const cors = require('cors'); const morgan = require('morgan'); const cookieSession = require('cookie-session'); const helmet = require('helmet'); +const childProcess = require('child_process'); +const fs = require('fs'); const getToken = async (code, clientId, clientSecret) => { if (code === undefined) { @@ -54,24 +56,21 @@ const authorize = async (token, workstationId) => { if (result.data.team.id !== workstationId) { throw new Error(); } + return result.data.user.name; }; 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(cors()); - this.routing(); - } - - routing() { this.app.use( cookieSession({ secret: this.config.privateKey, @@ -81,6 +80,27 @@ module.exports = class { }), ); + morgan.token('user', (req, res) => req.session.name || 'anonymous'); + this.app.use( + morgan( + '<@:user> [:date[clf]] :method :url :status :res[content-length] - :response-time ms', + { + skip: (req, res) => ['.ts', '.m3u8', '.jpg'].some(element => req.path.endsWith(element)), + }, + ), + ); + this.app.use( + morgan( + ':remote-addr - :remote-user <@:user> [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"', + { + stream: fs.createWriteStream(`${__dirname}/access.log`, { flags: 'a' }), + }, + ), + ); + this.routing(); + } + + routing() { this.app.get('/', (req, res) => res.send('Hello Bushitsuchan!')); this.app.get('/login', (req, res) => { @@ -114,69 +134,104 @@ 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.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 (token) { - next(); + if (!this.config.debug) { + res.json({ + hlsAddress: 'stream/output.m3u8', + photoAddress: 'photo.jpg', + }); return; } - res.redirect('login'); - }); - - this.app.get('/auth', (req, res) => { - const { token } = req.session; authorize(token, this.config.wsId) - .then(() => { + .then((name) => { + req.session.name = name; req.session.lastAutedTime = Date.now(); res.json({ - address: 'stream/output.m3u8', + hlsAddress: 'stream/output.m3u8', + photoAddress: 'photo.jpg', }); }) .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.jpg'], (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((name) => { + req.session.name = name; + req.session.lastAutedTime = Date.now(); + next(); + }) + .catch(() => res.send(403).end()); + } else { + next(); + } + }); + + this.app.use(['/stream', '/photo.jpg'], (req, res, next) => { + const { lastAutedTime } = req.session; + if (lastAutedTime > Date.now()) { + req.session = null; + res.sendStatus(403).end(); + } next(); - } - }); - - this.app.use('/stream', (req, res, next) => { - const { lastAutedTime } = req.session; - if (lastAutedTime > Date.now()) { - req.session = null; - res.status(403).end(); - } - next(); - }); + }); + } this.app.use('/stream', express.static(this.mountPath)); + + 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); + }); } - 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 f822c5a..dd0ca88 100644 --- a/stream.js +++ b/stream.js @@ -1,52 +1,27 @@ -const ramdisk = require('node-ramdisk'); -const childProcess = require('child_process'); -const util = require('util'); +const fs = require('fs'); -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); - } - } -}; module.exports = class { - constructor(volume) { - this.disk = ramdisk(volume); - this.mountPath = null; - } - - async run(size = 50) { - 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}.`); - 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; + constructor(mountPath, folder = 'bushitsuchan') { + this.mountPath = mountPath; + this.folder = folder; } - async close() { - return new Promise((resolve, reject) => { - this.disk.delete(this.mountPath, (err) => { - if (err) { + async run() { + await new Promise((resolve, reject) => { + fs.mkdir(`${this.mountPath}/${this.folder}`, (err) => { + if (err && err.code !== 'EEXIST') { reject(err); - } else { - resolve(); + return; } + resolve(); }); }); + // deamon(''); + return `${this.mountPath}/${this.folder}`; + } + + async close() { + return fs.rmdirSync(`${this.mountPath}/${this.folder}`); } }; diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..58ad52b --- /dev/null +++ b/utils.js @@ -0,0 +1,20 @@ +const childProcess = require('child_process'); +const util = require('util'); + +const exec = util.promisify(childProcess.exec); + +const wait = ms => new Promise(reolve => setTimeout(() => reolve(), ms)); + +module.exports.daemon = async (command, ms = 1000 * 10, maxCount = 100) => { + console.log(`start daemon ${command.slice(0, 40)}...`); + for (let i = 0; i < maxCount; i += 1) { + try { + // eslint-disable-next-line no-await-in-loop + await exec(command); + } catch (e) { + console.error(e); + // eslint-disable-next-line no-await-in-loop + await wait(ms); + } + } +}; diff --git a/views/photo-viewer.html b/views/photo-viewer.html new file mode 100644 index 0000000..0215caa --- /dev/null +++ b/views/photo-viewer.html @@ -0,0 +1,34 @@ + + + + + Photo + + + このページのUIを担当してくれるデザイナー募集中 +
+ + 動画版 +
+ + + + + diff --git a/views/viewer.html b/views/viewer.html index c79b5f4..67fdab8 100644 --- a/views/viewer.html +++ b/views/viewer.html @@ -12,12 +12,13 @@ + 画像版