Port the application to node.js. I am not confident using Python so I went with the better option for me
This commit is contained in:
parent
8201e932fc
commit
8cbf946715
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,6 +1,8 @@
|
||||
/config.ini
|
||||
config.*
|
||||
!config.example.*
|
||||
/node_modules/
|
||||
/dist/
|
||||
/venv/
|
||||
/demo/
|
||||
*.db
|
||||
*.pyc
|
||||
|
376
app.js
Normal file
376
app.js
Normal file
@ -0,0 +1,376 @@
|
||||
const express = require('express')
|
||||
const session = require('express-session')
|
||||
const bodyParser = require('body-parser')
|
||||
const request = require('request')
|
||||
const nunjucks = require('nunjucks')
|
||||
const sqlite = require('sqlite')
|
||||
const xml2js = require('xml2js')
|
||||
const path = require('path')
|
||||
const toml = require('toml')
|
||||
const fs = require('fs')
|
||||
const uuid = require('uuid/v4')
|
||||
|
||||
require('express-async-errors')
|
||||
|
||||
const util = require('util')
|
||||
const get = util.promisify(request.get)
|
||||
const post = util.promisify(request.post)
|
||||
|
||||
const dev = process.env.NODE_ENV === 'development'
|
||||
|
||||
// Load Configuration
|
||||
const filename = path.join(__dirname, 'config.toml')
|
||||
|
||||
let config
|
||||
let cache = { _updated: 0, streamers: {} }
|
||||
|
||||
try {
|
||||
config = toml.parse(fs.readFileSync(filename))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
config = Object.assign({
|
||||
'Streaming': {
|
||||
'Port': '9322',
|
||||
'Database': 'streaming.db',
|
||||
'StreamServer': 'https://tv.icynet.eu/live/',
|
||||
'ServerHost': 'icynet.eu',
|
||||
'PublishAddress': 'rtmp://{host}:1935/hls-live/{streamer}',
|
||||
'Secret': 'changeme'
|
||||
},
|
||||
'Auth': {
|
||||
'Server': 'http://localhost:8282',
|
||||
'Redirect': 'http://localhost:5000/auth/_callback/'
|
||||
},
|
||||
'OAuth2': {
|
||||
'ClientID': '1',
|
||||
'ClientSecret': 'changeme'
|
||||
}
|
||||
}, config)
|
||||
|
||||
// Constants
|
||||
const oauthAuth = '{server}/oauth2/authorize?response_type=code&state={state}&redirect_uri={redirect}&client_id={client}&scope=image'
|
||||
const port = parseInt(config['Streaming']['Port'])
|
||||
const streamServer = config['Streaming']['StreamServer']
|
||||
const streamServerHost = config['Streaming']['ServerHost']
|
||||
const authServer = config['Auth']['Server']
|
||||
const oauthRedirect = config['Auth']['Redirect']
|
||||
const oauthId = config['OAuth2']['ClientID'].toString()
|
||||
const oauthSecret = config['OAuth2']['ClientSecret']
|
||||
const streamAppName = streamServer.match(/\/([\w-_]+)\/$/)[1]
|
||||
|
||||
function teval (str, obj) {
|
||||
let res = str + ''
|
||||
for (let key in obj) {
|
||||
if (res.indexOf('{' + key + '}') === -1) continue
|
||||
res = res.replace('{' + key + '}', obj[key])
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Database
|
||||
const dbPromise = Promise.resolve()
|
||||
.then(() => sqlite.open(path.join(process.cwd(), config['Streaming']['Database']), { Promise, cache: true }))
|
||||
.then(db => db.migrate())
|
||||
|
||||
// Setup server
|
||||
let app = express()
|
||||
|
||||
app.enable('trust proxy', 1)
|
||||
|
||||
app.use(bodyParser.urlencoded({ extended: false }))
|
||||
app.use(bodyParser.json())
|
||||
|
||||
app.disable('x-powered-by')
|
||||
|
||||
nunjucks.configure('templates', {
|
||||
autoescape: true,
|
||||
express: app
|
||||
})
|
||||
|
||||
app.use(session({
|
||||
secret: config['Streaming']['Secret'],
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
secure: !dev,
|
||||
maxAge: 2678400000 // 1 month
|
||||
}
|
||||
}))
|
||||
|
||||
// Parse stream metrics from the stat.xml file
|
||||
async function pullMetrics (uuid) {
|
||||
let statPath = streamServer + 'stat'
|
||||
if (!cache.stats || cache._updated < Date.now() - 5000) {
|
||||
let { body } = await get(statPath)
|
||||
let rip = await xml2js.parseStringPromise(body)
|
||||
if (!rip.rtmp.server) throw new Error('Invalid response from server.')
|
||||
|
||||
// Autofind the correct server
|
||||
let rtmpserver = rip.rtmp.server[0].application
|
||||
let rtmpapp
|
||||
for (let i in rtmpserver) {
|
||||
if (rtmpserver[i].name[0] !== streamAppName) continue
|
||||
rtmpapp = rtmpserver[i]
|
||||
}
|
||||
|
||||
if (!rtmpapp) throw new Error('Invalid response from server.')
|
||||
|
||||
cache.stats = rtmpapp.live
|
||||
cache._updated = Date.now()
|
||||
}
|
||||
|
||||
// Extract applicable stream data
|
||||
let forUser
|
||||
for (let i in cache.stats) {
|
||||
if (!cache.stats[i].stream) continue
|
||||
if (cache.stats[i].stream[0].name[0] !== uuid) continue
|
||||
forUser = cache.stats[i].stream[0]
|
||||
}
|
||||
|
||||
if (!forUser) return null
|
||||
|
||||
// Generic data object
|
||||
let data = {
|
||||
time: forUser.time[0],
|
||||
bytes: forUser.bytes_in[0],
|
||||
video: null,
|
||||
audio: null
|
||||
}
|
||||
|
||||
// Add video metadata, if applicable
|
||||
if (forUser.meta[0].video[0] !== '') {
|
||||
data['video'] = {
|
||||
width: forUser.meta[0].video[0].width[0],
|
||||
height: forUser.meta[0].video[0].height[0],
|
||||
frame_rate: forUser.meta[0].video[0].frame_rate[0],
|
||||
codec: forUser.meta[0].video[0].codec[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Add audio metadata, if applicable
|
||||
if (forUser.meta[0].audio[0] !== '') {
|
||||
data['audio'] = {
|
||||
sample_rate: forUser.meta[0].audio[0].sample_rate[0],
|
||||
channels: forUser.meta[0].audio[0].channels[0],
|
||||
codec: forUser.meta[0].audio[0].codec[0]
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// Handle requests from nginx-rtmp-module
|
||||
|
||||
app.post('/publish', async (req, res) => {
|
||||
if (!req.body.name) throw new Error('Invalid request.')
|
||||
let db = await dbPromise
|
||||
|
||||
// Validate stream key
|
||||
let streamer = await db.get('SELECT * FROM channels WHERE key=?', req.body.name)
|
||||
|
||||
if (!streamer) throw new Error('Invalid stream key.')
|
||||
console.log('Streamer %s has started streaming!', streamer.name)
|
||||
|
||||
// Generate real publish address for the server
|
||||
let publishAddress = config['Streaming']['PublishAddress']
|
||||
.replace('{streamer}', streamer.name)
|
||||
.replace('{host}', '127.0.0.1')
|
||||
|
||||
// Set channel streaming status
|
||||
db.run('UPDATE channels SET live_at=? WHERE id=?', Date.now(), streamer.id)
|
||||
|
||||
// Redirect the streaming server to the target
|
||||
res.set('Location', publishAddress)
|
||||
res.status(302)
|
||||
res.end()
|
||||
})
|
||||
|
||||
app.post('/publish_done', async (req, res) => {
|
||||
if (!req.body.name) throw new Error('Invalid request.')
|
||||
|
||||
let db = await dbPromise
|
||||
db.run('UPDATE channels SET live_at=NULL, last_stream=? WHERE key=?', Date.now(), req.body.name)
|
||||
|
||||
res.send('OK')
|
||||
})
|
||||
|
||||
// Front-end server
|
||||
// OAuth2 authenticator
|
||||
app.get('/login', async (req, res) => {
|
||||
if (req.session.user) return res.redirect('/')
|
||||
req.session.state = uuid()
|
||||
res.redirect(teval(oauthAuth, { state: req.session.state, redirect: oauthRedirect, client: oauthId, server: authServer }))
|
||||
})
|
||||
|
||||
app.get('/auth/_callback', async (req, res) => {
|
||||
let state = req.session.state
|
||||
if (!state) throw new Error('Something went wrong!')
|
||||
|
||||
let code = req.query.code
|
||||
let provState = req.query.state
|
||||
if (!code || state !== provState) throw new Error('Something went wrong!')
|
||||
delete req.session.state
|
||||
|
||||
// Aquire token
|
||||
let { body } = await post(authServer + '/oauth2/token', {
|
||||
form: {
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
redirect_uri: oauthRedirect,
|
||||
client_id: oauthId,
|
||||
client_secret: oauthSecret
|
||||
},
|
||||
auth: {
|
||||
user: oauthId,
|
||||
pass: oauthSecret
|
||||
}
|
||||
})
|
||||
|
||||
if (!body) throw new Error('Could not obtain access token!')
|
||||
try {
|
||||
body = JSON.parse(body)
|
||||
} catch (e) {
|
||||
console.error(e, body)
|
||||
throw new Error('Authorization server gave us an invalid response!')
|
||||
}
|
||||
|
||||
if (body['error']) {
|
||||
throw new Error(body['error'] + ': ' + body['error_description'])
|
||||
}
|
||||
|
||||
let token = body.access_token
|
||||
|
||||
// Get user information
|
||||
let { body: bodyNew } = await get(authServer + '/oauth2/user', { auth: { bearer: token } })
|
||||
try {
|
||||
bodyNew = JSON.parse(bodyNew)
|
||||
} catch (e) {
|
||||
console.error(e, bodyNew)
|
||||
throw new Error('Authorization server gave us an invalid response for user!')
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
let db = await dbPromise
|
||||
let user = await db.get('SELECT * FROM signed_users WHERE uuid=?', bodyNew.uuid)
|
||||
if (!user) {
|
||||
await db.run('INSERT INTO signed_users (uuid,name) VALUES (?,?)', bodyNew.uuid, bodyNew.username)
|
||||
}
|
||||
|
||||
req.session.login = bodyNew.uuid
|
||||
req.session.username = bodyNew.username
|
||||
|
||||
// Lets see if this user is a streamer
|
||||
let streamer = await db.get('SELECT * FROM channels WHERE user_uuid = ?', bodyNew.uuid)
|
||||
if (streamer) cache.streamers[bodyNew.uuid] = streamer
|
||||
|
||||
res.redirect('/')
|
||||
})
|
||||
|
||||
app.get('/logout', (req, res) => {
|
||||
req.session.destroy()
|
||||
res.redirect('/')
|
||||
})
|
||||
|
||||
// Views
|
||||
|
||||
app.use('/dist', express.static(path.join(__dirname, 'dist'), { maxAge: dev ? 0 : 2678400000 }))
|
||||
app.use(async function (req, res, next) {
|
||||
req.isStreamer = false
|
||||
if (!req.session.login) return next()
|
||||
|
||||
res.locals.session = { uuid: req.session.login, username: req.session.username }
|
||||
|
||||
if (cache.streamers[req.session.login]) {
|
||||
req.isStreamer = true
|
||||
return next()
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
// Index
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index.html', { streamer: req.isStreamer })
|
||||
})
|
||||
|
||||
// Dashboard
|
||||
app.get('/dashboard', (req, res, next) => {
|
||||
if (!req.isStreamer) return next(new Error('Unauthorized'))
|
||||
let stream = cache.streamers[req.session.login]
|
||||
res.render('dashboard.html', { stream: stream.key, server: 'rtmp://' + streamServerHost + '/live/' })
|
||||
})
|
||||
|
||||
// Stats
|
||||
app.get('/dashboard/stats', async (req, res) => {
|
||||
if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' })
|
||||
let stream = cache.streamers[req.session.login]
|
||||
let data
|
||||
|
||||
try {
|
||||
data = await pullMetrics(stream.key)
|
||||
} catch (e) {
|
||||
return res.jsonp({ error: e.message })
|
||||
}
|
||||
|
||||
if (!data) return res.jsonp({ error: 'No data was returned.' })
|
||||
res.jsonp(data)
|
||||
})
|
||||
|
||||
// Data
|
||||
app.get('/dashboard/data', async (req, res) => {
|
||||
if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' })
|
||||
let stream = cache.streamers[req.session.login]
|
||||
let data
|
||||
|
||||
let db = await dbPromise
|
||||
|
||||
try {
|
||||
data = await db.get('SELECT * FROM channels WHERE key=?', stream.key)
|
||||
} catch (e) {
|
||||
return res.jsonp({ error: 'Unauthorized' })
|
||||
}
|
||||
|
||||
if (!data) return res.jsonp({ error: 'Unauthorized' })
|
||||
|
||||
res.jsonp({
|
||||
'name': data.name,
|
||||
'key': stream.key,
|
||||
'uuid': req.session.login,
|
||||
'live': data.live_at != null,
|
||||
'live_at': new Date(parseInt(data.live_at)),
|
||||
'last_stream': new Date(parseInt(data.last_stream))
|
||||
})
|
||||
})
|
||||
|
||||
// Player
|
||||
app.get('/watch/:name', (req, res) => {
|
||||
res.render('player.html', { name: req.params.name, server: streamServer })
|
||||
})
|
||||
|
||||
app.get('/player/:name', (req, res) => {
|
||||
res.redirect('/watch/' + req.params.name)
|
||||
})
|
||||
|
||||
// Public data
|
||||
app.get('/api/channel/:name', async (req, res) => {
|
||||
let name = req.params.name
|
||||
let db = await dbPromise
|
||||
let data = await db.get('SELECT name,live_at,last_stream FROM channels WHERE name=?', name)
|
||||
if (!data) return res.jsonp({ error: 'No such channel!' })
|
||||
data.live = data.live_at != null
|
||||
data.live_at = new Date(parseInt(data.live_at))
|
||||
data.last_stream = new Date(parseInt(data.last_stream))
|
||||
res.jsonp(data)
|
||||
})
|
||||
|
||||
// Error handler
|
||||
app.use((error, req, res, next) => {
|
||||
if (dev) console.error(error.stack)
|
||||
res.send(error.message)
|
||||
})
|
||||
|
||||
// Start server
|
||||
const host = dev ? '0.0.0.0' : '127.0.0.1'
|
||||
app.listen(port, host, () => console.log('Listening on %s:%d', host, port))
|
16
config.example.toml
Normal file
16
config.example.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[Streaming]
|
||||
Port = 5000
|
||||
Database = "streaming.db"
|
||||
StreamServer = "http://localhost:5000/live/"
|
||||
ServerHost = "localhost:1935"
|
||||
PublishAddress = "rtmp://{host}:1935/hls-live/{streamer}"
|
||||
Secret = "changeme"
|
||||
|
||||
[Auth]
|
||||
Server = "http://localhost:8282"
|
||||
Redirect = "http://localhost:5000/auth/_callback/"
|
||||
|
||||
[OAuth2]
|
||||
ClientID = 1
|
||||
ClientSecret = "hackme"
|
||||
|
20
migrations/001_initial.sql
Normal file
20
migrations/001_initial.sql
Normal file
@ -0,0 +1,20 @@
|
||||
-- Up
|
||||
CREATE TABLE channels (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"live_at" TEXT,
|
||||
"password" TEXT,
|
||||
"user_uuid" TEXT
|
||||
, "last_stream" TEXT);
|
||||
|
||||
CREATE TABLE signed_users (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"uuid" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Down
|
||||
|
||||
DROP TABLE channels;
|
||||
DROP TABLE signed_users;
|
93
nginx.example.conf
Normal file
93
nginx.example.conf
Normal file
@ -0,0 +1,93 @@
|
||||
|
||||
worker_processes 1;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root html;
|
||||
try_files $uri @distrib;
|
||||
}
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root html;
|
||||
}
|
||||
|
||||
location /live/stat {
|
||||
rtmp_stat all;
|
||||
}
|
||||
|
||||
location /live {
|
||||
types {
|
||||
application/vnd.apple.mpegurl m3u8;
|
||||
video/mp2t ts;
|
||||
}
|
||||
|
||||
root /tmp;
|
||||
add_header Cache-Control no-cache;
|
||||
add_header Access-Control-Allow-Origin $http_origin;
|
||||
}
|
||||
|
||||
location @distrib {
|
||||
proxy_pass http://localhost:5000;
|
||||
proxy_redirect off;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rtmp {
|
||||
server {
|
||||
listen 1935;
|
||||
chunk_size 4096;
|
||||
publish_time_fix off;
|
||||
|
||||
# Entry application
|
||||
# The streamer publishes here through a streaming software with their key
|
||||
# The authenticator will give the streamer a proper name on hls-live
|
||||
application live {
|
||||
# enable live streaming
|
||||
live on;
|
||||
record off;
|
||||
|
||||
allow play 127.0.0.1;
|
||||
deny play all;
|
||||
|
||||
on_publish 'http://127.0.0.1:9321/publish';
|
||||
on_publish_done 'http://127.0.0.1:9321/publish_done';
|
||||
|
||||
push rtmp://127.0.0.1:1935/hls-live;
|
||||
}
|
||||
|
||||
# Output application
|
||||
application hls-live {
|
||||
# enable live streaming
|
||||
live on;
|
||||
|
||||
allow publish 127.0.0.1;
|
||||
deny publish all;
|
||||
|
||||
hls on;
|
||||
hls_path /tmp/live;
|
||||
hls_fragment 2s;
|
||||
hls_cleanup on;
|
||||
hls_sync 100ms;
|
||||
hls_playlist_length 60;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2047
package-lock.json
generated
2047
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "icytv",
|
||||
"version": "1.0.0",
|
||||
"description": "Frontend builder",
|
||||
"version": "2.0.0",
|
||||
"description": "IcyTV - nginx-rtmp-server authenticator",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@ -10,17 +10,26 @@
|
||||
"build": "webpack -p"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bootstrap": "^4.1.3",
|
||||
"bootstrap": "^4.3.1",
|
||||
"copy-webpack-plugin": "^5.0.2",
|
||||
"css-loader": "^1.0.0",
|
||||
"file-loader": "^1.1.11",
|
||||
"font-awesome": "^4.7.0",
|
||||
"hls.js": "^0.10.1",
|
||||
"jquery": "^3.3.1",
|
||||
"jquery": "^3.4.1",
|
||||
"popper.js": "^1.14.4",
|
||||
"style-loader": "^0.21.0",
|
||||
"url-loader": "^1.0.1",
|
||||
"webpack": "^4.16.4",
|
||||
"webpack-command": "^0.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.18.3",
|
||||
"ejs": "^2.6.1",
|
||||
"express": "^4.16.4",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"express-session": "^1.16.1",
|
||||
"nunjucks": "^3.2.0",
|
||||
"request": "^2.88.0",
|
||||
"sqlite": "^3.0.3",
|
||||
"toml": "^3.0.0",
|
||||
"uuid": "^3.3.2",
|
||||
"xml2js": "^0.4.22"
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import $ from 'jquery'
|
||||
|
||||
// https://stackoverflow.com/a/18650828
|
||||
function formatBytes (a, b) {
|
||||
if (0 == a) return '0 Bytes'
|
||||
if (a === 0) return '0 Bytes'
|
||||
let c = 1024
|
||||
let d = b || 2
|
||||
let e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
@ -13,7 +13,7 @@ function formatBytes (a, b) {
|
||||
function recursiveStats (table, subtable) {
|
||||
for (let key in table) {
|
||||
let val = table[key]
|
||||
if (typeof(val) == 'object') {
|
||||
if (typeof val === 'object') {
|
||||
recursiveStats(val, key)
|
||||
} else {
|
||||
if (key === 'time') {
|
||||
@ -41,7 +41,8 @@ function dashboard (k) {
|
||||
$('#stream_live').text(res.live ? 'Yes' : 'No')
|
||||
})
|
||||
|
||||
$('#show_key').click(function () {
|
||||
$('#show_key').click(function (e) {
|
||||
e.preventDefault()
|
||||
$('#show_key').html(k)
|
||||
})
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* global alert, XMLHttpRequest, STREAM_SERVER, STREAM_NAME */
|
||||
import Hls from 'hls.js'
|
||||
|
||||
// Elements
|
||||
@ -7,6 +8,7 @@ let overlay = player.querySelector('.overlay')
|
||||
let btn = overlay.querySelector('#playbtn')
|
||||
let time = overlay.querySelector('#duration')
|
||||
let fullscreenbtn = overlay.querySelector('#fullscrbtn')
|
||||
let playbtn = overlay.querySelector('#playbtn')
|
||||
let mutebtn = overlay.querySelector('#mutebtn')
|
||||
let lstat = overlay.querySelector('.live')
|
||||
let opts = overlay.querySelector('.controls')
|
||||
@ -22,6 +24,27 @@ let retryTimeout
|
||||
let vidReady = false
|
||||
let shouldHide = true
|
||||
let inFullscreen = false
|
||||
let errored = false
|
||||
|
||||
function GET (url, istext) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var xmlHttp = new XMLHttpRequest()
|
||||
|
||||
xmlHttp.onreadystatechange = function () {
|
||||
if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
|
||||
resolve(xmlHttp.responseText)
|
||||
} else if (xmlHttp.readyState === 4 && xmlHttp.status >= 400) {
|
||||
let err = new Error(xmlHttp.status)
|
||||
err.request = xmlHttp
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
xmlHttp.open('GET', url, true)
|
||||
istext && (xmlHttp.responseType = 'text')
|
||||
xmlHttp.send(null)
|
||||
})
|
||||
}
|
||||
|
||||
function clampAddition (val) {
|
||||
let volume = vid.volume
|
||||
@ -204,17 +227,20 @@ volumebar.addEventListener(mousewheelevt, (e) => {
|
||||
updateVolume()
|
||||
}, false)
|
||||
|
||||
function loadSource () {
|
||||
if (!hls) return
|
||||
hls.loadSource(STREAM_SERVER + STREAM_NAME + '.m3u8')
|
||||
}
|
||||
|
||||
if (Hls.isSupported()) {
|
||||
hls = new Hls()
|
||||
hls.loadSource(STREAM_SERVER + STREAM_NAME + '.m3u8')
|
||||
hls.attachMedia(vid)
|
||||
loadSource()
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
vidReady = true
|
||||
liveStatus(true)
|
||||
})
|
||||
hls.on(Hls.Events.ERROR, (e) => {
|
||||
vidReady = false
|
||||
liveStatus(false)
|
||||
|
||||
if (!vid.paused) {
|
||||
toggleStream()
|
||||
@ -225,6 +251,24 @@ if (Hls.isSupported()) {
|
||||
alert('Your browser does not support HLS streaming!')
|
||||
}
|
||||
|
||||
function getStreamStatus () {
|
||||
GET('/api/channel/' + STREAM_NAME).then((data) => {
|
||||
let jd = JSON.parse(data)
|
||||
if (jd.error) {
|
||||
errored = true
|
||||
return alert(jd.error)
|
||||
}
|
||||
|
||||
if (jd.live && !vidReady) loadSource()
|
||||
liveStatus(jd.live)
|
||||
}, (e) => {
|
||||
errored = true
|
||||
liveStatus(false)
|
||||
})
|
||||
|
||||
if (!errored) setTimeout(getStreamStatus, 8000)
|
||||
}
|
||||
|
||||
playbtn.addEventListener('click', toggleStream)
|
||||
mutebtn.addEventListener('click', toggleSound)
|
||||
fullscreenbtn.addEventListener('click', toggleFullscreen)
|
||||
@ -235,3 +279,5 @@ document.addEventListener('fullscreenchange', exitHandler, false)
|
||||
document.addEventListener('MSFullscreenChange', exitHandler, false)
|
||||
|
||||
vid.addEventListener('timeupdate', updateTime, false)
|
||||
|
||||
getStreamStatus()
|
||||
|
@ -6,8 +6,8 @@
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/dist/css/player.css">
|
||||
<script type="text/javascript">
|
||||
const STREAM_SERVER = "{{ server }}"
|
||||
const STREAM_NAME = "{{ name }}"
|
||||
const STREAM_SERVER = '{{ server }}'
|
||||
const STREAM_NAME = '{{ name }}'
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
Reference in New Issue
Block a user