Merge branch 'develop' into 'master'

develop->master

See merge request knotteye/satyr!1
merge-requests/2/merge v0.3.2
knotteye 5 years ago
commit 15eb76a30a
  1. 2
      CONTRIBUTORS
  2. 10
      config/default.toml
  3. 16
      docs/INSTALLATION.md
  4. 4
      docs/USAGE.md
  5. 10
      package-lock.json
  6. 3
      package.json
  7. 49
      site/dashjs/AUTHORS.md
  8. 14
      site/dashjs/LICENSE.md
  9. 3
      site/dashjs/dash.all.min.js
  10. 2
      site/videojs/videojs-dash.min.js
  11. 23
      src/cleanup.ts
  12. 8
      src/controller.ts
  13. 32
      src/http.ts
  14. 212
      src/irc.js
  15. 2
      templates/base.njk
  16. 4
      templates/changesk.njk
  17. 16
      templates/help.njk
  18. 6
      templates/user.njk

@ -0,0 +1,2 @@
knotteye <knotteye@airmail.cc>
crushv <nik@telekem.net>

@ -12,9 +12,11 @@ rootredirect = '/users/live'
[ircd]
enable = false
port = 7000
user = ''
port = 6667
sid = ''
server = ''
pass = ''
vhost = 'web.satyr.net'
[database]
host = 'localhost'
@ -51,7 +53,7 @@ privateEndpoint = 'stream'
ffmpeg = ''
[transcode]
hls = true
hls = false
hlsFlags = '[hls_time=2:hls_list_size=3:hls_flags=delete_segments]'
dash = false
dash = true
dashFlags = '[f=dash:window_size=3:extra_window_size=5]'

@ -32,4 +32,18 @@ npm start
```
It is reccomended that you run Satyr behind a TLS terminating reverse proxy, like nginx.
An example systemd service is provided at install/satyr.service. It assumes you've installed satyr into /opt/satyr, and created a satyr user with the home directory /var/lib/satyr for the purpose of running the service.
An example systemd service is provided at install/satyr.service. It assumes you've installed satyr into /opt/satyr, and created a satyr user with the home directory /var/lib/satyr for the purpose of running the service.
## Updating Satyr
Updating should be as simple as pulling the latest code and dependencies, then building and restarting the server.
```bash
git pull
npm i
npm run build
```
Then restart the server.
## Migrating Satyr
To backup and restore, you will need to export the mysqlDB. Restore the new database from the backup, then copy the config/local.toml file and the site directory to the new install.

@ -3,9 +3,9 @@
### Administration
Satyr needs access to port 1935 for RTMP streams, and will serve HTTP on port 8000. The ports can be changed with follow config lines.
```
[http]
[server.http]
port = 8000
[rtmp]
[server.rtmp]
port = 1935
```
Changing the rtmp port is not recommended.

10
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "satyr",
"version": "0.2.0",
"version": "0.3.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -2370,6 +2370,14 @@
"readable-stream": "^2.0.2"
}
},
"recursive-readdir": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz",
"integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==",
"requires": {
"minimatch": "3.0.4"
}
},
"regex-not": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",

@ -1,6 +1,6 @@
{
"name": "satyr",
"version": "0.3.0",
"version": "0.3.2",
"description": "A livestreaming server.",
"license": "AGPL-3.0",
"author": "knotteye",
@ -24,6 +24,7 @@
"mysql": "^2.17.1",
"node-media-server": ">=2.1.3 <3.0.0",
"nunjucks": "^3.2.0",
"recursive-readdir": "^2.2.2",
"socket.io": "^2.3.0",
"toml": "^3.0.0"
},

@ -0,0 +1,49 @@
# Dash.js Authors List
#####Please add entries to the bottom of the list in the following format
* @GitHub UserName (Required) [Name and/or Organization] (Optional)
#Authors
* @nweber [Digital Primates]
* @jefftapper [Jeff Tapper, Digital Primates]
* @KozhinM [Mikail Kozhin, Microsoft Open Technologies]
* @kirkshoop [Kirk Shoop, Microsoft Open Technologies]
* @wilaw [Will Law, Akamai Technologies]
* @AkamaiDASH [Dan Sparacio, Akamai Technologies]
* @dsilhavy [Daniel Silhavy, Fraunhofer Fokus]
* @greg80303 [Greg Rutz, CableLabs]
* @heff [Steve Hefferman, Brightcove]
* @Tomjohnson916 [Tom Johnson, Brightcove]
* @jeroenwiljering [Jeroen Wijering, JWPlayer]
* @bbcrddave [David Evans, BBC R&D]
* @bbert [Bertrand Berthelot, Orange]
* @vigneshvg [Vignesh Venkatasubramanian, Google]
* @nicosang [Nicolas Angot, Orange]
* @PriyadarshiniV
* @senthil-codr [Senthil]
* @dweremeichik [Dylan Weremeichik]
* @aleal-envivio
* @mconlin
* @umavinoth
* @lbonn
* @mdale [Michael Dale, Kaltura]
* @sgrebnov [Sergey Grebnov, Microsoft Open Technologies]
* @wesleytodd [Wes Todd, Vubeology]
* @colde [Loke Dupont, Xstream]
* @rgardler [Ross Gardler, Microsoft Open Technologies]
* @squapp
* @xiaomings
* @rcollins112 [Rich Collins, Wowza]
* @timothy003 [Timothy Liang]
* @JaapHaitsma
* @72lions [Thodoris Tsiridis, 72lions]
* @TobbeMobiTV [Torbjörn Einarsson, MobiTV]
* @TobbeEdgeware [Torbjörn Einarsson, Edgeware]
* @mstorsjo [Martin Storsjö]
* @Hyddan [Daniel Hedenius]
* @qjia7
* @waqarz
* @WMSPanel [WMSPanel Team]
* @matt-hammond-bbc [Matt Hammond, BBC R&D]
* @siropeich [Siro Blanco, Epic Labs]
* @epiclabsDASH [Jesus Oliva, Epic Labs]
* @adripanico [Adrian Caballero, Epic Labs]

@ -0,0 +1,14 @@
# dash.js BSD License Agreement
The copyright in this software is being made available under the BSD License, included below. This software may be subject to other third party and contributor rights, including patent rights, and no such rights are granted under this license.
**Copyright (c) 2015, Dash Industry Forum.
**All rights reserved.**
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of the Dash Industry Forum nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
**THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.**

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,23 @@
import * as db from "./database";
import * as read from "recursive-readdir";
import * as fs from "fs";
async function init(siteDir: string) {
//If satyr is restarted in the middle of a stream
//it causes problems
//Live flags in the database stay live
await db.query('update user_meta set live=false');
//and stray m3u8 files will play the last
//few seconds of a stream back
try {
var files = await read(siteDir+'/live', ['!*.m3u8']);
}
catch (error) {}
if(files === undefined || files.length == 0) return;
for(let i=0;i<files.length;i++){
fs.unlinkSync(files[i]);
}
return;
}
export { init };

@ -2,9 +2,10 @@ import * as mediaserver from "./server";
import * as db from "./database";
import * as api from "./api";
import * as http from "./http";
import * as cleanup from "./cleanup";
import * as config from "config";
function run(): void{
async function run() {
const dbcfg: object = config.database;
const bcryptcfg: object = config.bcrypt;
const satyr: object = {
@ -52,9 +53,10 @@ function run(): void{
}
};
api.init(satyr);
http.init(satyr, config.server.http.port);
db.init(dbcfg, bcryptcfg);
await cleanup.init(config.server.http.directory);
api.init(satyr);
http.init(satyr, config.server.http.port, config.ircd);
mediaserver.init(nms, satyr);
}
run();

@ -7,6 +7,7 @@ import * as http from "http";
import * as dirty from "dirty";
import * as api from "./api";
import * as db from "./database";
import * as irc from "./irc";
const app = express();
const server = http.createServer(app);
@ -14,11 +15,11 @@ const io = socketio(server);
const store = dirty();
var njkconf;
function init(satyr: any, port: number){
async function init(satyr: any, port: number, ircconf: any){
njk.configure('templates', {
autoescape: true,
express : app,
watch: true
watch: false
});
njkconf ={
sitename: satyr.name,
@ -51,9 +52,9 @@ function init(satyr: any, port: number){
});
});
app.get('/users/*', (req, res) => {
njkconf.user = req.url.split('/')[2].toLowerCase();
db.query('select title,about from user_meta where username='+db.raw.escape(njkconf.user)).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]){
njkconf.user = result[0].username;
njkconf.streamtitle = result[0].title;
njkconf.about = result[0].about;
res.render('user.njk', njkconf);
@ -89,6 +90,9 @@ function init(satyr: any, port: number){
app.get('/chat', (req, res) => {
res.render('chat.html', njkconf);
});
app.get('/help', (req, res) => {
res.render('help.njk', njkconf);
});
//api handlers
app.post('/api/register', (req, res) => {
api.register(req.body.username, req.body.password, req.body.confirm).then( (result) => {
@ -116,26 +120,44 @@ function init(satyr: any, port: number){
app.use(function (req, res, next) {
res.status(404).render('404.njk', njkconf);
});
//irc peering
if(ircconf.enable){
await irc.connect({
port: ircconf.port,
sid: ircconf.sid,
pass: ircconf.pass,
server: ircconf.server,
vhost: ircconf.vhost
});
irc.events.on('message', (nick, channel, msg) => {
io.to(channel).emit('MSG', {nick: nick, msg: msg});
});
}
//socket.io chat logic
io.on('connection', async (socket) => {
socket.nick = await newNick(socket);
if(ircconf.enable) irc.registerUser(socket.nick);
socket.on('JOINROOM', async (data) => {
let t: any = await db.query('select username from users where username='+db.raw.escape(data));
if(t[0]){
socket.join(data);
io.to(data).emit('JOINED', {nick: socket.nick});
if(ircconf.enable) irc.join(socket.nick, data);
}
else socket.emit('ALERT', 'Room does not exist');
});
socket.on('LEAVEROOM', (data) => {
socket.leave(data);
if(ircconf.enable) irc.part(socket.nick, data);
io.to(data).emit('LEFT', {nick: socket.nick});
});
socket.on('disconnecting', (reason) => {
let rooms = Object.keys(socket.rooms);
for(let i=1;i<rooms.length;i++){
if(ircconf.enable) irc.part(socket.nick, rooms[i]);
io.to(rooms[i]).emit('ALERT', socket.nick+' disconnected');
}
if(ircconf.enable) irc.unregisterUser(socket.nick);
store.rm(socket.nick);
});
socket.on('NICK', async (data) => {
@ -160,7 +182,9 @@ function init(satyr: any, port: number){
}
});
socket.on('MSG', (data) => {
if(data.msg === "") return;
io.to(data.room).emit('MSG', {nick: socket.nick, msg: data.msg});
if(ircconf.enable) irc.send(socket.nick, data.room, data.msg);
});
socket.on('KICK', (data) => {
if(socket.nick === data.room){

@ -0,0 +1,212 @@
// written by crushv <nik@telekem.net>
// thanks nikki
const net = require('net')
const EventEmitter = require('events')
const socket = new net.Socket()
const emitter = new EventEmitter()
socket.setEncoding('utf8')
socket.on('error', console.error)
function m (text) {
console.log('> ' + text)
socket.write(text + '\r\n')
}
var config
socket.once('connect', async () => {
console.log('Connected')
m(`PASS ${config.pass} TS 6 :${config.sid}`)
m('CAPAB QS ENCAP EX IE SAVE EUID')
m(`SERVER ${config.server} 1 satyr`)
})
function parseLine (l) {
const colIndex = l.lastIndexOf(':')
if (colIndex > -1) {
return {
params: l.substring(0, colIndex - 1).split(' '),
query: l.substring(colIndex + 1)
}
} else return { params: l.split(' ') }
}
const servers = []
const users = {}
const channels = {}
const globalCommands = {
// PING :42X
// params: SID
PING: l => {
const { query } = parseLine(l)
m(`PONG :${query}`)
emitter.emit('ping')
},
// PASS hunter2 TS 6 :42X
// params: password, 'TS', TS version, SID
PASS: l => {
const { query } = parseLine(l)
// adds a server
servers.push(query)
}
}
const serverCommands = {
// EUID nik 1 1569146316 +i ~nik localhost6.attlocal.net 0::1 42XAAAAAB * * :nik
// params: nickname, hopcount, nickTS, umodes, username, visible hostname, IP address, UID, real hostname, account name, gecos
EUID: l => {
const { params } = parseLine(l)
const user = {
nick: params[0],
nickTS: params[2],
modes: params[3],
username: params[4],
vhost: params[5],
ip: params[6],
uid: params[7]
}
users[user.uid] = user
},
// SJOIN 1569142987 #test +nt :42XAAAAAB
// params: channelTS, channel, simple modes, opt. mode parameters..., nicklist
SJOIN: l => {
const { params, query } = parseLine(l)
const channel = {
timestamp: params[0],
name: params[1],
modes: params.slice(2).join(' '),
nicklist: query.split(' ').map(uid => {
if (/[^0-9a-zA-Z]/.test(uid[0])) return { uid: uid.slice(1), mode: uid[0] }
else return { uid: uid, mode: '' }
})
}
channels[channel.name] = channel
}
}
const userCommands = {
// :42XAAAAAC PRIVMSG #test :asd
// params: target, msg
PRIVMSG: (l, source) => {
const { params, query } = parseLine(l)
emitter.emit('message', users[source].nick, params[0], query)
},
// :42XAAAAAC JOIN 1569149395 #test +
JOIN: (l, source) => {
const { params } = parseLine(l)
channels[params[1]].nicklist.push({
uid: source
})
},
// :42XAAAAAC PART #test :WeeChat 2.6
PART: (l, source) => {
const { params } = parseLine(l)
for (let i = 0; i < channels[params[0]].nicklist.length; i++) {
if (channels[params[0]].nicklist[i].uid === source) {
channels[params[0]].nicklist.splice(i, 1)
return
}
}
},
QUIT: (_l, source) => {
delete users[source]
}
}
function parser (l) {
const split = l.split(' ')
const cmd = split[0]
const args = split.slice(1).join(' ')
if (globalCommands[cmd]) return globalCommands[cmd](args)
if (cmd[0] === ':') {
const source = cmd.slice(1)
const subcmd = split[1]
const subargs = split.slice(2).join(' ')
if (servers.indexOf(source) > -1 && serverCommands[subcmd]) serverCommands[subcmd](subargs)
if (users[source] && userCommands[subcmd]) userCommands[subcmd](subargs, source)
}
}
socket.on('data', data => {
data.split('\r\n')
.filter(l => l !== '')
.forEach(l => {
console.log('< ' + l)
parser(l)
})
})
module.exports.connect = conf => new Promise((resolve, reject) => {
emitter.once('ping', resolve)
config = conf
socket.connect(config.port)
process.on('SIGINT', () => {
socket.write('QUIT\r\n')
process.exit()
})
})
module.exports.events = emitter
const genTS = () => Math.trunc((new Date()).getTime() / 1000)
const genUID = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
var uid = ''
for (let i = 0; i < 6; i++) uid += chars.charAt(Math.floor(Math.random() * chars.length))
if (users[uid]) return genUID()
return config.sid + uid
}
const getUID = nick => {
for (const key in users) if (users[key].nick === nick) return key
}
module.exports.registerUser = nick => {
const user = {
nick: nick,
nickTS: genTS(),
modes: '+i',
username: '~' + nick,
vhost: config.vhost,
ip: '0::1',
uid: genUID()
}
users[user.uid] = user
m(`EUID ${user.nick} 1 ${user.nickTS} ${user.modes} ~${user.nick} ${user.vhost} 0::1 ${user.uid} * * :${user.nick}`)
}
module.exports.unregisterUser = nick => {
const uid = getUID(nick)
m(`:${uid} QUIT :Quit: satyr`)
delete users[uid]
}
module.exports.join = (nick, channelName) => {
const uid = getUID(nick)
if (!channels[channelName]) {
const channel = {
timestamp: genTS(),
name: channelName,
modes: '+nt',
nicklist: [{ uid: uid, mode: '' }]
}
channels[channel.name] = channel
}
m(`:${uid} JOIN ${channels[channelName].timestamp} ${channelName} +`)
}
module.exports.part = (nick, channelName) => {
const uid = getUID(nick)
m(`:${uid} PART ${channelName} :satyr`)
for (let i = 0; i < channels[channelName].nicklist.length; i++) {
if (channels[channelName].nicklist[i].uid === uid) {
channels[channelName].nicklist.splice(i, 1)
return
}
}
}
module.exports.send = (nick, channelName, message) => {
const uid = getUID(nick)
m(`:${uid} PRIVMSG ${channelName} :${message}`)
emitter.emit('message', nick, channelName, message)
}

@ -8,7 +8,7 @@
<body>
<div id="wrapper">
<div id="header">
<span style="float:left;"><h4><a href="/">{{ sitename }}</a> | <a href="/users">Users</a> <a href="/users/live">Live</a> <a href="/about">About</a></h4></span><span style="float:right;"><h4><a href="/chat">Chat</a> | <a href="/profile">Profile</a></h4></span>
<span style="float:left;"><h4><a href="/">{{ sitename }}</a> | <a href="/users">Users</a> <a href="/users/live">Live</a> <a href="/about">About</a></h4></span><span style="float:right;"><h4><a href="/help">Help</a> | <a href="/profile">Profile</a></h4></span>
</div>
<div id="content">
{% block content %}

@ -1,6 +1,6 @@
{% extends "base.njk" %}
{% block content %}
<h3>Change your password on {{ sitename }}</h3><span style="font-size: small;">Not registered yet? Sign up <a href="/register">here</a>.</br> Update your <a href="/profile">profile</a> or <a href="/changepwd">password</a>.</span>
<h3>Get a new stream key on {{ sitename }}</h3><span style="font-size: small;">Not registered yet? Sign up <a href="/register">here</a>.</br> Update your <a href="/profile">profile</a> or <a href="/changepwd">password</a>.</span>
<p></p>
<form action="/api/user/streamkey" method="POST" target="responseFrame">
Username: </br><input type="text" name="username" style="min-width: 300px" placeholder="e.g. lain"/></br>
@ -8,4 +8,4 @@
<input type="submit" value="Submit">
</form>
<iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe>
{% endblock %}
{% endblock %}

@ -0,0 +1,16 @@
{% extends "base.njk" %}
{% block content %}
<p></p>
<h4>Chatting</h4>
The webclient for chat can be accessed on the streamer's page, or at <a href="https://{{ domain }}/chat">https://{{ domain }}/chat</a></br></br>
The following commands are available:</br>
`/nick kawen (password)` Password is only required if kawen is a registered user.</br>
`/join kawen` Join the chatroom for kawen's stream and leave the previous room.</br>
`/kick cvcvcv` Available only in your own room if you are a streamer. Forcefully disconnect the user.</br>
<h4>Streaming</h4>
Users should stream to <a>rtmp://{{ domain }}/stream/Stream-Key</a></br></br>
The stream will be available at <a>rtmp://{{ domain }}/live/username</a> </br>or at your page on <a>https://{{ domain }}/users/username</a></br>
</br>
Most software, such as OBS, will have a separate field for the URL and stream key, in which case you can enter rtmp://{{ domain }}/stream/ and the stream key in the appropriate field.
{% endblock %}

@ -18,6 +18,8 @@ function newPopup(url) {
</div>
<script>window.HELP_IMPROVE_VIDEOJS = false;</script>
<script src="/videojs/video.min.js"></script>
<script src="/dashjs/dash.all.min.js"></script>
<script src="/videojs/videojs-dash.min.js"></script>
<link rel="stylesheet" type="text/css" href="/videojs/video-js.min.css">
<script>
var player = videojs('live-video', {
@ -30,8 +32,8 @@ function newPopup(url) {
document.querySelector(".vjs-modal-dialog-content").textContent = "The stream is currently offline.";
});
player.src({
src: '/live/{{ user }}/index.m3u8',
type: 'application/x-mpegURL'
src: '/live/{{ user }}/index.mpd',
type: 'application/dash+xml'
});
})
</script></br>