Merge branch 'develop' into 'master'
Develop -> Master See merge request knotteye/satyr!5merge-requests/6/merge
commit
f6da919b5e
|
@ -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",
|
||||
|
|
25
src/http.ts
25
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);
|
||||
|
|
100
src/server.ts
100
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
});
|
||||
})
|
||||
|
|
Reference in New Issue