Merge branch 'develop' into 'master'

Develop -> Master

See merge request knotteye/satyr!5
merge-requests/5/merge
knotteye 4 years ago
commit c1e5d0691e
  1. 2
      install/satyr.service
  2. 2
      package.json
  3. 25
      src/http.ts
  4. 92
      src/server.ts
  5. 6
      templates/user.njk

@ -1,5 +1,5 @@
[Unit]
Description=A livestreaming server.
Description=satyr livestreaming server
After=network.target
[Service]

@ -1,6 +1,6 @@
{
"name": "satyr",
"version": "0.4.3",
"version": "0.4.4",
"description": "A livestreaming server.",
"license": "AGPL-3.0",
"author": "knotteye",

@ -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);

@ -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,45 +18,11 @@ 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(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'], {
detached : true,
stdio : 'inherit',
maxBuffer: Infinity
}).unref();
//spawn an ffmpeg process to record the stream, then detach it completely
//ffmpeg can then (probably) finalize the recording if satyr crashes mid-stream
});
}
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);
@ -63,17 +32,13 @@ function init (mediaconfig: any, satyrconfig: any) {
//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) => {
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});
//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);
//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
mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, (err) => {;});
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});
@ -81,6 +46,24 @@ function init (mediaconfig: any, satyrconfig: any) {
}
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.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
}).unref();
//spawn an ffmpeg process to record the stream, then detach it completely
//ffmpeg can then (probably) finalize the recording if satyr crashes mid-stream
});
}
else {
console.log('[NodeMediaServer] Skipping recording for stream:',id);
}
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;
}
}
});
}

@ -7,13 +7,13 @@ function newPopup(url) {
}
</script>
</br>
<span style="float: left;font-size: large;"><a href="/live/{{ user }}/index.mpd">{{ user }}</a> | {{ streamtitle | escape }}</b></span><span style="float: right;font-size: large;"> Links | <a href="rtmp://{{ domain }}/live/{{ user }}">Watch</a> <a href="JavaScript:newPopup('/chat?room={{ user }}');">Chat</a> <a href="/vods/{{ user }}">VODs</a></span>
<span style="float: left;font-size: large;"><a href="/live/{{ username }}/index.mpd">{{ username }}</a> | {{ title | escape }}</b></span><span style="float: right;font-size: large;"> Links | <a href="rtmp://{{ domain }}/live/{{ username }}">Watch</a> <a href="JavaScript:newPopup('/chat?room={{ username }}');">Chat</a> <a href="/vods/{{ username }}">VODs</a></span>
<div id="jscontainer">
<div id="jschild" style="width: 70%;height: 100%;">
<video controls poster="/thumbnail.jpg" class="video-js vjs-default-skin" id="live-video" style="width:100%;height:100%;"></video>
</div>
<div id="jschild" class="webchat" style="width: 30%;height: 100%;">
<iframe src="/chat?room={{ user }}" frameborder="0" style="width: 100%;height: 100%;"></iframe>
<iframe src="/chat?room={{ username }}" frameborder="0" style="width: 100%;height: 100%;"></iframe>
</div>
</div>
<script>window.HELP_IMPROVE_VIDEOJS = false;</script>
@ -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'
});
})