Merge branch 'develop' into 'master'
Develop -> Master See merge request knotteye/satyr!5merge-requests/6/merge
commit
f6da919b5e
|
@ -1,5 +1,5 @@
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=A livestreaming server.
|
Description=satyr livestreaming server
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "satyr",
|
"name": "satyr",
|
||||||
"version": "0.4.3",
|
"version": "0.4.4",
|
||||||
"description": "A livestreaming server.",
|
"description": "A livestreaming server.",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"author": "knotteye",
|
"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) => {
|
app.get('/users', (req, res) => {
|
||||||
db.query('select username from users').then((result) => {
|
db.query('select username from users').then((result) => {
|
||||||
njkconf.list = result;
|
res.render('list.njk', Object.assign({list: result}, njkconf));
|
||||||
res.render('list.njk', njkconf);
|
|
||||||
njkconf.list = '';
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.get('/users/live', (req, res) => {
|
app.get('/users/live', (req, res) => {
|
||||||
db.query('select username,title from user_meta where live=1;').then((result) => {
|
db.query('select username,title from user_meta where live=1;').then((result) => {
|
||||||
njkconf.list = result;
|
res.render('live.njk', Object.assign({list: result}, njkconf));
|
||||||
res.render('live.njk', njkconf);
|
|
||||||
njkconf.list = '';
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.get('/users/*', (req, res) => {
|
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) => {
|
db.query('select username,title,about from user_meta where username='+db.raw.escape(req.url.split('/')[2].toLowerCase())).then((result) => {
|
||||||
if(result[0]){
|
if(result[0]){
|
||||||
njkconf.user = result[0].username;
|
res.render('user.njk', Object.assign(result[0], njkconf));
|
||||||
njkconf.streamtitle = result[0].title;
|
|
||||||
njkconf.about = result[0].about;
|
|
||||||
res.render('user.njk', njkconf);
|
|
||||||
}
|
}
|
||||||
else res.render('404.njk', njkconf);
|
else res.render('404.njk', njkconf);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.get('/vods/*', (req, res) => {
|
app.get('/vods/*', (req, res) => {
|
||||||
njkconf.user = req.url.split('/')[2].toLowerCase();
|
db.query('select username from user_meta where username='+db.raw.escape(req.url.split('/')[2].toLowerCase())).then((result) => {
|
||||||
db.query('select username from user_meta where username='+db.raw.escape(njkconf.user)).then((result) => {
|
|
||||||
if(result[0]){
|
if(result[0]){
|
||||||
fs.readdir('./site/live/'+njkconf.user, {withFileTypes: true} , (err, files) => {
|
fs.readdir('./site/live/'+njkconf.user, {withFileTypes: true} , (err, files) => {
|
||||||
if(files) njkconf.list = files.filter(fn => fn.name.endsWith('.mp4'));
|
res.render('vods.njk', Object.assign({user: result[0].username, list: files.filter(fn => fn.name.endsWith('.mp4'))}, njkconf));
|
||||||
else njkconf.list = [];
|
|
||||||
res.render('vods.njk', njkconf);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else res.render('404.njk', njkconf);
|
else res.render('404.njk', njkconf);
|
||||||
|
@ -182,7 +172,7 @@ async function init(satyr: any, port: number, ircconf: any){
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
socket.on('MSG', (data) => {
|
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});
|
io.to(data.room).emit('MSG', {nick: socket.nick, msg: data.msg});
|
||||||
if(ircconf.enable) irc.send(socket.nick, data.room, 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) {
|
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);
|
if(store.get(n)) return newNick(socket);
|
||||||
else {
|
else {
|
||||||
store.set(n, socket.id);
|
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 NodeMediaServer from "node-media-server";
|
||||||
|
import * as dirty from "dirty";
|
||||||
import { mkdir, fstat, access } from "fs";
|
import { mkdir, fstat, access } from "fs";
|
||||||
import * as strf from "strftime";
|
import * as strf from "strftime";
|
||||||
import * as db from "./database";
|
import * as db from "./database";
|
||||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
const { exec, execFile } = require('child_process');
|
const { exec, execFile } = require('child_process');
|
||||||
|
|
||||||
|
const keystore = dirty();
|
||||||
|
|
||||||
function init (mediaconfig: any, satyrconfig: any) {
|
function init (mediaconfig: any, satyrconfig: any) {
|
||||||
const nms = new NodeMediaServer(mediaconfig);
|
const nms = new NodeMediaServer(mediaconfig);
|
||||||
nms.run();
|
nms.run();
|
||||||
|
@ -15,31 +18,39 @@ function init (mediaconfig: any, satyrconfig: any) {
|
||||||
let app: string = StreamPath.split("/")[1];
|
let app: string = StreamPath.split("/")[1];
|
||||||
let key: string = StreamPath.split("/")[2];
|
let key: string = StreamPath.split("/")[2];
|
||||||
//disallow urls not formatted exactly right
|
//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);
|
console.log("[NodeMediaServer] Malformed URL, closing connection for stream:",id);
|
||||||
session.reject();
|
session.reject();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if(app === satyrconfig.publicEndpoint) {
|
if(app !== satyrconfig.privateEndpoint){
|
||||||
if(session.isLocal) {
|
//app isn't at public endpoint if we've reached this point
|
||||||
//only allow publish to public endpoint from localhost
|
console.log("[NodeMediaServer] Wrong endpoint, rejecting stream:",id);
|
||||||
console.log("[NodeMediaServer] Local publish, stream:",`${id} ok.`);
|
session.reject();
|
||||||
}
|
return false;
|
||||||
else{
|
}
|
||||||
console.log("[NodeMediaServer] Non-local Publish to public endpoint, rejecting stream:",id);
|
//if the url is formatted correctly and the user is streaming to the correct private endpoint
|
||||||
session.reject();
|
//grab the username from the database and redirect the stream there if the key is valid
|
||||||
return false;
|
//otherwise kill the session
|
||||||
}
|
db.query('select username,record_flag from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => {
|
||||||
console.log("[NodeMediaServer] Public endpoint, checking record flag.");
|
if(results[0]){
|
||||||
//set live flag
|
//push to rtmp
|
||||||
db.query('update user_meta set live=true where username=\''+key+'\' limit 1');
|
//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});
|
||||||
//if this stream is from the public endpoint, check if we should be recording
|
//push to mpd after making sure directory exists
|
||||||
return db.query('select username,record_flag from users where username=\''+key+'\' limit 1').then((results) => {
|
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){
|
if(results[0].record_flag && satyrconfig.record){
|
||||||
console.log('[NodeMediaServer] Initiating recording for stream:',id);
|
console.log('[NodeMediaServer] Initiating recording for stream:',id);
|
||||||
mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, (err) => {
|
mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, (err) => {
|
||||||
if (err) throw 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,
|
detached : true,
|
||||||
stdio : 'inherit',
|
stdio : 'inherit',
|
||||||
maxBuffer: Infinity
|
maxBuffer: Infinity
|
||||||
|
@ -51,36 +62,8 @@ function init (mediaconfig: any, satyrconfig: any) {
|
||||||
else {
|
else {
|
||||||
console.log('[NodeMediaServer] Skipping recording for stream:',id);
|
console.log('[NodeMediaServer] Skipping recording for stream:',id);
|
||||||
}
|
}
|
||||||
return true;
|
db.query('update user_meta set live=true where username=\''+results[0].username+'\' limit 1');
|
||||||
});
|
console.log('[NodeMediaServer] Stream key ok for stream:',id);
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
console.log('[NodeMediaServer] Invalid stream key for stream:',id);
|
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) => {
|
nms.on('donePublish', (id, StreamPath, args) => {
|
||||||
let app: string = StreamPath.split("/")[1];
|
let app: string = StreamPath.split("/")[1];
|
||||||
let key: string = StreamPath.split("/")[2];
|
let key: string = StreamPath.split("/")[2];
|
||||||
if(app === satyrconfig.publicEndpoint) {
|
if(app === satyrconfig.privateEndpoint) {
|
||||||
db.query('update user_meta set live=false where username=\''+key+'\' limit 1');
|
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) => {
|
nms.on('prePlay', (id, StreamPath, args) => {
|
||||||
|
@ -105,11 +92,20 @@ function init (mediaconfig: any, satyrconfig: any) {
|
||||||
session.reject();
|
session.reject();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
//disallow playing from the private endpoint for anyone except localhost
|
//localhost can play from whatever endpoint
|
||||||
//(this will be the ffmpeg instance redirecting the stream)
|
//other clients must use private endpoint
|
||||||
if(app === satyrconfig.privateEndpoint && !session.isLocal) {
|
if(app !== satyrconfig.publicEndpoint && !session.isLocal) {
|
||||||
console.log("[NodeMediaServer] Non-local Play from private endpoint, rejecting client:",id);
|
console.log("[NodeMediaServer] Non-local Play from private endpoint, rejecting client:",id);
|
||||||
session.reject();
|
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>
|
</script>
|
||||||
</br>
|
</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="jscontainer">
|
||||||
<div id="jschild" style="width: 70%;height: 100%;">
|
<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>
|
<video controls poster="/thumbnail.jpg" class="video-js vjs-default-skin" id="live-video" style="width:100%;height:100%;"></video>
|
||||||
</div>
|
</div>
|
||||||
<div id="jschild" class="webchat" style="width: 30%;height: 100%;">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<script>window.HELP_IMPROVE_VIDEOJS = false;</script>
|
<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.";
|
document.querySelector(".vjs-modal-dialog-content").textContent = "The stream is currently offline.";
|
||||||
});
|
});
|
||||||
player.src({
|
player.src({
|
||||||
src: '/live/{{ user }}/index.mpd',
|
src: '/live/{{ username }}/index.mpd',
|
||||||
type: 'application/dash+xml'
|
type: 'application/dash+xml'
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
Reference in New Issue