Merge branch 'develop' into 'master'

develop->master

See merge request knotteye/satyr!1
merge-requests/2/merge v0.3.2
knotteye 2019-11-10 20:24:40 +00:00
commit 15eb76a30a
18 changed files with 395 additions and 21 deletions

2
CONTRIBUTORS Normal file
View File

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

View File

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

View File

@ -32,4 +32,18 @@ npm start
``` ```
It is reccomended that you run Satyr behind a TLS terminating reverse proxy, like nginx. 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.

View File

@ -3,9 +3,9 @@
### Administration ### 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. 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 port = 8000
[rtmp] [server.rtmp]
port = 1935 port = 1935
``` ```
Changing the rtmp port is not recommended. Changing the rtmp port is not recommended.

10
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "satyr", "name": "satyr",
"version": "0.2.0", "version": "0.3.2",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -2370,6 +2370,14 @@
"readable-stream": "^2.0.2" "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": { "regex-not": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",

View File

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

49
site/dashjs/AUTHORS.md Normal file
View File

@ -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]

14
site/dashjs/LICENSE.md Normal file
View File

@ -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.**

3
site/dashjs/dash.all.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
site/videojs/videojs-dash.min.js vendored Normal file

File diff suppressed because one or more lines are too long

23
src/cleanup.ts Normal file
View File

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

View File

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

View File

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

212
src/irc.js Normal file
View File

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

View File

@ -8,7 +8,7 @@
<body> <body>
<div id="wrapper"> <div id="wrapper">
<div id="header"> <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>
<div id="content"> <div id="content">
{% block content %} {% block content %}

View File

@ -1,6 +1,6 @@
{% extends "base.njk" %} {% extends "base.njk" %}
{% block content %} {% 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> <p></p>
<form action="/api/user/streamkey" method="POST" target="responseFrame"> <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> 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"> <input type="submit" value="Submit">
</form> </form>
<iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe> <iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe>
{% endblock %} {% endblock %}

16
templates/help.njk Normal file
View File

@ -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 %}

View File

@ -18,6 +18,8 @@ function newPopup(url) {
</div> </div>
<script>window.HELP_IMPROVE_VIDEOJS = false;</script> <script>window.HELP_IMPROVE_VIDEOJS = false;</script>
<script src="/videojs/video.min.js"></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"> <link rel="stylesheet" type="text/css" href="/videojs/video-js.min.css">
<script> <script>
var player = videojs('live-video', { var player = videojs('live-video', {
@ -30,8 +32,8 @@ 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.m3u8', src: '/live/{{ user }}/index.mpd',
type: 'application/x-mpegURL' type: 'application/dash+xml'
}); });
}) })
</script></br> </script></br>