diff --git a/install/satyr.service b/install/satyr.service index c5df3cc..8e535f8 100644 --- a/install/satyr.service +++ b/install/satyr.service @@ -1,5 +1,5 @@ [Unit] -Description=A livestreaming server. +Description=satyr livestreaming server After=network.target [Service] diff --git a/package.json b/package.json index 6e2ca0b..221094e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "satyr", - "version": "0.4.3", + "version": "0.4.4", "description": "A livestreaming server.", "license": "AGPL-3.0", "author": "knotteye", diff --git a/src/http.ts b/src/http.ts index 65e9f52..2d2b5ff 100644 --- a/src/http.ts +++ b/src/http.ts @@ -39,37 +39,27 @@ async function init(satyr: any, port: number, ircconf: any){ }); app.get('/users', (req, res) => { db.query('select username from users').then((result) => { - njkconf.list = result; - res.render('list.njk', njkconf); - njkconf.list = ''; + res.render('list.njk', Object.assign({list: result}, njkconf)); }); }); app.get('/users/live', (req, res) => { db.query('select username,title from user_meta where live=1;').then((result) => { - njkconf.list = result; - res.render('live.njk', njkconf); - njkconf.list = ''; + res.render('live.njk', Object.assign({list: result}, njkconf)); }); }); app.get('/users/*', (req, res) => { db.query('select username,title,about from user_meta where username='+db.raw.escape(req.url.split('/')[2].toLowerCase())).then((result) => { if(result[0]){ - njkconf.user = result[0].username; - njkconf.streamtitle = result[0].title; - njkconf.about = result[0].about; - res.render('user.njk', njkconf); + res.render('user.njk', Object.assign(result[0], njkconf)); } else res.render('404.njk', njkconf); }); }); app.get('/vods/*', (req, res) => { - njkconf.user = req.url.split('/')[2].toLowerCase(); - db.query('select username from user_meta where username='+db.raw.escape(njkconf.user)).then((result) => { + db.query('select username from user_meta where username='+db.raw.escape(req.url.split('/')[2].toLowerCase())).then((result) => { if(result[0]){ fs.readdir('./site/live/'+njkconf.user, {withFileTypes: true} , (err, files) => { - if(files) njkconf.list = files.filter(fn => fn.name.endsWith('.mp4')); - else njkconf.list = []; - res.render('vods.njk', njkconf); + res.render('vods.njk', Object.assign({user: result[0].username, list: files.filter(fn => fn.name.endsWith('.mp4'))}, njkconf)); }); } else res.render('404.njk', njkconf); @@ -182,7 +172,7 @@ async function init(satyr: any, port: number, ircconf: any){ } }); socket.on('MSG', (data) => { - if(data.msg === "") return; + if(data.msg === "" || !data.msg.replace(/\s/g, '').length) return; io.to(data.room).emit('MSG', {nick: socket.nick, msg: data.msg}); if(ircconf.enable) irc.send(socket.nick, data.room, data.msg); }); @@ -204,7 +194,8 @@ async function init(satyr: any, port: number, ircconf: any){ } async function newNick(socket) { - let n: string = 'Guest'+Math.floor(Math.random() * Math.floor(100)); + //i just realized how shitty of an idea this is + let n: string = 'Guest'+Math.floor(Math.random() * Math.floor(1000)); if(store.get(n)) return newNick(socket); else { store.set(n, socket.id); diff --git a/src/server.ts b/src/server.ts index 90fdf88..a52d807 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,10 +1,13 @@ import * as NodeMediaServer from "node-media-server"; +import * as dirty from "dirty"; import { mkdir, fstat, access } from "fs"; import * as strf from "strftime"; import * as db from "./database"; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); const { exec, execFile } = require('child_process'); +const keystore = dirty(); + function init (mediaconfig: any, satyrconfig: any) { const nms = new NodeMediaServer(mediaconfig); nms.run(); @@ -15,31 +18,39 @@ function init (mediaconfig: any, satyrconfig: any) { let app: string = StreamPath.split("/")[1]; let key: string = StreamPath.split("/")[2]; //disallow urls not formatted exactly right - if (StreamPath.split("/").length !== 3){ + if (StreamPath.split("/").length !== 3 || key.includes(' ')){ console.log("[NodeMediaServer] Malformed URL, closing connection for stream:",id); session.reject(); return false; } - if(app === satyrconfig.publicEndpoint) { - if(session.isLocal) { - //only allow publish to public endpoint from localhost - console.log("[NodeMediaServer] Local publish, stream:",`${id} ok.`); - } - else{ - console.log("[NodeMediaServer] Non-local Publish to public endpoint, rejecting stream:",id); - session.reject(); - return false; - } - console.log("[NodeMediaServer] Public endpoint, checking record flag."); - //set live flag - db.query('update user_meta set live=true where username=\''+key+'\' limit 1'); - //if this stream is from the public endpoint, check if we should be recording - return db.query('select username,record_flag from users where username=\''+key+'\' limit 1').then((results) => { + if(app !== satyrconfig.privateEndpoint){ + //app isn't at public endpoint if we've reached this point + console.log("[NodeMediaServer] Wrong endpoint, rejecting stream:",id); + session.reject(); + return false; + } + //if the url is formatted correctly and the user is streaming to the correct private endpoint + //grab the username from the database and redirect the stream there if the key is valid + //otherwise kill the session + db.query('select username,record_flag from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => { + if(results[0]){ + //push to rtmp + //execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-analyzeduration', '0', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', '-crf', '18', '-f', 'flv', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.publicEndpoint+'/'+results[0].username], {maxBuffer: Infinity}); + //push to mpd after making sure directory exists + keystore[results[0].username] = key; + mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, ()=>{;}); + while(true){ + if(session.audioCodec !== 0 && session.videoCodec !== 0){ + execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-y', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-map', '0:2', '-map', '0:2', '-map', '0:2', '-map', '0:1', '-c:a', 'copy', '-c:v:0', 'copy', '-c:v:1', 'libx264', '-c:v:2', 'libx264', '-crf:1', '33', '-crf:2', '40', '-b:v:1', '3000K', '-b:v:2', '1500K', '-remove_at_exit', '1', '-seg_duration', '1', '-window_size', '30', '-f', 'dash', satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/index.mpd'], {maxBuffer: Infinity}); + break; + } + await sleep(300); + } if(results[0].record_flag && satyrconfig.record){ console.log('[NodeMediaServer] Initiating recording for stream:',id); mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, (err) => { if (err) throw err; - execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, '-vcodec', 'copy', '-acodec', 'copy', satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/'+strf('%d%b%Y-%H%M')+'.mp4'], { + execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.prviateEndpoint+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/'+strf('%d%b%Y-%H%M')+'.mp4'], { detached : true, stdio : 'inherit', maxBuffer: Infinity @@ -51,36 +62,8 @@ function init (mediaconfig: any, satyrconfig: any) { else { console.log('[NodeMediaServer] Skipping recording for stream:',id); } - return true; - }); - } - if(app !== satyrconfig.privateEndpoint){ - //app isn't at public endpoint if we've reached this point - console.log("[NodeMediaServer] Wrong endpoint, rejecting stream:",id); - session.reject(); - return false; - } - //if the url is formatted correctly and the user is streaming to the correct private endpoint - //grab the username from the database and redirect the stream there if the key is valid - //otherwise kill the session - if(key.includes(' ')) { - session.reject(); - return false; - } - db.query('select username from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => { - if(results[0]){ - //push to rtmp - execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-analyzeduration', '0', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', '-crf', '18', '-f', 'flv', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.publicEndpoint+'/'+results[0].username], {maxBuffer: Infinity}); - //exec('ffmpeg -analyzeduration 0 -i rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key+' -vcodec copy -acodec copy -crf 18 -f flv rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.publicEndpoint+'/'+results[0].username); - //push to mpd after making sure directory exists - mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, (err) => {;}); - while(true){ - if(session.audioCodec !== 0 && session.videoCodec !== 0){ - execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-y', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-map', '0:2', '-map', '0:2', '-map', '0:2', '-map', '0:1', '-c:a', 'copy', '-c:v:0', 'copy', '-c:v:1', 'libx264', '-c:v:2', 'libx264', '-crf:1', '33', '-crf:2', '40', '-b:v:1', '3000K', '-b:v:2', '1500K', '-remove_at_exit', '1', '-seg_duration', '1', '-window_size', '30', '-f', 'dash', satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/index.mpd'], {maxBuffer: Infinity}); - break; - } - await sleep(300); - } + db.query('update user_meta set live=true where username=\''+results[0].username+'\' limit 1'); + console.log('[NodeMediaServer] Stream key ok for stream:',id); } else{ console.log('[NodeMediaServer] Invalid stream key for stream:',id); @@ -91,8 +74,12 @@ function init (mediaconfig: any, satyrconfig: any) { nms.on('donePublish', (id, StreamPath, args) => { let app: string = StreamPath.split("/")[1]; let key: string = StreamPath.split("/")[2]; - if(app === satyrconfig.publicEndpoint) { - db.query('update user_meta set live=false where username=\''+key+'\' limit 1'); + if(app === satyrconfig.privateEndpoint) { + db.query('update user_meta,users set user_meta.live=false where users.stream_key='+db.raw.escape(key)); + db.query('select username from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => { + keystore.rm(results[0].username); + }); + } }); nms.on('prePlay', (id, StreamPath, args) => { @@ -105,11 +92,20 @@ function init (mediaconfig: any, satyrconfig: any) { session.reject(); return false; } - //disallow playing from the private endpoint for anyone except localhost - //(this will be the ffmpeg instance redirecting the stream) - if(app === satyrconfig.privateEndpoint && !session.isLocal) { + //localhost can play from whatever endpoint + //other clients must use private endpoint + if(app !== satyrconfig.publicEndpoint && !session.isLocal) { console.log("[NodeMediaServer] Non-local Play from private endpoint, rejecting client:",id); session.reject(); + return false; + } + //rewrite playpath to private endpoint serverside + //(hopefully) + if(app === satyrconfig.publicEndpoint) { + if(keystore[key]){ + session.playStreamPath = '/'+satyrconfig.privateEndpoint+'/'+keystore[key]; + return true; + } } }); } diff --git a/templates/user.njk b/templates/user.njk index ed13dbe..540089c 100644 --- a/templates/user.njk +++ b/templates/user.njk @@ -7,13 +7,13 @@ function newPopup(url) { }
- {{ user }} | {{ streamtitle | escape }} Links | Watch Chat VODs + {{ username }} | {{ title | escape }} Links | Watch Chat VODs
- +
@@ -32,7 +32,7 @@ function newPopup(url) { document.querySelector(".vjs-modal-dialog-content").textContent = "The stream is currently offline."; }); player.src({ - src: '/live/{{ user }}/index.mpd', + src: '/live/{{ username }}/index.mpd', type: 'application/dash+xml' }); })