Merge branch 'develop' into 'master'

Develop -> Master

See merge request knotteye/satyr!7
merge-requests/8/merge
knotteye 2019-12-21 23:34:59 +00:00
commit e5dfa446a2
24 changed files with 435 additions and 479 deletions

5
.gitignore vendored
View File

@ -1,7 +1,6 @@
node_modules
site/live
config/local.toml
config/jwt.pem
config/generated.toml
config/**/*
!config/.gitkeep
install/db_setup.sql
build/**

0
config/.gitkeep Normal file
View File

View File

@ -1,58 +0,0 @@
#DO NOT EDIT THIS FILE
#ALL CHANGES SHOULD GO IN LOCAL.TOML
[bcrypt]
saltRounds = 12
[satyr]
name = ''
domain = ''
registration = false
restrictedNames = ['live']
rootredirect = '/users/live'
[ircd]
enable = false
port = 6667
sid = ''
server = ''
pass = ''
vhost = 'web.satyr.net'
[database]
host = 'localhost'
user = 'satyr'
password = ''
database = 'satyr_db'
connectionLimit = '50'
connectionTimeout = '1000'
insecureAuth = false
debug = false
[server]
logs = 0
api = false
api_user = false
api_pass = false
[server.rtmp]
port = 1935
chunk_size = 6000
gop_cache = true
ping = 30
ping_timeout = 60
[server.http]
hsts = false
directory = './site'
port = 8000
[media]
record = false
publicEndpoint = 'live'
privateEndpoint = 'stream'
ffmpeg = ''
[transcode]
adapative = false
variants = 3
format = 'dash'

View File

@ -1,27 +1,48 @@
## Configuring Satyr
### Config file
All changes to satyr's config will go in the config/local.toml file
All changes to satyr's config will go in the config/config.yml file
Some values you might want to change are
```
[satyr]
registration = true
#allow new users to register
rootRedirect = '/users/live'
#the page users are directed to when they visit your site root
[media]
record = true
#allow users to record VODs
[bcrypt]
saltRounds = 12
satyr:
registration: true
# allow new users to register
rootRedirect: '/users/live'
# the page users are directed to when they visit your site root
http:
hsts: true
# enable strict transport security
media:
record: true
# allow users to record VODs
transcode:
adapative: true
# enable adaptive livestreaming
# will help users with poor connections, but EXTREMELY cpu intensive
# even 3 variants will max out most budget VPSs with a single stream
variants: 3
# the number of adaptive streaming variants to generate
# satyr will always copy the source stream
# and the remaining variants will lower the quality incrementally
# So the default setting of 3 will copy the source stream once
# And generate two lower quality & bitrate variants
crypto:
saltRounds: 12
#change the number of rounds of bcrypt to fit your hardware
#if you don't understand the implications, don't change this
[ircd]
enable = true
#enable IRC peering
#unused for now
irc:
port: 6667
#irc settings
#currently unused
```
### Web Frontend
If you want to customize the front-end css, place a file with any changes you wish to make at site/local.css
You can change the logo by replacing site/logo.svg.
You should also consider editing templates/about.html and templates/tos.html

View File

@ -2,8 +2,8 @@
A more detailed walkthrough.
### System Dependencies
Install ffmpeg and mysql through your distribution's package manager.
See [this page](https://nodejs.org/en/download/package-manager/) for instructions on install node. Compatible versions are >=10. Nightly builds may fail to compile some of the native addons.
Install ffmpeg(>= 4.2.1) and mysql through your distribution's package manager.
See [this page](https://nodejs.org/en/download/package-manager/) for instructions on installing node. Compatible versions are >=10. Nightly builds may fail to compile some of the native addons.
### Installing Satyr
Clone the repository and change to the directory
@ -25,10 +25,9 @@ Run the setup script for the database.
sudo mysql
source install/db_setup.sql;
```
Compile the code and start the server.
Then start the server.
```bash
npm run build
npm start
npm run start
```
It is reccomended that you run Satyr behind a TLS terminating reverse proxy, like nginx.
@ -40,10 +39,10 @@ Updating should be as simple as pulling the latest code and dependencies, then b
```bash
git pull
npm i
npm run build
npm update
```
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.
To backup and restore, you will need to export the mysqlDB. Restore the new database from the backup, then copy config and site directories to the new location.

View File

@ -52,6 +52,9 @@ The following commands are available:
`/nick kawen (password)` Password is only required if kawen is a registered user.
`/join kawen` Join the chatroom for kawen's stream and leave the previous room.
`/kick lain` Available only in your own room if you are a streamer. Forcefully disconnect the user.
`/ban lain (time)` Ban a user from your room. Bans are based on IP address. The optional time is in minutes. The default is 30.
`/banlist` List the IPs currently banned from your room.
`/unban (ip)` self explanatory
#### Streaming
Users should stream to rtmp://example.tld/stream/examplestreamkey

View File

@ -0,0 +1,24 @@
satyr:
name: '<iname>'
domain: '<domain>'
email: '<email>'
registration: false
media:
record: false
ffmpeg: '<ffmpeg>'
http:
# uncomment to set HSTS when SSL is ready
#hsts: true
database:
user: '<dbuser>'
password: '<dbpass>'
database: '<dbname>'
host: '<dbhost>'
transcode:
adaptive: false
format: dash
variants: 3

View File

@ -38,8 +38,8 @@ dbclient="${dbclient:='*'}"
else
dbclient="localhost"
fi
sed -e "s#<iname>#$name#g" -e "s#<domain>#$domain#g" -e "s#<ffmpeg>#$ffmpeg#g" -e "s#<dbuser>#$dbuser#g" -e "s#<dbname>#$dbname#g" -e "s#<dbpass>#$dbpass#g" -e "s#<dbhost>#$dbhost#g" -e "s#<email>#$email#g" install/template.local.toml > config/generated.toml
sed -e "s#<iname>#$name#g" -e "s#<domain>#$domain#g" -e "s#<ffmpeg>#$ffmpeg#g" -e "s#<dbuser>#$dbuser#g" -e "s#<dbname>#$dbname#g" -e "s#<dbpass>#$dbpass#g" -e "s#<dbhost>#$dbhost#g" -e "s#<email>#$email#g" install/config.example.yml > config/generated.yml
sed -e "s#<dbuser>#$dbuser#g" -e "s#<dbname>#$dbname#g" -e "s#<dbpass>#$dbpass#g" -e "s#<dbhost>#$dbhost#g" -e "s#<dbclient>#$dbclient#g" install/db_template.sql > install/db_setup.sql
echo "A setup script for the database has been generated at install/db_setup.sql. Please run it by connecting to your database software and executing 'source install/db_setup.sql;''"
echo "A default configuration file has been generated at config/generated.toml"
echo "If everything looks fine, move it to config/local.toml and start your instance."
echo "A default configuration file has been generated at config/generated.yml"
echo "If everything looks fine, move it to config/config.yml and start your instance."

View File

@ -1,19 +0,0 @@
[satyr]
name = '<iname>'
domain = '<domain>'
email = '<email>'
registration = false
[media]
record = false
ffmpeg = '<ffmpeg>'
[server.http]
# uncomment to set HSTS when SSL is enabled
# hsts = true
[database]
user = '<dbuser>'
password = '<dbpass>'
database = '<dbname>'
host = '<dbhost>'

73
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "satyr",
"version": "0.4.4",
"version": "0.5.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -82,6 +82,11 @@
"readable-stream": "^2.0.6"
}
},
"arg": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.2.tgz",
"integrity": "sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg=="
},
"arr-diff": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@ -342,6 +347,11 @@
}
}
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@ -644,6 +654,11 @@
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
},
"diff": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz",
"integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q=="
},
"dirty": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.0.tgz",
@ -1910,6 +1925,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"make-error": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
"integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g=="
},
"map-cache": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@ -2043,6 +2063,11 @@
"minimist": "0.0.8"
}
},
"moment": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -2287,6 +2312,11 @@
"os-tmpdir": "^1.0.0"
}
},
"parse-yaml": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/parse-yaml/-/parse-yaml-0.1.0.tgz",
"integrity": "sha512-tLfs2QiziUPFTA4nNrv2rrC0CnHDIF2o2m5TCgNss/E0asI0ltVjBcNKhcd/8vteZa8xKV5RGfD0ZFFlECMCqQ=="
},
"parseqs": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
@ -2712,6 +2742,14 @@
}
}
},
"socket-anti-spam": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/socket-anti-spam/-/socket-anti-spam-2.0.0.tgz",
"integrity": "sha512-glCDT8LrqwSY+tQJtvaz3YwTw1HL6bgWVvaQFumkClOcF+Jbg0NlAImqQabowNJcrCxr1dibKRoAvIfN98FKVw==",
"requires": {
"moment": "^2.21.0"
}
},
"socket.io": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz",
@ -2855,6 +2893,22 @@
"urix": "^0.1.0"
}
},
"source-map-support": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
"integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"source-map-url": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
@ -3016,6 +3070,18 @@
"resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
},
"ts-node": {
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.5.4.tgz",
"integrity": "sha512-izbVCRV68EasEPQ8MSIGBNK9dc/4sYJJKYA+IarMQct1RtEot6Xp0bXuClsbUSnKpg50ho+aOAx8en5c+y4OFw==",
"requires": {
"arg": "^4.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"source-map-support": "^0.5.6",
"yn": "^3.0.0"
}
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@ -3188,6 +3254,11 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
}
}
}

View File

@ -5,8 +5,8 @@
"license": "AGPL-3.0",
"author": "knotteye",
"scripts": {
"start": "tsc && node build/controller.js",
"user": "node build/cli.js",
"start": "ts-node src/index.ts",
"user": "ts-node src/cli.ts",
"setup": "sh install/setup.sh"
},
"repository": {
@ -16,7 +16,6 @@
"dependencies": {
"bcrypt": "^3.0.6",
"body-parser": "^1.19.0",
"config": "^3.2.2",
"cookie-parser": "^1.4.4",
"dirty": "^1.1.0",
"express": "^4.17.1",
@ -25,10 +24,12 @@
"mysql": "^2.17.1",
"node-media-server": ">=2.1.3 <3.0.0",
"nunjucks": "^3.2.0",
"parse-yaml": "^0.1.0",
"recursive-readdir": "^2.2.2",
"socket-anti-spam": "^2.0.0",
"socket.io": "^2.3.0",
"strftime": "^0.10.0",
"toml": "^3.0.0",
"ts-node": "^8.5.4",
"typescript": "^3.6.3"
},
"devDependencies": {

View File

@ -1,17 +1,12 @@
import * as db from "./database"
import { unregisterUser } from "./irc";
var config: any;
function init(conf: object){
config = conf;
}
import { config } from "./config";
async function register(name: string, password: string, confirm: string) {
if(!config.registration) return {"error":"registration disabled"};
if(!config['satyr']['registration']) return {"error":"registration disabled"};
if(name.includes(';') || name.includes(' ') || name.includes('\'')) return {"error":"illegal characters"};
if(password !== confirm) return {"error":"mismatched passwords"};
for(let i=0;i<config.restrictedNames.length;i++){
if (name === config.restrictedNames[i]) return {"error":"restricted name"};
for(let i=0;i<config['satyr']['restrictedNames'].length;i++){
if (name === config['satyr']['restrictedNames'][i]) return {"error":"restricted name"};
}
let r: boolean = await db.addUser(name, password);
if(r) {
@ -61,4 +56,4 @@ async function login(name: string, password: string){
return false;
}
export { init, register, update, changepwd, changesk, login };
export { register, update, changepwd, changesk, login };

View File

@ -1,8 +1,7 @@
import * as db from "./database"
import * as flags from "flags";
import * as config from "config"
db.init(config.database, config.bcrypt);
db.init();
flags.defineString('adduser', '', 'User to add');
flags.defineString('rmuser', '', 'User to remove');

49
src/config.ts Normal file
View File

@ -0,0 +1,49 @@
import {parseAsYaml as parse} from "parse-yaml";
import {readFileSync as read} from "fs";
var localconfig: Object = parse(read('config/config.yml'));
const config: Object = {
crypto: Object.assign({
saltRounds: 12
}, localconfig['crypto']),
satyr: Object.assign({
name: '',
domain: '',
registration: false,
restrictedNames: [ 'live' ],
rootredirect: '/users/live',
version: process.env.npm_package_version,
}, localconfig['satyr']),
ircd: Object.assign({
port: 6667,
}, localconfig['ircd']),
database: Object.assign({
host: 'localhost',
user: 'satyr',
password: '',
database: 'satyr_db',
connectionLimit: '50',
connectionTimeout: '1000',
insecureAuth: false,
debug: false }, localconfig['database']),
rtmp: Object.assign({
port: 1935,
chunk_size: 6000,
gop_cache: true,
ping: 30,
ping_timeout: 60 }, localconfig['rtmp']),
http: Object.assign({
hsts: false, directory: './site', port: 8000
}, localconfig['http']),
media: Object.assign({
record: false,
publicEndpoint: 'live',
privateEndpoint: 'stream',
ffmpeg: ''
}, localconfig['media']),
transcode: Object.assign({
adapative: false,
variants: 3,
format: 'dash'
}, localconfig['transcode'])
};
export { config };

View File

@ -1,67 +0,0 @@
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";
async function run() {
const dbcfg: object = config.database;
const bcryptcfg: object = config.bcrypt;
const satyr: object = {
privateEndpoint: config.media.privateEndpoint,
publicEndpoint: config.media.publicEndpoint,
record: config.media.record,
registration: config.satyr.registration,
webFormat: config.satyr.webFormat,
restrictedNames: config.satyr.restrictedNames,
name: config.satyr.name,
domain: config.satyr.domain,
email: config.satyr.email,
rootredirect: config.satyr.rootredirect,
version: process.env.npm_package_version,
directory: config.server.http.directory,
ffmpeg: config.media.ffmpeg
};
const nms: object = {
logType: config.server.logs,
rtmp: {
port: config.server.rtmp.port,
chunk_size: config.server.rtmp.chunk_size,
gop_cache: config.server.rtmp.gop_cache,
ping: config.server.rtmp.ping,
ping_timeout: config.server.rtmp.ping_timeout,
},
/*http: {
port: config.server.http.port + 1,
mediaroot: config.server.http.directory,
allow_origin: config.server.http.allow_origin
},
trans: {
ffmpeg: config.media.ffmpeg,
tasks: [
{
app: config.media.publicEndpoint,
hls: config.transcode.hls,
hlsFlags: config.transcode.hlsFlags,
dash: config.transcode.dash,
dashFlags: config.transcode.dashFlags
}
]
},*/
auth: {
api: config.server.api,
api_user: config.server.api_user,
api_pass: config.server.api_pass
}
};
db.init(dbcfg, bcryptcfg);
await cleanup.init();
api.init(satyr);
http.init(satyr, config.server.http, config.ircd);
mediaserver.init(nms, satyr);
console.log(`Satyr v${process.env.npm_package_version} ready`);
}
run();
export { run };

View File

@ -1,20 +1,21 @@
import * as mysql from "mysql";
import * as bcrypt from "bcrypt";
import * as crypto from "crypto";
import { config } from "./config";
import { resolve } from "url";
var raw: any;
var cryptoconfig: any;
var raw;
var cryptoconfig: Object;
function init (db: object, bcrypt: object){
raw = mysql.createPool(db);
cryptoconfig = bcrypt;
function init (){
raw = mysql.createPool(config['database']);
cryptoconfig = config['crypto'];
}
async function addUser(name: string, password: string){
//does not respect registration setting in config
if(password === '') return false;
let key: string = await genKey();
let hash: string = await bcrypt.hash(password, cryptoconfig.saltRounds);
let hash: string = await bcrypt.hash(password, cryptoconfig['saltRounds']);
let dupe = await query('select * from users where username='+raw.escape(name));
if(dupe[0]) return false;
await query('INSERT INTO users (username, password_hash, stream_key, record_flag) VALUES ('+raw.escape(name)+', '+raw.escape(hash)+', '+raw.escape(key)+', 0)');
@ -54,7 +55,7 @@ async function validatePassword(username: string, password: string){
}
async function hash(pwd){
return await bcrypt.hash(pwd, cryptoconfig.saltRounds);
return await bcrypt.hash(pwd, cryptoconfig['saltRounds']);
}
export { query, raw, init, addUser, rmUser, validatePassword, hash, genKey };

View File

@ -5,9 +5,10 @@ import * as socketio from "socket.io";
import * as http from "http";
import * as cookies from "cookie-parser";
import * as dirty from "dirty";
import * as socketSpam from "socket-anti-spam";
import * as api from "./api";
import * as db from "./database";
import * as irc from "./irc";
import { config } from "./config";
import { readdir, readFileSync, writeFileSync } from "fs";
import { JWT, JWK } from "jose";
import { strict } from "assert";
@ -18,9 +19,11 @@ const app = express();
const server = http.createServer(app);
const io = socketio(server);
const store = dirty();
var banlist;
var jwkey;
try{
jwkey = JWK.asKey(readFileSync('./config/jwt.pem'));
console.log('Found key for JWT signing.');
} catch (e) {
console.log("No key found for JWT signing, generating one now.");
jwkey = JWK.generateSync('RSA', 2048, { use: 'sig' });
@ -28,23 +31,23 @@ try{
}
var njkconf;
async function init(satyr: any, http: object, ircconf: any){
async function init(){
njk.configure('templates', {
autoescape : true,
express : app,
watch : false
});
njkconf ={
sitename: satyr.name,
domain: satyr.domain,
email: satyr.email,
rootredirect: satyr.rootredirect,
version: satyr.version
njkconf = {
sitename: config['satyr']['name'],
domain: config['satyr']['domain'],
email: config['satyr']['email'],
rootredirect: config['satyr']['rootredirect'],
version: config['satyr']['version']
};
app.use(cookies());
app.use(bodyparser.json());
app.use(bodyparser.urlencoded({ extended: true }));
if(http['hsts']){
if(config['http']['hsts']){
app.use((req, res, next) => {
res.append('Strict-Transport-Security', 'max-age=5184000');
next();
@ -52,11 +55,11 @@ async function init(satyr: any, http: object, ircconf: any){
}
app.disable('x-powered-by');
//site handlers
await initSite(satyr.registration);
await initSite(config['satyr']['registration']);
//api handlers
await initAPI();
//static files if nothing else matches first
app.use(express.static(satyr.directory));
app.use(express.static(config['http']['directory']));
//404 Handler
app.use(function (req, res, next) {
if(tryDecode(req.cookies.Authorization)) {
@ -65,19 +68,22 @@ async function init(satyr: any, http: object, ircconf: any){
else res.status(404).render('404.njk', njkconf);
//res.status(404).render('404.njk', njkconf);
});
await initChat(ircconf);
server.listen(http['port']);
banlist = new dirty('./config/bans.db').on('load', () => {initChat()});
server.listen(config['http']['port']);
}
async function newNick(socket, skip?: boolean) {
async function newNick(socket, skip?: boolean, i?: number) {
if(socket.handshake.headers['cookie'] && !skip){
let c = await parseCookie(socket.handshake.headers['cookie']);
let t = await validToken(c['Authorization']);
if(t) return t['username'];
if(t) {
store.set(t['username'], [].concat(store.get(t['username']), socket.id).filter(item => item !== undefined));
return t['username'];
}
//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, true);
}
if(!i) i = 10;
let n: string = 'Guest'+Math.floor(Math.random() * Math.floor(i));
if(store.get(n)) return newNick(socket, true, Math.floor(i * 10));
else {
store.set(n, socket.id);
return n;
@ -89,8 +95,12 @@ async function chgNick(socket, nick, f?: boolean) {
for(let i=1;i<rooms.length;i++){
io.to(rooms[i]).emit('ALERT', socket.nick+' is now known as '+nick);
}
if(store.get(socket.nick)) store.rm(socket.nick);
if (!f) store.set(nick, socket.id);
if(store.get(socket.nick)) {
if(Array.isArray(store.get(socket.nick))) store.set(socket.nick, store.get(socket.nick).filter(item => item !== socket.id));
else store.rm(socket.nick);
}
if(f) store.set(nick, [].concat(store.get(nick), [socket.id]).filter(item => item !== undefined));
else store.set(nick, socket.id);
socket.nick = nick;
}
@ -328,30 +338,26 @@ async function initSite(openReg) {
});
}
async function initChat(ircconf: any) {
//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});
});
}
async function initChat() {
//set a cookie to request same nick
//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]){
if(banlist.get(data) && banlist.get(data)[socket['handshake']['address']]){
if(Math.floor(banlist.get(data)[socket['handshake']['address']]['time'] + (banlist.get(data)[socket['handshake']['address']]['length'] * 60)) < Math.floor(Date.now() / 1000)){
banlist.set(data, Object.assign({}, banlist.get(data), {[socket['handshake']['address']]: null}));
}
else {
socket.emit('ALERT', 'You are banned from that room');
return;
}
}
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');
});
@ -372,24 +378,22 @@ async function initChat(ircconf: any) {
});
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);
if(Array.isArray(store.get(socket.nick))) {
store.set(socket.nick, store.get(socket.nick).filter(item => item !== socket.id))
if(store.get(socket.nick) !== [])
return;
}
store.rm(socket.nick);
});
socket.on('NICK', async (data) => {
data.nick = data.nick.replace(' ','');
if(store.get(data.nick)){
socket.emit('ALERT', 'Nickname is already in use');
return false;
}
let user = await db.query('select username from users where username='+db.raw.escape(data.nick));
if(user[0]){
if(!data.password){
@ -402,27 +406,102 @@ async function initChat(ircconf: any) {
else socket.emit('ALERT','Incorrect username or password');
}
else {
if(store.get(data.nick)){
socket.emit('ALERT', 'Nickname is already in use');
return false;
}
chgNick(socket, data.nick);
}
});
socket.on('MSG', (data) => {
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);
if(socket.rooms[data['room']]) io.to(data.room).emit('MSG', {nick: socket.nick, msg: data.msg});
});
socket.on('KICK', (data) => {
if(socket.nick === data.room){
//find client with data.nick
let id: string = store.get(data.nick);
if(id){
if(Array.isArray(id)) {
for(let i=0;i<id.length;i++){
io.sockets.connected[id[i]].leave(data.room);
}
io.in(data.room).emit('ALERT', data.nick+' has been kicked.');
return;
}
let target = io.sockets.connected[id];
io.in(data.room).emit('ALERT', data.nick+' has been kicked.');
target.disconnect(true);
target.leave(data.room);
}
else socket.emit('ALERT', 'No such user found.');
}
else socket.emit('ALERT', 'Not authorized to do that.');
});
socket.on('BAN', (data: Object) => {
if(socket.nick === data['room']){
let id: string = store.get(data['nick']);
if(id){
if(Array.isArray(id)) {
for(let i=0;i<id.length;i++){
let target = io.sockets.connected[id[i]];
if(typeof(data['time']) === 'number' && (data['time'] !== 0 && data['time'] !== NaN)) banlist.set(data['room'], Object.assign({}, banlist.get(data['room']), {[target.ip]: {time: Math.floor(Date.now() / 1000), length: data['time']}}));
else banlist.set(data['room'], Object.assign({}, banlist.get(data['room']), {[target.ip]: {time: Math.floor(Date.now() / 1000), length: 30}}));
target.leave(data['room']);
}
io.to(data['room']).emit('ALERT', data['nick']+' was banned.');
return;
}
let target = io.sockets.connected[id];
if(typeof(data['time']) === 'number' && (data['time'] !== 0 && data['time'] !== NaN)) banlist.set(data['room'], Object.assign({}, banlist.get(data['room']), {[target.ip]: {time: Math.floor(Date.now() / 1000), length: data['time']}}));
else banlist.set(data['room'], Object.assign({}, banlist.get(data['room']), {[target.ip]: {time: Math.floor(Date.now() / 1000), length: 30}}));
target.leave(data['room']);
io.to(data['room']).emit('ALERT', target.nick+' was banned.');
}
else socket.emit('ALERT', 'No such user found.');
}
else socket.emit('ALERT', 'Not authorized to do that.');
});
socket.on('UNBAN', (data: Object) => {
if(socket.nick === data['room']){
if(banlist.get(data['room']) && banlist.get(data['room'])[data['ip']]){
banlist.set(data['room'], Object.assign({}, banlist.get(data['room']), {[data['ip']]: null}));
socket.emit('ALERT', data['ip']+' was unbanned.');
}
else
socket.emit('ALERT', 'That IP is not banned.');
}
else socket.emit('ALERT', 'Not authorized to do that.');
});
socket.on('LISTBAN', (data: Object) => {
if(socket.nick === data['room']){
if(banlist.get(data['room'])) {
let bans = Object.keys(banlist.get(data['room']));
let str = '';
for(let i=0;i<bans.length;i++){
str += bans[i]+', ';
}
socket.emit('ALERT', 'Banned IP adresses: '+str.substring(0, str.length - 2));
return;
}
socket.emit('ALERT', 'No one is banned from this room');
}
else socket.emit('ALERT', 'Not authorized to do that.');
});
});
//socketio spam
const socketAS = new socketSpam({
banTime: 20,
kickThreshold: 10,
kickTimesBeforeBan: 3,
banning: true,
io: io
});
socketAS.event.on('ban', (socket) => {
let rooms = Object.keys(socket.rooms);
for(let i=1;i<rooms.length;i++){
io.to(rooms[i]).emit('ALERT', socket.nick+' was banned.');
}
store.rm(socket.nick);
});
}

15
src/index.ts Normal file
View File

@ -0,0 +1,15 @@
import * as mediaserver from "./server";
import * as db from "./database";
import * as http from "./http";
import * as cleanup from "./cleanup";
import { config } from "./config";
async function run() {
await db.init();
await cleanup.init();
await http.init();
await mediaserver.init();
console.log(`Satyr v${config['satyr']['version']} ready`);
}
run();
export { run };

View File

@ -1,212 +0,0 @@
// 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)
}

View File

@ -3,13 +3,14 @@ import * as dirty from "dirty";
import { mkdir, fstat, access } from "fs";
import * as strf from "strftime";
import * as db from "./database";
import {config} from "./config";
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);
function init () {
const nms = new NodeMediaServer({logType: 0,rtmp: config['rtmp']});
nms.run();
nms.on('postPublish', (id, StreamPath, args) => {
@ -23,7 +24,7 @@ function init (mediaconfig: any, satyrconfig: any) {
session.reject();
return false;
}
if(app !== satyrconfig.privateEndpoint){
if(app !== config['media']['privateEndpoint']){
//app isn't at public endpoint if we've reached this point
console.log("[NodeMediaServer] Wrong endpoint, rejecting stream:",id);
session.reject();
@ -34,23 +35,23 @@ function init (mediaconfig: any, satyrconfig: any) {
//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
//transcode to mpd after making sure directory exists
keystore[results[0].username] = key;
mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, ()=>{;});
mkdir(config['http']['directory']+'/'+config['media']['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});
transCommand(results[0].username, key).then((r) => {
execFile(config['media']['ffmpeg'], r, {maxBuffer: Infinity});
});
break;
}
await sleep(300);
}
if(results[0].record_flag && satyrconfig.record){
if(results[0].record_flag && config['media']['record']){
console.log('[NodeMediaServer] Initiating recording for stream:',id);
mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, (err) => {
mkdir(config['http']['directory']+'/'+config['media']['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'], {
execFile(config['media']['ffmpeg'], ['-loglevel', 'fatal', '-i', 'rtmp://127.0.0.1:'+config['rtmp']['port']+'/'+config['media']['privateEndpoint']+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+results[0].username+'/'+strf('%d%b%Y-%H%M')+'.mp4'], {
detached : true,
stdio : 'inherit',
maxBuffer: Infinity
@ -74,7 +75,7 @@ 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.privateEndpoint) {
if(app === config['media']['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) => {
if(results[0]) keystore.rm(results[0].username);
@ -94,19 +95,48 @@ function init (mediaconfig: any, satyrconfig: any) {
}
//localhost can play from whatever endpoint
//other clients must use private endpoint
if(app !== satyrconfig.publicEndpoint && !session.isLocal) {
if(app !== config['media']['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(app === config['media']['publicEndpoint']) {
if(keystore[key]){
session.playStreamPath = '/'+satyrconfig.privateEndpoint+'/'+keystore[key];
session.playStreamPath = '/'+config['media']['privateEndpoint']+'/'+keystore[key];
return true;
}
}
});
}
async function transCommand(user: string, key: string): Promise<string[]>{
let args: string[] = ['-loglevel', 'fatal', '-y', '-i', 'rtmp://127.0.0.1:'+config['rtmp']['port']+'/'+config['media']['privateEndpoint']+'/'+key];
if(config['transcode']['adaptive']===true && config['transcode']['variants'] > 1) {
for(let i=0;i<config['transcode']['variants'];i++){
args = args.concat(['-map', '0:2']);
}
args = args.concat(['-map', '0:1', '-c:a', 'copy', '-c:v:0', 'copy']);
for(let i=1;i<config['transcode']['variants'];i++){
args = args.concat(['-c:v:'+i, 'libx264',]);
}
for(let i=1;i<config['transcode']['variants'];i++){
let crf: number = Math.floor(18 + (i * 8)) > 51 ? 51 : Math.floor(18 + (i * 7));
args = args.concat(['-crf:'+i, ''+crf]);
}
for(let i=1;i<config['transcode']['variants'];i++){
let bv: number = Math.floor((5000 / config['transcode']['variants']) * (config['transcode']['variants'] - i));
args = args.concat(['-b:v:'+i, ''+bv]);
}
}
else {
args = args.concat(['-c:a', 'copy', '-c:v', 'copy']);
}
if(config['transcode']['format'] === 'dash')
args = args.concat(['-remove_at_exit', '1', '-seg_duration', '1', '-window_size', '30', '-f', 'dash', config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+user+'/index.mpd']);
else if(config['transcode']['format'] === 'hls')
args = args.concat(['-remove_at_exit', '1', '-hls_time', '1', '-hls_list_size', '30', '-f', 'hls', config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+user+'/index.m3u8']);
return args;
}
export { init };

View File

@ -4,6 +4,8 @@
<link rel="stylesheet" type="text/css" href="/local.css">
<link rel="icon" type="image/svg" href="/logo.svg">
<title>{{ sitename }}</title>
{% block head %}
{% endblock %}
</head>
<body>
<div id="wrapper">

View File

@ -36,6 +36,24 @@
else if(m.startsWith('/list')){
socket.emit('LIST', {room: room});
}
else if(m.startsWith('/banlist')){
socket.emit('LISTBAN', {
room: room,
});
}
else if(m.startsWith('/ban')){
socket.emit('BAN', {
room: room,
nick: m.split(' ')[1],
time: (1 * m.split(' ')[2])
});
}
else if(m.startsWith('/unban')){
socket.emit('UNBAN', {
room: room,
ip: m.split(' ')[1]
});
}
else socket.emit('MSG', {room: room, msg: m});
document.getElementById('m').value = '';
}

View File

@ -4,9 +4,12 @@
<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>
<code><a>/nick kawen (password)</a></code> Password is only required if kawen is a registered user.</br>
<code><a>/join kawen</a></code> Join the chatroom for kawen's stream and leave the previous room.</br>
<code><a>/kick cvcvcv</a></code> Available only in your own room if you are a streamer. Forcefully disconnect the user.</br>
<code><a>/ban cvcvcv (time)</a></code> Ban a user from your room. Bans are based on IP address. The optional time is in minutes. The default is 30.</br>
<code><a>/banlist</a></code> List the IPs currently banned from your room.</br>
<code><a>/unban (ip)</a></code> self explanatory</br>
<h4>Streaming</h4>
Users should stream to <a>rtmp://{{ domain }}/stream/Stream-Key</a></br></br>

View File

@ -1,4 +1,8 @@
{% extends "base.njk" %}
{% block head %}
<script src="/videojs/video.min.js"></script>
<link rel="stylesheet" type="text/css" href="/videojs/video-js.min.css">
{% endblock %}
{% block content %}
<script>
function newPopup(url) {
@ -16,10 +20,11 @@ function newPopup(url) {
<iframe src="/chat?room={{ username }}" frameborder="0" style="width: 100%;height: 100%; min-height: 500px;" allowfullscreen></iframe>
</div>
</div>
<script>window.HELP_IMPROVE_VIDEOJS = false;</script>
<script src="/videojs/video.min.js"></script>
<link rel="stylesheet" type="text/css" href="/videojs/video-js.min.css">
</br>
<noscript>The webclients for the stream and the chat require javascript, but feel free to use the direct links above!</br></br></noscript>
{{ about | escape }}
<script>
window.HELP_IMPROVE_VIDEOJS = false;
var player = videojs('live-video', {
html: {
nativeCaptions: false,
@ -34,7 +39,5 @@ function newPopup(url) {
type: 'application/dash+xml'
});
})
</script></br>
<noscript>The webclients for the stream and the chat require javascript, but feel free to use the direct links above!</br></br></noscript>
{{ about | escape }}
</script>
{% endblock %}