Maintenance commit.

This commit is contained in:
Evert Prants 2020-05-28 22:06:09 +03:00
parent dbffa4262a
commit fb3d54205e
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
7 changed files with 3154 additions and 2739 deletions

164
app.js
View File

@ -5,15 +5,15 @@ const nunjucks = require('nunjucks')
const passport = require('passport') const passport = require('passport')
const express = require('express') const express = require('express')
const request = require('request') const request = require('request')
const sqlite3 = require('sqlite3')
const sqlite = require('sqlite') const sqlite = require('sqlite')
const xml2js = require('xml2js') const xml2js = require('xml2js')
const WebSocket = require('ws') const WebSocket = require('ws')
const uuid = require('uuid/v4')
const redis = require('redis') const redis = require('redis')
const path = require('path') const path = require('path')
const toml = require('toml') const toml = require('toml')
const http = require('http') const http = require('http')
const URL = require('url') const URL = require('url').URL
const fs = require('fs') const fs = require('fs')
require('express-async-errors') require('express-async-errors')
@ -22,7 +22,6 @@ const SessionStore = connectSession(session)
const util = require('util') const util = require('util')
const get = util.promisify(request.get) const get = util.promisify(request.get)
const post = util.promisify(request.post)
const dev = process.env.NODE_ENV === 'development' const dev = process.env.NODE_ENV === 'development'
@ -30,7 +29,7 @@ const dev = process.env.NODE_ENV === 'development'
const filename = path.join(__dirname, 'config.toml') const filename = path.join(__dirname, 'config.toml')
let config let config
let cache = { _updated: 0, streamers: {}, viewers: {}, live: [] } const cache = { _updated: 0, streamers: {}, viewers: {}, live: [] }
try { try {
config = toml.parse(fs.readFileSync(filename)) config = toml.parse(fs.readFileSync(filename))
@ -40,32 +39,33 @@ try {
} }
config = Object.assign({ config = Object.assign({
'Streaming': { Streaming: {
'port': '9322', port: '9322',
'database': 'streaming.db', database: 'streaming.db',
'streamServer': 'https://tv.icynet.eu/live/', streamServer: 'https://tv.icynet.eu/live/',
'serverHost': 'icynet.eu', serverHost: 'icynet.eu',
'publishAddress': 'rtmp://{host}:1935/hls-live/{streamer}', publishAddress: 'rtmp://{host}:1935/hls-live/{streamer}',
'secret': 'changeme' secret: 'changeme'
}, },
'Auth': { Auth: {
'strategy': 'passport-oauth2', strategy: 'passport-oauth2',
'callbackURL': 'http://localhost:5000/auth/_callback/', callbackURL: 'http://localhost:5000/auth/_callback/',
'clientID': '1', clientID: '1',
'clientSecret': 'changeme' clientSecret: 'changeme'
} }
}, config) }, config)
// Constants // Constants
const port = parseInt(config['Streaming']['port']) const port = parseInt(config.Streaming.port)
const streamServer = config['Streaming']['streamServer'] const streamServer = config.Streaming.streamServer
const streamServerHost = config['Streaming']['serverHost'] const streamServerHost = config.Streaming.serverHost
const streamAppName = streamServer.match(/\/([\w-_]+)\/$/)[1] const streamAppName = streamServer.match(/\/([\w-_]+)\/$/)[1]
// Database // Database
const dbPromise = Promise.resolve() const dbPromise = sqlite.open({
.then(() => sqlite.open(path.join(process.cwd(), config['Streaming']['database']), { Promise, cache: true })) filename: path.join(process.cwd(), config.Streaming.database),
.then(db => db.migrate()) driver: sqlite3.cached.Database
})
// Setup server // Setup server
const app = express() const app = express()
@ -73,8 +73,8 @@ const server = http.createServer(app)
const wss = new WebSocket.Server({ clientTracking: false, noServer: true }) const wss = new WebSocket.Server({ clientTracking: false, noServer: true })
// Authentication // Authentication
const Strategy = require(config['Auth']['strategy']) const Strategy = require(config.Auth.strategy)
const strategyConfig = Object.assign({}, config['Auth']) const strategyConfig = Object.assign({}, config.Auth)
if (!strategyConfig.provider) strategyConfig.provider = strategyConfig.strategy.replace('passport-', '') if (!strategyConfig.provider) strategyConfig.provider = strategyConfig.strategy.replace('passport-', '')
passport.use(new Strategy(strategyConfig, function (accessToken, refreshToken, profile, done) { passport.use(new Strategy(strategyConfig, function (accessToken, refreshToken, profile, done) {
process.nextTick(function () { process.nextTick(function () {
@ -104,7 +104,7 @@ nunjucks.configure('templates', {
const sessionParser = session({ const sessionParser = session({
key: 'Streamserver Session', key: 'Streamserver Session',
secret: config['Streaming']['secret'], secret: config.Streaming.secret,
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: true,
store: new SessionStore({ client: redis.createClient() }), store: new SessionStore({ client: redis.createClient() }),
@ -121,16 +121,16 @@ app.use(passport.session())
// Parse stream metrics from the stat.xml file // Parse stream metrics from the stat.xml file
async function pullMetrics (uuid) { async function pullMetrics (uuid) {
let statPath = streamServer + 'stat' const statPath = streamServer + 'stat'
if (!cache.stats || cache._updated < Date.now() - 5000) { if (!cache.stats || cache._updated < Date.now() - 5000) {
let { body } = await get(statPath) const { body } = await get(statPath)
let rip = await xml2js.parseStringPromise(body) const rip = await xml2js.parseStringPromise(body)
if (!rip.rtmp.server) throw new Error('Invalid response from server.') if (!rip.rtmp.server) throw new Error('Invalid response from server.')
// Autofind the correct server // Autofind the correct server
let rtmpserver = rip.rtmp.server[0].application const rtmpserver = rip.rtmp.server[0].application
let rtmpapp let rtmpapp
for (let i in rtmpserver) { for (const i in rtmpserver) {
if (rtmpserver[i].name[0] !== streamAppName) continue if (rtmpserver[i].name[0] !== streamAppName) continue
rtmpapp = rtmpserver[i] rtmpapp = rtmpserver[i]
} }
@ -143,7 +143,7 @@ async function pullMetrics (uuid) {
// Extract applicable stream data // Extract applicable stream data
let forUser let forUser
for (let i in cache.stats) { for (const i in cache.stats) {
if (!cache.stats[i].stream) continue if (!cache.stats[i].stream) continue
if (cache.stats[i].stream[0].name[0] !== uuid) continue if (cache.stats[i].stream[0].name[0] !== uuid) continue
forUser = cache.stats[i].stream[0] forUser = cache.stats[i].stream[0]
@ -152,7 +152,7 @@ async function pullMetrics (uuid) {
if (!forUser) return null if (!forUser) return null
// Generic data object // Generic data object
let data = { const data = {
time: forUser.time[0], time: forUser.time[0],
bytes: forUser.bytes_in[0], bytes: forUser.bytes_in[0],
video: null, video: null,
@ -161,7 +161,7 @@ async function pullMetrics (uuid) {
// Add video metadata, if applicable // Add video metadata, if applicable
if (forUser.meta[0].video[0] !== '') { if (forUser.meta[0].video[0] !== '') {
data['video'] = { data.video = {
width: forUser.meta[0].video[0].width[0], width: forUser.meta[0].video[0].width[0],
height: forUser.meta[0].video[0].height[0], height: forUser.meta[0].video[0].height[0],
frame_rate: forUser.meta[0].video[0].frame_rate[0], frame_rate: forUser.meta[0].video[0].frame_rate[0],
@ -171,7 +171,7 @@ async function pullMetrics (uuid) {
// Add audio metadata, if applicable // Add audio metadata, if applicable
if (forUser.meta[0].audio[0] !== '') { if (forUser.meta[0].audio[0] !== '') {
data['audio'] = { data.audio = {
sample_rate: forUser.meta[0].audio[0].sample_rate[0], sample_rate: forUser.meta[0].audio[0].sample_rate[0],
channels: forUser.meta[0].audio[0].channels[0], channels: forUser.meta[0].audio[0].channels[0],
codec: forUser.meta[0].audio[0].codec[0] codec: forUser.meta[0].audio[0].codec[0]
@ -185,16 +185,16 @@ async function pullMetrics (uuid) {
app.post('/publish', async (req, res) => { app.post('/publish', async (req, res) => {
if (!req.body.name) throw new Error('Invalid request.') if (!req.body.name) throw new Error('Invalid request.')
let db = await dbPromise const db = await dbPromise
// Validate stream key // Validate stream key
let streamer = await db.get('SELECT * FROM channels WHERE key=?', req.body.name) const streamer = await db.get('SELECT * FROM channels WHERE key=?', req.body.name)
if (!streamer) throw new Error('Invalid stream key.') if (!streamer) throw new Error('Invalid stream key.')
console.log('=> Streamer %s has started streaming!', streamer.name) console.log('=> Streamer %s has started streaming!', streamer.name)
// Generate real publish address for the server // Generate real publish address for the server
let publishAddress = config['Streaming']['publishAddress'] const publishAddress = config.Streaming.publishAddress
.replace('{streamer}', streamer.name) .replace('{streamer}', streamer.name)
.replace('{host}', '127.0.0.1') .replace('{host}', '127.0.0.1')
@ -211,8 +211,8 @@ app.post('/publish', async (req, res) => {
app.post('/publish_done', async (req, res) => { app.post('/publish_done', async (req, res) => {
if (!req.body.name) throw new Error('Invalid request.') if (!req.body.name) throw new Error('Invalid request.')
let db = await dbPromise const db = await dbPromise
let chan = await db.get('SELECT * FROM channels WHERE key = ?', req.body.name) const chan = await db.get('SELECT * FROM channels WHERE key = ?', req.body.name)
console.log('<= Streamer %s has stopped streaming!', chan.name) console.log('<= Streamer %s has stopped streaming!', chan.name)
try { delete cache.viewers[chan.name] } catch (e) {} try { delete cache.viewers[chan.name] } catch (e) {}
@ -230,14 +230,14 @@ app.get('/login', passport.authenticate(strategyConfig.provider, Object.assign({
app.get('/auth/_callback', passport.authenticate(strategyConfig.provider, { failureRedirect: '/' }), async (req, res) => { app.get('/auth/_callback', passport.authenticate(strategyConfig.provider, { failureRedirect: '/' }), async (req, res) => {
dev && console.log(req.user.username, 'logged in') dev && console.log(req.user.username, 'logged in')
// Get user from database // Get user from database
let db = await dbPromise const db = await dbPromise
let user = await db.get('SELECT * FROM signed_users WHERE uuid=?', req.user.uuid) const user = await db.get('SELECT * FROM signed_users WHERE uuid=?', req.user.uuid)
if (!user) { if (!user) {
await db.run('INSERT INTO signed_users (uuid,name) VALUES (?,?)', req.user.uuid, req.user.username) await db.run('INSERT INTO signed_users (uuid,name) VALUES (?,?)', req.user.uuid, req.user.username)
} }
// Lets see if this user is a streamer // Lets see if this user is a streamer
let streamer = await db.get('SELECT * FROM channels WHERE user_uuid = ?', req.user.uuid) const streamer = await db.get('SELECT * FROM channels WHERE user_uuid = ?', req.user.uuid)
if (streamer) cache.streamers[req.user.uuid] = streamer if (streamer) cache.streamers[req.user.uuid] = streamer
res.redirect('/') res.redirect('/')
@ -263,8 +263,8 @@ app.use(async function (req, res, next) {
res.locals.user = req.user res.locals.user = req.user
if (!cache.streamers[req.user.uuid]) { if (!cache.streamers[req.user.uuid]) {
let db = await dbPromise const db = await dbPromise
let streamer = await db.get('SELECT * FROM channels WHERE user_uuid = ?', req.user.uuid) const streamer = await db.get('SELECT * FROM channels WHERE user_uuid = ?', req.user.uuid)
if (streamer) cache.streamers[req.user.uuid] = streamer if (streamer) cache.streamers[req.user.uuid] = streamer
} }
@ -283,13 +283,13 @@ app.get('/', (req, res) => {
// Dashboard // Dashboard
app.get('/dashboard', authed, (req, res) => { app.get('/dashboard', authed, (req, res) => {
let stream = cache.streamers[req.user.uuid] const stream = cache.streamers[req.user.uuid]
res.render('dashboard.html', { stream: stream.key, server: 'rtmp://' + streamServerHost + '/live/' }) res.render('dashboard.html', { stream: stream.key, server: 'rtmp://' + streamServerHost + '/live/' })
}) })
// Stats // Stats
app.get('/dashboard/stats', authed, async (req, res) => { app.get('/dashboard/stats', authed, async (req, res) => {
let stream = cache.streamers[req.user.uuid] const stream = cache.streamers[req.user.uuid]
let data let data
try { try {
@ -304,10 +304,10 @@ app.get('/dashboard/stats', authed, async (req, res) => {
// Data // Data
app.get('/dashboard/data', authed, async (req, res) => { app.get('/dashboard/data', authed, async (req, res) => {
let stream = cache.streamers[req.user.uuid] const stream = cache.streamers[req.user.uuid]
let data let data
let db = await dbPromise const db = await dbPromise
try { try {
data = await db.get('SELECT * FROM channels WHERE key=?', stream.key) data = await db.get('SELECT * FROM channels WHERE key=?', stream.key)
@ -318,30 +318,30 @@ app.get('/dashboard/data', authed, async (req, res) => {
if (!data) return res.jsonp({ error: 'Unauthorized' }) if (!data) return res.jsonp({ error: 'Unauthorized' })
res.jsonp({ res.jsonp({
'name': data.name, name: data.name,
'key': stream.key, key: stream.key,
'uuid': req.user.uuid, uuid: req.user.uuid,
'live': data.live_at != null, live: data.live_at != null,
'live_at': new Date(parseInt(data.live_at)), live_at: new Date(parseInt(data.live_at)),
'last_stream': new Date(parseInt(data.last_stream)) last_stream: new Date(parseInt(data.last_stream))
}) })
}) })
// Get links // Get links
app.get('/dashboard/link', authed, async (req, res) => { app.get('/dashboard/link', authed, async (req, res) => {
let user = req.user.uuid const user = req.user.uuid
let db = await dbPromise const db = await dbPromise
let links = await db.all('SELECT * FROM link WHERE uuid = ?', user) const links = await db.all('SELECT * FROM link WHERE uuid = ?', user)
res.jsonp(links) res.jsonp(links)
}) })
// Add link URL // Add link URL
app.post('/dashboard/link', authed, async (req, res) => { app.post('/dashboard/link', authed, async (req, res) => {
let user = req.user.uuid const user = req.user.uuid
let name = req.body.name const name = req.body.name
let url = req.body.url const url = req.body.url
if (name == null || url == null) return res.jsonp({ error: 'Missing parameters!' }) if (name == null || url == null) return res.jsonp({ error: 'Missing parameters!' })
if (name.length > 120) return res.jsonp({ error: 'Only 120 characters are allowed in the name.' }) if (name.length > 120) return res.jsonp({ error: 'Only 120 characters are allowed in the name.' })
@ -350,15 +350,15 @@ app.post('/dashboard/link', authed, async (req, res) => {
url.indexOf('<') !== -1 || url.indexOf('>') !== -1) return res.jsonp({ error: 'HTML tags are forbidden!' }) url.indexOf('<') !== -1 || url.indexOf('>') !== -1) return res.jsonp({ error: 'HTML tags are forbidden!' })
// Validate URL // Validate URL
let a = URL.parse(url) const a = new URL(url)
if (a.protocol === null || a.host === null || a.slashes !== true) return res.jsonp({ error: 'Invalid URL!' }) if (a.protocol === '' || a.host === '') return res.jsonp({ error: 'Invalid URL!' })
// Checks // Checks
let db = await dbPromise const db = await dbPromise
let links = await db.all('SELECT * FROM link WHERE uuid = ?', user) const links = await db.all('SELECT * FROM link WHERE uuid = ?', user)
if (links.length > 10) return res.jsonp({ error: 'You can currently only add up to 10 links!' }) if (links.length > 10) return res.jsonp({ error: 'You can currently only add up to 10 links!' })
let link = await db.get('SELECT * FROM link WHERE url = ? AND uuid = ?', url, user) const link = await db.get('SELECT * FROM link WHERE url = ? AND uuid = ?', url, user)
if (link) return res.jsonp({ error: 'This URL already exists!' }) if (link) return res.jsonp({ error: 'This URL already exists!' })
// Add // Add
@ -368,12 +368,12 @@ app.post('/dashboard/link', authed, async (req, res) => {
// Remove link URL // Remove link URL
app.post('/dashboard/link/delete', authed, async (req, res) => { app.post('/dashboard/link/delete', authed, async (req, res) => {
let user = req.user.uuid const user = req.user.uuid
if (req.body.name == null && req.body.url == null) return res.jsonp({ error: 'Missing parameters!' }) if (req.body.name == null && req.body.url == null) return res.jsonp({ error: 'Missing parameters!' })
// Check // Check
let db = await dbPromise const db = await dbPromise
let link = await db.get('SELECT * FROM link WHERE url = ? AND uuid = ?', req.body.url, user) let link = await db.get('SELECT * FROM link WHERE url = ? AND uuid = ?', req.body.url, user)
if (!link) { if (!link) {
link = await db.get('SELECT * FROM link WHERE name = ? AND uuid = ?', req.body.name, user) link = await db.get('SELECT * FROM link WHERE name = ? AND uuid = ?', req.body.name, user)
@ -397,11 +397,11 @@ app.get('/player/:name', (req, res) => {
// Public data // Public data
app.get('/api/channel/:name', async (req, res) => { app.get('/api/channel/:name', async (req, res) => {
let name = req.params.name const name = req.params.name
let db = await dbPromise const db = await dbPromise
let data = await db.get('SELECT user_uuid,name,live_at,last_stream FROM channels WHERE name=?', name) const data = await db.get('SELECT user_uuid,name,live_at,last_stream FROM channels WHERE name=?', name)
if (!data) return res.jsonp({ error: 'No such channel!' }) if (!data) return res.jsonp({ error: 'No such channel!' })
let links = await db.all('SELECT name,url FROM link WHERE uuid = ?', data.user_uuid) const links = await db.all('SELECT name,url FROM link WHERE uuid = ?', data.user_uuid)
delete data.user_uuid delete data.user_uuid
data.live = data.live_at != null data.live = data.live_at != null
@ -424,7 +424,7 @@ app.use((error, req, res, next) => {
wss.on('connection', (ws, request, client) => { wss.on('connection', (ws, request, client) => {
let userId = request.session.id let userId = request.session.id
let username = 'A Friendly Guest' let username = 'A Friendly Guest'
let myChannels = [] const myChannels = []
if (request.user) { if (request.user) {
userId = request.user.uuid userId = request.user.uuid
@ -434,8 +434,8 @@ wss.on('connection', (ws, request, client) => {
dev && console.log(userId, 'connected') dev && console.log(userId, 'connected')
ws.on('message', (msg) => { ws.on('message', (msg) => {
dev && console.log(userId, 'said', msg) dev && console.log(userId, 'said', msg)
let is = msg.toString().trim().split(' ') const is = msg.toString().trim().split(' ')
let chan = is[1] const chan = is[1]
if (!chan) return if (!chan) return
switch (is[0]) { switch (is[0]) {
case 'watch': case 'watch':
@ -461,9 +461,9 @@ wss.on('connection', (ws, request, client) => {
ws.on('close', () => { ws.on('close', () => {
dev && console.log(userId, 'disconnected') dev && console.log(userId, 'disconnected')
for (let i in myChannels) { for (const i in myChannels) {
let chan = myChannels[i] const chan = myChannels[i]
let viewers = cache.viewers[chan] const viewers = cache.viewers[chan]
if (viewers && viewers[userId]) delete cache.viewers[chan][userId] if (viewers && viewers[userId]) delete cache.viewers[chan][userId]
} }
}) })
@ -492,11 +492,15 @@ const host = dev ? '0.0.0.0' : '127.0.0.1'
server.listen(port, host, () => { server.listen(port, host, () => {
// Get currently live channels, for example, when server restarted while someone was live // Get currently live channels, for example, when server restarted while someone was live
(async function () { (async function () {
let db = await dbPromise const db = await dbPromise
let allLive = await db.all('SELECT name FROM channels WHERE live_at IS NOT NULL') await db.migrate()
for (let i in allLive) {
const allLive = await db.all('SELECT name FROM channels WHERE live_at IS NOT NULL')
for (const i in allLive) {
cache.live.push(allLive[i].name) cache.live.push(allLive[i].name)
} }
console.log(`=> Found ${cache.live.length} channels still live`) console.log(`=> Found ${cache.live.length} channels still live`)
})().catch(e => console.error(e.stack)) })().catch(e => console.error(e.stack))

5537
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,30 +12,31 @@
"serve": "NODE_ENV=\"development\" node app.js" "serve": "NODE_ENV=\"development\" node app.js"
}, },
"devDependencies": { "devDependencies": {
"bootstrap": "^4.3.1", "bootstrap": "^4.5.0",
"copy-webpack-plugin": "^5.0.2", "copy-webpack-plugin": "^6.0.1",
"file-loader": "^1.1.11", "file-loader": "^6.0.0",
"hls.js": "^0.10.1", "hls.js": "^0.13.2",
"jquery": "^3.4.1", "jquery": "^3.5.1",
"popper.js": "^1.14.4", "popper.js": "^1.16.1",
"webpack": "^4.16.4", "webpack": "^4.43.0",
"webpack-command": "^0.4.1" "webpack-command": "^0.5.0"
}, },
"dependencies": { "dependencies": {
"body-parser": "^1.18.3", "body-parser": "^1.19.0",
"connect-redis": "^4.0.3", "connect-redis": "^4.0.4",
"ejs": "^2.6.1", "ejs": "^3.1.3",
"express": "^4.16.4", "express": "^4.17.1",
"express-async-errors": "^3.1.1", "express-async-errors": "^3.1.1",
"express-session": "^1.16.1", "express-session": "^1.17.1",
"nunjucks": "^3.2.0", "nunjucks": "^3.2.1",
"passport": "^0.4.0", "passport": "^0.4.1",
"redis": "^2.8.0", "redis": "^3.0.2",
"request": "^2.88.0", "request": "^2.88.2",
"sqlite": "^3.0.3", "sqlite": "^4.0.9",
"sqlite3": "^4.2.0",
"toml": "^3.0.0", "toml": "^3.0.0",
"uuid": "^3.3.2", "uuid": "^8.1.0",
"ws": "^7.2.0", "ws": "^7.3.0",
"xml2js": "^0.4.22" "xml2js": "^0.4.23"
} }
} }

View File

@ -3,21 +3,21 @@ import $ from 'jquery'
// https://stackoverflow.com/a/18650828 // https://stackoverflow.com/a/18650828
function formatBytes (a, b) { function formatBytes (a, b) {
if (a === 0) return '0 Bytes' if (a === 0) return '0 Bytes'
let c = 1024 const c = 1024
let d = b || 2 const d = b || 2
let e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] const e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
let f = Math.floor(Math.log(a) / Math.log(c)) const f = Math.floor(Math.log(a) / Math.log(c))
return parseFloat((a / Math.pow(c, f)).toFixed(d)) + ' ' + e[f] return parseFloat((a / Math.pow(c, f)).toFixed(d)) + ' ' + e[f]
} }
function recursiveStats (table, subtable) { function recursiveStats (table, subtable) {
for (let key in table) { for (const key in table) {
let val = table[key] let val = table[key]
if (typeof val === 'object') { if (typeof val === 'object') {
recursiveStats(val, key) recursiveStats(val, key)
} else { } else {
if (key === 'time') { if (key === 'time') {
let date = new Date(null) const date = new Date(null)
date.setSeconds(Math.floor(parseInt(val) / 1000)) // specify value for SECONDS here date.setSeconds(Math.floor(parseInt(val) / 1000)) // specify value for SECONDS here
val = date.toISOString().substr(11, 8) val = date.toISOString().substr(11, 8)
} else if (key.indexOf('bytes') !== -1) { } else if (key.indexOf('bytes') !== -1) {
@ -35,8 +35,8 @@ function updateLinkList (k) {
$('#link-list').html('<tbody></tbody>') $('#link-list').html('<tbody></tbody>')
$('#link-list tbody').append('<tr><th>Name</th><th>URL</th><th>Action</th></tr>') $('#link-list tbody').append('<tr><th>Name</th><th>URL</th><th>Action</th></tr>')
for (let i in res) { for (const i in res) {
let p = res[i] const p = res[i]
p.name = p.name.replace(/\</g, '&lt;').replace(/\>/g, '&gt;') p.name = p.name.replace(/\</g, '&lt;').replace(/\>/g, '&gt;')
$('#link-list tbody').append('<tr data-url="' + p.url + '"><td>' + p.name + '</td><td>' + $('#link-list tbody').append('<tr data-url="' + p.url + '"><td>' + p.name + '</td><td>' +
'<a href="' + p.url + '" target="_blank" rel="nofollow">' + '<a href="' + p.url + '" target="_blank" rel="nofollow">' +
@ -45,7 +45,7 @@ function updateLinkList (k) {
$('.delete-link').click(function (e) { $('.delete-link').click(function (e) {
e.preventDefault() e.preventDefault()
let pr = $(this).parent().parent().attr('data-url') const pr = $(this).parent().parent().attr('data-url')
$.post('/dashboard/link/delete', { url: pr }, updateLinkList) $.post('/dashboard/link/delete', { url: pr }, updateLinkList)
}) })
}) })
@ -58,9 +58,11 @@ function dashboard (k) {
return return
} }
let fullURL = window.location.origin + '/watch/' + res.name const fullURL = window.location.origin + '/watch/' + res.name
const sourceURL = window.location.origin + '/live/' + res.name + '.m3u8'
$('#myStream').attr('src', fullURL) $('#myStream').attr('src', fullURL)
$('#stream_url').text(fullURL).attr('href', fullURL) $('#stream_url').text(fullURL).attr('href', fullURL)
$('#source_url').text(sourceURL).attr('href', sourceURL)
$('#stream_live').text(res.live ? 'Yes' : 'No') $('#stream_live').text(res.live ? 'Yes' : 'No')
}) })
@ -70,7 +72,7 @@ function dashboard (k) {
}) })
$('.go-page').click(function (e) { $('.go-page').click(function (e) {
let el = $(this) const el = $(this)
$('.go-page').removeClass('active') $('.go-page').removeClass('active')
el.addClass('active') el.addClass('active')
$('.page:visible').fadeOut(function () { $('.page:visible').fadeOut(function () {
@ -80,8 +82,8 @@ function dashboard (k) {
$('#add-link').submit(function (e) { $('#add-link').submit(function (e) {
e.preventDefault() e.preventDefault()
let name = $('input[name="name"]').val() const name = $('input[name="name"]').val()
let url = $('input[name="url"]').val() const url = $('input[name="url"]').val()
if (name.length > 120) return alert('Only 120 characters are allowed in the name.') if (name.length > 120) return alert('Only 120 characters are allowed in the name.')

View File

@ -2,21 +2,21 @@
import Hls from 'hls.js' import Hls from 'hls.js'
// Elements // Elements
let player = document.querySelector('.livecnt') const player = document.querySelector('.livecnt')
let vid = player.querySelector('#stream') const vid = player.querySelector('#stream')
let overlay = player.querySelector('.overlay') const overlay = player.querySelector('.overlay')
let btn = overlay.querySelector('#playbtn') const btn = overlay.querySelector('#playbtn')
let time = overlay.querySelector('#duration') const time = overlay.querySelector('#duration')
let fullscreenbtn = overlay.querySelector('#fullscrbtn') const fullscreenbtn = overlay.querySelector('#fullscrbtn')
let playbtn = overlay.querySelector('#playbtn') const playbtn = overlay.querySelector('#playbtn')
let mutebtn = overlay.querySelector('#mutebtn') const mutebtn = overlay.querySelector('#mutebtn')
let lstat = overlay.querySelector('.live') const lstat = overlay.querySelector('.live')
let opts = overlay.querySelector('.controls') const opts = overlay.querySelector('.controls')
let bigbtn = overlay.querySelector('.bigplaybtn') const bigbtn = overlay.querySelector('.bigplaybtn')
let volumebar = overlay.querySelector('#volume_seek') const volumebar = overlay.querySelector('#volume_seek')
let volumeseek = volumebar.querySelector('.seeker') const volumeseek = volumebar.querySelector('.seeker')
let volumeseekInner = volumeseek.querySelector('.seekbar') const volumeseekInner = volumeseek.querySelector('.seekbar')
let viewers = overlay.querySelector('.viewers') const viewers = overlay.querySelector('.viewers')
let links let links
let linksList let linksList
@ -39,7 +39,7 @@ function GET (url, istext) {
if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
resolve(xmlHttp.responseText) resolve(xmlHttp.responseText)
} else if (xmlHttp.readyState === 4 && xmlHttp.status >= 400) { } else if (xmlHttp.readyState === 4 && xmlHttp.status >= 400) {
let err = new Error(xmlHttp.status) const err = new Error(xmlHttp.status)
err.request = xmlHttp err.request = xmlHttp
reject(err) reject(err)
} }
@ -105,9 +105,9 @@ function handleWebSocket () {
if (!vid.paused) ws.send('watch ' + STREAM_NAME) if (!vid.paused) ws.send('watch ' + STREAM_NAME)
ws.onmessage = function (event) { ws.onmessage = function (event) {
if (!event) return if (!event) return
let message = event.data const message = event.data
if (message.indexOf('viewlist ') === 0) { if (message.indexOf('viewlist ') === 0) {
let str = message.substring(9) const str = message.substring(9)
let list = str.split(',') let list = str.split(',')
if (str === '') list = [] if (str === '') list = []
viewersCount(list) viewersCount(list)
@ -160,8 +160,8 @@ function resetHide () {
} }
function updateTime () { function updateTime () {
let minutes = Math.floor(vid.currentTime / 60) const minutes = Math.floor(vid.currentTime / 60)
let seconds = Math.floor(vid.currentTime - minutes * 60) const seconds = Math.floor(vid.currentTime - minutes * 60)
time.innerHTML = minutes + ':' + (seconds < 10 ? '0' + seconds : seconds) time.innerHTML = minutes + ':' + (seconds < 10 ? '0' + seconds : seconds)
} }
@ -178,7 +178,7 @@ function toggleStream () {
} }
function toggleSound () { function toggleSound () {
let muteicon = mutebtn.querySelector('.fa') const muteicon = mutebtn.querySelector('.fa')
if (vid.muted) { if (vid.muted) {
vid.muted = false vid.muted = false
muteicon.className = 'fa fa-volume-up fa-fw' muteicon.className = 'fa fa-volume-up fa-fw'
@ -270,11 +270,11 @@ volumeseek.addEventListener('click', (e) => {
updateVolume() updateVolume()
}) })
let mousewheelevt = (/Firefox/i.test(navigator.userAgent)) ? 'DOMMouseScroll' : 'mousewheel' const mousewheelevt = (/Firefox/i.test(navigator.userAgent)) ? 'DOMMouseScroll' : 'mousewheel'
volumebar.addEventListener(mousewheelevt, (e) => { volumebar.addEventListener(mousewheelevt, (e) => {
e.preventDefault() e.preventDefault()
let scrollAmnt = (e.wheelDelta == null ? e.detail * -40 : e.wheelDelta) const scrollAmnt = (e.wheelDelta == null ? e.detail * -40 : e.wheelDelta)
if (scrollAmnt < 0) { if (scrollAmnt < 0) {
vid.volume = clampAddition(-0.1) vid.volume = clampAddition(-0.1)
@ -325,8 +325,8 @@ function getPosition(el) {
while (el) { while (el) {
if (el.tagName == 'BODY') { if (el.tagName == 'BODY') {
// deal with browser quirks with body/window/document and page scroll // deal with browser quirks with body/window/document and page scroll
let xScrollPos = el.scrollLeft || document.documentElement.scrollLeft const xScrollPos = el.scrollLeft || document.documentElement.scrollLeft
let yScrollPos = el.scrollTop || document.documentElement.scrollTop const yScrollPos = el.scrollTop || document.documentElement.scrollTop
xPosition += (el.offsetLeft - xScrollPos + el.clientLeft) xPosition += (el.offsetLeft - xScrollPos + el.clientLeft)
yPosition += (el.offsetTop - yScrollPos + el.clientTop) yPosition += (el.offsetTop - yScrollPos + el.clientTop)
@ -349,8 +349,8 @@ function hideOnClickOutside (element) {
} }
const outsideClickListener = event => { const outsideClickListener = event => {
if ((!element.contains(event.target) && isVisible(element)) if ((!element.contains(event.target) && isVisible(element)) &&
&& (event.target !== links && !links.contains(event.target))) { (event.target !== links && !links.contains(event.target))) {
element.style.display = 'none' element.style.display = 'none'
removeClickListener() removeClickListener()
} }
@ -390,7 +390,7 @@ function updateLinks (srcs) {
hideOnClickOutside(linksList) hideOnClickOutside(linksList)
let pos = getPosition(links) const pos = getPosition(links)
linksList.style = 'display: block;' linksList.style = 'display: block;'
pos.x -= linksList.offsetWidth - links.offsetWidth / 2 pos.x -= linksList.offsetWidth - links.offsetWidth / 2
pos.y -= linksList.offsetHeight pos.y -= linksList.offsetHeight
@ -401,9 +401,9 @@ function updateLinks (srcs) {
links.style.display = 'block' links.style.display = 'block'
linksList.innerHTML = '' linksList.innerHTML = ''
for (let i in srcs) { for (const i in srcs) {
let link = srcs[i] const link = srcs[i]
let el = document.createElement('a') const el = document.createElement('a')
el.href = link.url el.href = link.url
el.innerText = link.name el.innerText = link.name
el.target = '_blank' el.target = '_blank'
@ -413,7 +413,7 @@ function updateLinks (srcs) {
function getStreamStatus () { function getStreamStatus () {
GET('/api/channel/' + STREAM_NAME).then((data) => { GET('/api/channel/' + STREAM_NAME).then((data) => {
let jd = JSON.parse(data) const jd = JSON.parse(data)
if (jd.error) { if (jd.error) {
errored = true errored = true
return alert(jd.error) return alert(jd.error)

View File

@ -78,12 +78,21 @@
<div class="row"> <div class="row">
<div class="col-2"> <div class="col-2">
<label>Stream Link</label> <label>Stream URL</label>
</div> </div>
<div class="col"> <div class="col">
<a href="" id="stream_url"></a> <a href="" id="stream_url"></a>
</div> </div>
</div> </div>
<div class="row">
<div class="col-2">
<label>Source URL</label>
</div>
<div class="col">
<a href="" id="source_url"></a>
</div>
</div>
</p> </p>
<h1 class="h2">Metrics</h1> <h1 class="h2">Metrics</h1>

View File

@ -14,8 +14,8 @@ module.exports = {
rules: [] rules: []
}, },
plugins: [ plugins: [
new CopyPlugin([ new CopyPlugin({
{ patterns: [{
from: 'src/css/*.css', from: 'src/css/*.css',
to: 'css', to: 'css',
flatten: true flatten: true
@ -24,7 +24,7 @@ module.exports = {
from: 'node_modules/bootstrap/dist/css/bootstrap.min.css', from: 'node_modules/bootstrap/dist/css/bootstrap.min.css',
to: 'css', to: 'css',
flatten: true flatten: true
} }]
]), })
], ]
} }