git
This commit is contained in:
commit
164439d3a7
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/config.ini
|
||||
/node_modules/
|
||||
/dist/
|
||||
/venv/
|
||||
*.db
|
||||
*.pyc
|
280
app.py
Normal file
280
app.py
Normal file
@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env python3
|
||||
from flask import Flask, request, make_response, session, redirect, render_template, send_from_directory, jsonify
|
||||
from time import gmtime, strftime
|
||||
from uuid import uuid4
|
||||
|
||||
import requests
|
||||
import json
|
||||
import sqlite3
|
||||
import base64
|
||||
import configparser
|
||||
import os
|
||||
|
||||
"""
|
||||
SQLite3 Database Schema
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
d2c45910-8eb8-11e8-b357-b10f0028b927
|
||||
"""
|
||||
|
||||
# Load configuration
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
config['Streaming'] = {
|
||||
'Database': 'streaming.db'
|
||||
'StreamServer': 'https://tv.icynet.eu/live/',
|
||||
'ServerHost': 'icynet.eu',
|
||||
'PublishAddress': 'rtmp://{host}:1935/hls-live/{streamer}',
|
||||
}
|
||||
config['Auth'] = {
|
||||
'Server': 'http://localhost:8282',
|
||||
'Redirect': 'http://localhost:5000/auth/_callback/',
|
||||
}
|
||||
config['OAuth2'] = {
|
||||
'ClientID': '1',
|
||||
'ClientSecret': 'changeme',
|
||||
}
|
||||
|
||||
if os.path.exists('config.ini'):
|
||||
config.read('config.ini')
|
||||
|
||||
with open('config.ini', 'w') as configfile:
|
||||
config.write(configfile)
|
||||
|
||||
# App settings
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
auth_image = "{server}/api/avatar/{uuid}"
|
||||
oauth_auth = "{server}/oauth2/authorize?response_type=code&state={state}&redirect_uri={redirect}&client_id={client}&scope=image"
|
||||
stream_server = config['Streaming']['StreamServer']
|
||||
auth_server = config['Auth']['Server']
|
||||
oauth_redirect = config['Auth']['Redirect']
|
||||
oauth_id = int(config['OAuth2']['ClientID'])
|
||||
oauth_secret = config['OAuth2']['ClientSecret']
|
||||
|
||||
# Database
|
||||
conn = sqlite3.connect(config['Streaming']['Database'], check_same_thread=False)
|
||||
|
||||
# Streamer Cache
|
||||
stream_cache = {}
|
||||
|
||||
# Check if user is a streamer
|
||||
def valid_streamer(uuid):
|
||||
streamer = None
|
||||
|
||||
if 'uuid' in session:
|
||||
if session['uuid'] in stream_cache:
|
||||
streamer = stream_cache[session['uuid']]
|
||||
else:
|
||||
# Find key in database
|
||||
data = conn.execute('SELECT * FROM channels WHERE user_uuid=?', (session['uuid'],))
|
||||
row = data.fetchone()
|
||||
|
||||
# Deny stream publish
|
||||
if row:
|
||||
streamer = row[2]
|
||||
|
||||
return streamer
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
streamer = False
|
||||
if 'uuid' in session:
|
||||
if valid_streamer(session['uuid']):
|
||||
streamer = True
|
||||
|
||||
return render_template("index.html", streamer = streamer)
|
||||
|
||||
@app.route("/dashboard")
|
||||
def dashboard():
|
||||
if not 'uuid' in session:
|
||||
return redirect("/login", code = 302)
|
||||
|
||||
streamkey = valid_streamer(session['uuid'])
|
||||
if not streamkey:
|
||||
return make_response("Unauthorized.", 402)
|
||||
|
||||
return render_template("dashboard.html", stream = streamkey,
|
||||
server = config['Streaming']['PublishAddress'].format(streamer = "", host = config['Streaming']['ServerHost']))
|
||||
|
||||
@app.route("/dashboard/data")
|
||||
def dashboard_data():
|
||||
if not 'uuid' in session:
|
||||
return jsonify({'error': 'Unauthorized'})
|
||||
|
||||
streamkey = valid_streamer(session['uuid'])
|
||||
if not streamkey:
|
||||
return jsonify({'error': 'Unauthorized'})
|
||||
|
||||
# Find key in database
|
||||
data = conn.execute('SELECT * FROM channels WHERE key=?', (streamkey,))
|
||||
row = data.fetchone()
|
||||
|
||||
if not row:
|
||||
return jsonify({'error': 'Unauthorized'})
|
||||
|
||||
livedate = row[3]
|
||||
|
||||
data = {
|
||||
'name': row[1],
|
||||
'key': streamkey,
|
||||
'uuid': session['uuid'],
|
||||
'live': livedate != None,
|
||||
'live_at': livedate,
|
||||
'last_stream': row[6],
|
||||
}
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
# Called when starting publishing
|
||||
@app.route("/publish", methods=["POST"])
|
||||
def publish():
|
||||
print(json.dumps(request.form, ensure_ascii=False))
|
||||
|
||||
streamkey = request.form["name"]
|
||||
|
||||
# Find key in database
|
||||
data = conn.execute('SELECT * FROM channels WHERE key=?', (streamkey,))
|
||||
row = data.fetchone()
|
||||
|
||||
# Deny stream publish
|
||||
if not row:
|
||||
return make_response("Request Denied", 400)
|
||||
|
||||
streamer = row[1]
|
||||
|
||||
print("Streamer %s has started streaming!" % streamer)
|
||||
|
||||
# Redirect stream publish to stream name
|
||||
url = config['Streaming']['PublishAddress'].format(streamer = streamer, host = "127.0.0.1")
|
||||
|
||||
response = make_response(url, 302)
|
||||
response.headers["Location"] = url
|
||||
|
||||
# Update database with stream timestamp
|
||||
starttime = strftime("%Y-%m-%d %H:%M:%S", gmtime())
|
||||
|
||||
conn.execute('UPDATE channels SET live_at=? WHERE id=?', (starttime, row[0]))
|
||||
conn.commit()
|
||||
|
||||
return response
|
||||
|
||||
# Called when stopped publishing
|
||||
@app.route("/publish_done", methods=["POST"])
|
||||
def publish_done():
|
||||
streamkey = request.form["name"]
|
||||
|
||||
# Update database with stream end time
|
||||
endtime = strftime("%Y-%m-%d %H:%M:%S", gmtime())
|
||||
|
||||
conn.execute('UPDATE channels SET live_at=NULL, last_stream=? WHERE key=?', (endtime, streamkey,))
|
||||
conn.commit()
|
||||
|
||||
return "OK"
|
||||
|
||||
@app.route("/login")
|
||||
def login():
|
||||
if 'uuid' in session:
|
||||
return make_response("Already authenticated as %s!" % session['username'])
|
||||
|
||||
state = str(uuid4())
|
||||
session['state'] = state
|
||||
|
||||
return redirect(oauth_auth.format(state = state, client = oauth_id, redirect = oauth_redirect, server = auth_server), code = 302)
|
||||
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect("/", code = 302)
|
||||
|
||||
@app.route("/auth/_callback/")
|
||||
def cb():
|
||||
if not 'state' in session:
|
||||
return make_response("Something went wrong!", 402)
|
||||
|
||||
code = request.args.get('code')
|
||||
state = request.args.get('state')
|
||||
|
||||
if session['state'] != state:
|
||||
return make_response("Something went wrong!", 402)
|
||||
|
||||
if not code:
|
||||
return make_response("Authorization denied by user", 402)
|
||||
|
||||
# Get access token
|
||||
r = requests.post(auth_server + "/oauth2/token", data = {
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': oauth_redirect,
|
||||
'client_id': oauth_id,
|
||||
'client_secret': oauth_secret
|
||||
}, headers = {
|
||||
'Authorization': 'Basic ' + str(base64.b64encode(bytes("%s:%s" % (oauth_id, oauth_secret), 'utf-8')))
|
||||
})
|
||||
|
||||
res_token = None
|
||||
try:
|
||||
res_token = r.json()
|
||||
except ValueError:
|
||||
return make_response("Something went wrong while getting an access token!")
|
||||
|
||||
if 'error' in res_token:
|
||||
return make_response("%s: %s" % (res_token['error'], res_token['error_description']), 500)
|
||||
|
||||
token = res_token['access_token']
|
||||
|
||||
# Get user info
|
||||
ru = requests.get(auth_server + "/oauth2/user", headers = {
|
||||
'Authorization': 'Bearer ' + token
|
||||
})
|
||||
|
||||
res_uinfo = None
|
||||
try:
|
||||
res_uinfo = ru.json()
|
||||
except ValueError:
|
||||
return make_response("Something went wrong while getting user information!")
|
||||
|
||||
udata = conn.execute('SELECT * FROM signed_users WHERE uuid=?', (res_uinfo['uuid'],))
|
||||
row = udata.fetchone()
|
||||
|
||||
if not row:
|
||||
conn.execute('INSERT INTO signed_users (uuid, name) VALUES (?, ?)', (res_uinfo['uuid'], res_uinfo['username']))
|
||||
conn.commit()
|
||||
|
||||
session['uuid'] = res_uinfo['uuid']
|
||||
session['username'] = res_uinfo['username']
|
||||
|
||||
return redirect("/", code = 302)
|
||||
|
||||
@app.route("/watch/<name>")
|
||||
def watch(name):
|
||||
return render_template("player.html", name = name, server = stream_server)
|
||||
|
||||
@app.route("/player/<name>")
|
||||
def watch_old(name):
|
||||
return redirect("/watch/%s" % name, code = 302)
|
||||
|
||||
@app.route("/dist/<path:path>")
|
||||
def dist(path):
|
||||
return send_from_directory("dist", path)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.secret_key = '00-wegrhr[gqw[er=1ew qwergfdq.///**+'
|
||||
app.config['SESSION_TYPE'] = 'redis'
|
||||
app.debug = True
|
||||
app.run()
|
5509
package-lock.json
generated
Normal file
5509
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
Normal file
25
package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "icytv",
|
||||
"version": "1.0.0",
|
||||
"description": "Frontend builder",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"watch": "webpack -w --mode=development --log-level=debug",
|
||||
"build": "webpack -p"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bootstrap": "^4.1.3",
|
||||
"css-loader": "^1.0.0",
|
||||
"file-loader": "^1.1.11",
|
||||
"font-awesome": "^4.7.0",
|
||||
"hls.js": "^0.10.1",
|
||||
"jquery": "^3.3.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"
|
||||
}
|
||||
}
|
93
src/css/dashboard.css
Normal file
93
src/css/dashboard.css
Normal file
@ -0,0 +1,93 @@
|
||||
body {
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
.feather {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sidebar
|
||||
*/
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100; /* Behind the navbar */
|
||||
padding: 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar-sticky {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 48px; /* Height of navbar */
|
||||
height: calc(100vh - 48px);
|
||||
padding-top: .5rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link .feather {
|
||||
margin-right: 4px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover .feather,
|
||||
.sidebar .nav-link.active .feather {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
font-size: .75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/*
|
||||
* Navbar
|
||||
*/
|
||||
|
||||
.navbar-brand {
|
||||
padding-top: .75rem;
|
||||
padding-bottom: .75rem;
|
||||
font-size: 1rem;
|
||||
background-color: rgba(0, 0, 0, .25);
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
|
||||
}
|
||||
|
||||
.navbar .form-control {
|
||||
padding: .75rem 1rem;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.form-control-dark {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
border-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
.form-control-dark:focus {
|
||||
border-color: transparent;
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
|
||||
}
|
||||
|
||||
/*
|
||||
* Utilities
|
||||
*/
|
||||
|
||||
.border-top { border-top: 1px solid #e5e5e5; }
|
||||
.border-bottom { border-bottom: 1px solid #e5e5e5; }
|
110
src/css/index.css
Normal file
110
src/css/index.css
Normal file
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Globals
|
||||
*/
|
||||
|
||||
/* Links */
|
||||
a,
|
||||
a:focus,
|
||||
a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Custom default button */
|
||||
.btn-secondary,
|
||||
.btn-secondary:hover,
|
||||
.btn-secondary:focus {
|
||||
color: #333;
|
||||
text-shadow: none; /* Prevent inheritance from `body` */
|
||||
background-color: #fff;
|
||||
border: .05rem solid #fff;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Base structure
|
||||
*/
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
background-color: #006289;
|
||||
}
|
||||
|
||||
body {
|
||||
display: -ms-flexbox;
|
||||
display: -webkit-box;
|
||||
display: flex;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-box-pack: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
|
||||
box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
.cover-container {
|
||||
max-width: 50em;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Header
|
||||
*/
|
||||
.masthead {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.masthead-brand {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nav-masthead .nav-link {
|
||||
padding: .25rem 0;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, .5);
|
||||
background-color: transparent;
|
||||
border-bottom: .25rem solid transparent;
|
||||
}
|
||||
|
||||
.nav-masthead .nav-link:hover,
|
||||
.nav-masthead .nav-link:focus {
|
||||
border-bottom-color: rgba(255, 255, 255, .25);
|
||||
}
|
||||
|
||||
.nav-masthead .nav-link + .nav-link {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.nav-masthead .active {
|
||||
color: #fff;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
|
||||
@media (min-width: 48em) {
|
||||
.masthead-brand {
|
||||
float: left;
|
||||
}
|
||||
.nav-masthead {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Cover
|
||||
*/
|
||||
.cover {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
.cover .btn-lg {
|
||||
padding: .75rem 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Footer
|
||||
*/
|
||||
.mastfoot {
|
||||
color: rgba(255, 255, 255, .5);
|
||||
}
|
119
src/css/player.css
Normal file
119
src/css/player.css
Normal file
@ -0,0 +1,119 @@
|
||||
body {
|
||||
font-family: Helvetica;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.livecnt {
|
||||
position: absolute;
|
||||
background-color: black;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.videobox {
|
||||
height: 100%;
|
||||
}
|
||||
#stream {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.live {
|
||||
position: absolute;
|
||||
color: white;
|
||||
font-size: 120%;
|
||||
margin: 20px;
|
||||
text-transform: uppercase;
|
||||
background-color: #F44336;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.live.offline {
|
||||
background-color: rgba(93, 93, 93, 0.7);
|
||||
}
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
transition: opacity .25s ease-in-out;
|
||||
-moz-transition: opacity .25s ease-in-out;
|
||||
-webkit-transition: opacity .25s ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
.overlay.hidden {
|
||||
opacity: 0;
|
||||
cursor: none;
|
||||
}
|
||||
.controls {
|
||||
background-color: rgba(0, 142, 255, 0.42);
|
||||
border-top: 2px solid #006cc1;
|
||||
color: white;
|
||||
height: 45px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.controls .inline {
|
||||
float: left;
|
||||
display: inline-block;
|
||||
}
|
||||
.controls .ilbtn {
|
||||
font-size: 200%;
|
||||
width: 40px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
#duration {
|
||||
line-height: 2.4;
|
||||
font-size: 110%;
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.controls .right {
|
||||
float: right;
|
||||
display: inline-block;
|
||||
}
|
||||
.bigplaybtn {
|
||||
color: #fff;
|
||||
font-size: 400%;
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
margin: auto;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
line-height: 1.2;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(3, 169, 244, 0.7);
|
||||
padding: 10px;
|
||||
border-radius: 100%;
|
||||
border: 5px dotted #2196F3;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bigplaybtn .fa {
|
||||
top: 6px;
|
||||
position: relative;
|
||||
left: 3px;
|
||||
}
|
||||
.bigplaybtn.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
.seekview .seeker {
|
||||
display: none;
|
||||
width: 120px;
|
||||
height: 10px;
|
||||
background-color: #9be0ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.seeker .seekbar {
|
||||
background-color: #00b0ff;
|
||||
height: 10px;
|
||||
}
|
||||
.seekview:hover > .seeker {
|
||||
display: inline-block;
|
||||
}
|
20
src/dashboard.js
Normal file
20
src/dashboard.js
Normal file
@ -0,0 +1,20 @@
|
||||
import $ from 'jquery'
|
||||
|
||||
function dashboard () {
|
||||
$.get("/dashboard/data", function (res) {
|
||||
if (res.error) {
|
||||
window.location.href = "/"
|
||||
return
|
||||
}
|
||||
|
||||
var fullURL = window.location.origin + "/player/" + res.name
|
||||
$('#myStream').attr('src', fullURL)
|
||||
$('#stream_url').text(fullURL).attr("href", fullURL)
|
||||
})
|
||||
|
||||
$('#show_key').click(function () {
|
||||
$('#show_key').html(window.STREAM_KEY)
|
||||
})
|
||||
}
|
||||
|
||||
export default {start: dashboard}
|
17
src/index.js
Normal file
17
src/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
import 'bootstrap'
|
||||
|
||||
// Style
|
||||
import bootstrap from 'bootstrap/dist/css/bootstrap.min.css'
|
||||
import dash from './css/dashboard.css'
|
||||
import index from './css/index.css'
|
||||
|
||||
// Components
|
||||
import dashboard from './dashboard.js'
|
||||
|
||||
bootstrap.ref()
|
||||
if (window.STREAM_KEY) {
|
||||
dash.ref()
|
||||
dashboard.start()
|
||||
} else {
|
||||
index.ref()
|
||||
}
|
248
src/player.js
Normal file
248
src/player.js
Normal file
@ -0,0 +1,248 @@
|
||||
import css from './css/player.css'
|
||||
import Hls from 'hls.js'
|
||||
|
||||
let player
|
||||
let vid
|
||||
let btn
|
||||
let bigbtn
|
||||
let overlay
|
||||
let time
|
||||
let fscrn
|
||||
let mutebtn
|
||||
let hideTimeout
|
||||
let hls
|
||||
let lstat
|
||||
let opts
|
||||
let volumebar
|
||||
let retry
|
||||
let vidReady = false
|
||||
let shouldHide = true
|
||||
let infscr = false
|
||||
|
||||
window.onload = function (argument) {
|
||||
player = document.querySelector('.livecnt')
|
||||
vid = player.querySelector('#stream')
|
||||
overlay = player.querySelector('.overlay')
|
||||
btn = overlay.querySelector('#playbtn')
|
||||
time = overlay.querySelector('#duration')
|
||||
fscrn = overlay.querySelector('#fullscrbtn')
|
||||
mutebtn = overlay.querySelector('#mutebtn')
|
||||
lstat = overlay.querySelector('.live')
|
||||
opts = overlay.querySelector('.controls')
|
||||
bigbtn = overlay.querySelector('.bigplaybtn')
|
||||
volumebar = overlay.querySelector('#volume_seek')
|
||||
|
||||
player.addEventListener('mousemove', resetHide)
|
||||
|
||||
opts.addEventListener('mouseenter', () => {
|
||||
shouldHide = false
|
||||
})
|
||||
|
||||
opts.addEventListener('mouseleave', () => {
|
||||
shouldHide = true
|
||||
})
|
||||
|
||||
opts.addEventListener('mousemove', () => {
|
||||
shouldHide = false
|
||||
})
|
||||
|
||||
bigbtn.addEventListener('click', () => {
|
||||
toggleStream()
|
||||
})
|
||||
|
||||
volumebar.addEventListener('click', (e) => {
|
||||
vid.volume = ((e.pageX - volumebar.offsetLeft) / volumebar.clientWidth)
|
||||
updateVolume()
|
||||
})
|
||||
|
||||
let mousewheelevt = (/Firefox/i.test(navigator.userAgent)) ? 'DOMMouseScroll' : 'mousewheel'
|
||||
|
||||
mutebtn.addEventListener(mousewheelevt, (e) => {
|
||||
e.preventDefault()
|
||||
let scrollAmnt = (e.wheelDelta == null ? e.detail * -40 : e.wheelDelta)
|
||||
|
||||
if (scrollAmnt < 0) {
|
||||
vid.volume = clampAddition(-0.1)
|
||||
} else {
|
||||
vid.volume = clampAddition(0.1)
|
||||
}
|
||||
|
||||
updateVolume()
|
||||
}, false)
|
||||
|
||||
if (Hls.isSupported()) {
|
||||
hls = new Hls()
|
||||
hls.loadSource(STREAM_SERVER + STREAM_NAME + '.m3u8')
|
||||
hls.attachMedia(vid)
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
vidReady = true
|
||||
liveStatus(true)
|
||||
})
|
||||
hls.on(Hls.Events.ERROR, (e) => {
|
||||
vidReady = false
|
||||
liveStatus(false)
|
||||
|
||||
if (!vid.paused) {
|
||||
toggleStream()
|
||||
resetHide()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
alert('Your browser does not support HLS streaming!')
|
||||
}
|
||||
|
||||
playbtn.addEventListener('click', toggleStream)
|
||||
mutebtn.addEventListener('click', toggleSound)
|
||||
fscrn.addEventListener('click', toggleFullscreen)
|
||||
|
||||
document.addEventListener('webkitfullscreenchange', exitHandler, false)
|
||||
document.addEventListener('mozfullscreenchange', exitHandler, false)
|
||||
document.addEventListener('fullscreenchange', exitHandler, false)
|
||||
document.addEventListener('MSFullscreenChange', exitHandler, false)
|
||||
|
||||
vid.addEventListener('timeupdate', updateTime)
|
||||
}
|
||||
|
||||
function clampAddition (val) {
|
||||
let volume = vid.volume
|
||||
|
||||
if (volume + val > 1) {
|
||||
volume = 1
|
||||
} else if (volume + val < 0) {
|
||||
volume = 0
|
||||
} else {
|
||||
volume += val
|
||||
}
|
||||
|
||||
return volume.toFixed(2)
|
||||
}
|
||||
|
||||
function showBigBtn (show) {
|
||||
if (show) {
|
||||
bigbtn.className = 'bigplaybtn'
|
||||
} else {
|
||||
bigbtn.className = 'bigplaybtn hidden'
|
||||
}
|
||||
}
|
||||
|
||||
function updateVolume () {
|
||||
let inner = volumebar.querySelector('.seekbar')
|
||||
inner.style.width = vid.volume * 100 + '%'
|
||||
}
|
||||
|
||||
function liveStatus (status) {
|
||||
if (status) {
|
||||
lstat.innerHTML = 'live now'
|
||||
lstat.className = 'live'
|
||||
clearTimeout(retry)
|
||||
if (vid.paused) {
|
||||
showBigBtn(true)
|
||||
}
|
||||
} else {
|
||||
lstat.innerHTML = 'offline'
|
||||
lstat.className = 'live offline'
|
||||
retry = setTimeout(() => {
|
||||
if (vidReady) return
|
||||
hls.loadSource(readFromURI())
|
||||
}, 10000)
|
||||
}
|
||||
}
|
||||
|
||||
function hide () {
|
||||
if (vid.paused || !shouldHide) {
|
||||
overlay.className = 'overlay'
|
||||
return
|
||||
}
|
||||
|
||||
overlay.className = 'overlay hidden'
|
||||
}
|
||||
|
||||
function resetHide () {
|
||||
overlay.className = 'overlay'
|
||||
clearTimeout(hideTimeout)
|
||||
|
||||
if (vid.paused) return
|
||||
if (!shouldHide) return
|
||||
|
||||
hideTimeout = setTimeout(() => {
|
||||
hide()
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
function updateTime () {
|
||||
let minutes = Math.floor(vid.currentTime / 60)
|
||||
let seconds = Math.floor(vid.currentTime - minutes * 60)
|
||||
time.innerHTML = minutes + ':' + (seconds < 10 ? '0' + seconds : seconds)
|
||||
}
|
||||
|
||||
function toggleStream () {
|
||||
if (!vid) return
|
||||
if (!vidReady) return
|
||||
if (vid.paused) {
|
||||
vid.play()
|
||||
btn.innerHTML = '<i class="fa fa-pause fa-fw"></i>'
|
||||
showBigBtn(false)
|
||||
} else {
|
||||
vid.pause()
|
||||
btn.innerHTML = '<i class="fa fa-play fa-fw"></i>'
|
||||
showBigBtn(true)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSound () {
|
||||
let muteicon = mutebtn.querySelector('.fa')
|
||||
if (vid.muted) {
|
||||
vid.muted = false
|
||||
muteicon.className = 'fa fa-volume-up fa-fw'
|
||||
} else {
|
||||
vid.muted = true
|
||||
muteicon.className = 'fa fa-volume-off fa-fw'
|
||||
}
|
||||
}
|
||||
|
||||
function exitHandler () {
|
||||
if (!(document.fullScreen || document.webkitIsFullScreen || document.mozFullScreen)) {
|
||||
infscr = false
|
||||
}
|
||||
|
||||
if (infscr) {
|
||||
fscrn.innerHTML = '<i class="fa fa-compress fa-fw"></i>'
|
||||
} else {
|
||||
fscrn.innerHTML = '<i class="fa fa-expand fa-fw"></i>'
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFullscreen () {
|
||||
if (vid.enterFullscreen) {
|
||||
if (!document.fullScreen) {
|
||||
player.requestFullScreen()
|
||||
infscr = true
|
||||
} else {
|
||||
document.cancelFullScreen()
|
||||
}
|
||||
} else if (vid.webkitEnterFullscreen) {
|
||||
if (!document.webkitIsFullScreen) {
|
||||
player.webkitRequestFullScreen()
|
||||
infscr = true
|
||||
} else {
|
||||
document.webkitCancelFullScreen()
|
||||
}
|
||||
} else if (vid.mozRequestFullScreen) {
|
||||
if (!document.mozFullScreen) {
|
||||
player.mozRequestFullScreen()
|
||||
infscr = true
|
||||
} else {
|
||||
document.mozCancelFullScreen()
|
||||
}
|
||||
} else {
|
||||
alert('Your browser doesn\'t support fullscreen!')
|
||||
}
|
||||
}
|
||||
|
||||
window.onblur = () => {
|
||||
shouldHide = true
|
||||
}
|
||||
|
||||
window.onfocus = () => {
|
||||
shouldHide = true
|
||||
}
|
75
templates/dashboard.html
Normal file
75
templates/dashboard.html
Normal file
@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="text/javascript">
|
||||
window.STREAM_KEY = "{{ stream }}"
|
||||
</script>
|
||||
<meta charset="utf-8">
|
||||
<title>IcyTV Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0">
|
||||
<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="/">IcyTV</a>
|
||||
<ul class="navbar-nav px-3">
|
||||
<li class="nav-item text-nowrap">
|
||||
<a class="nav-link" href="/logout">Sign out</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<nav class="col-md-2 d-none d-md-block bg-light sidebar">
|
||||
<div class="sidebar-sticky">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
||||
Dashboard <span class="sr-only">(current)</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<iframe allowfullscreen src="" id="myStream" width="1542" height="651" style="display: block; width: 1542px; height: 651px;"></iframe>
|
||||
<h1 class="h2">Information</h1>
|
||||
<p class="lead">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<label>Stream Server</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<span>{{ server }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<label>Stream Key</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<a href="#" id="show_key">Show Stream Key</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<label>Stream Link</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<a href="" id="stream_url"></a>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/dist/main.bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
44
templates/index.html
Normal file
44
templates/index.html
Normal file
@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>IcyTV Private Livestreaming Server</title>
|
||||
</head>
|
||||
<body class="text-center">
|
||||
<div class="cover-container d-flex h-100 p-3 mx-auto flex-column">
|
||||
<header class="masthead mb-auto">
|
||||
<div class="inner">
|
||||
<h3 class="masthead-brand">IcyTV</h3>
|
||||
<nav class="nav nav-masthead justify-content-center">
|
||||
<a class="nav-link active" href="/">Home</a>
|
||||
{% if session.uuid %}
|
||||
<a class="nav-link" href="/logout">{{ session.username }}</a>
|
||||
{% else %}
|
||||
<a class="nav-link" href="/login">Login</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main role="main" class="inner cover">
|
||||
<h1 class="cover-heading">IcyTV</h1>
|
||||
<p class="lead">This is a private livestreaming server for Icy Network members.</p>
|
||||
{% if streamer %}
|
||||
<p class="lead">Welcome back, {{ session.username }}!</p>
|
||||
<p class="lead">
|
||||
<a href="/dashboard" class="btn btn-lg btn-secondary">My Dashboard</a>
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="lead">Email me at <code>evert(at)lunasqu.ee</code> if you're interested.</p>
|
||||
{% endif %}
|
||||
</main>
|
||||
|
||||
<footer class="mastfoot mt-auto">
|
||||
<div class="inner">
|
||||
<p>Copyleft © 2018 - <a href="https://icynet.eu/" target="_blank">Icy Network</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="/dist/main.bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
35
templates/player.html
Normal file
35
templates/player.html
Normal file
@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>IcyTV - {{ name }}</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<script type="text/javascript">
|
||||
const STREAM_SERVER = "{{ server }}"
|
||||
const STREAM_NAME = "{{ name }}"
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="livecnt">
|
||||
<div class="videobox">
|
||||
<video id="stream"></video>
|
||||
</div>
|
||||
<div class="overlay">
|
||||
<div class="live offline">offline</div>
|
||||
<div class="bigplaybtn hidden"><i class="fa fa-play fa-fw"></i></div>
|
||||
<div class="controls">
|
||||
<div id="playbtn" class="inline ilbtn"><i class="fa fa-play fa-fw"></i></div>
|
||||
<span class="seekview">
|
||||
<div id="mutebtn" class="inline ilbtn"><i class="fa fa-volume-up fa-fw"></i></div>
|
||||
<div class="seeker" id="volume_seek">
|
||||
<div class="seekbar" style="width: 100%"></div>
|
||||
</div>
|
||||
</span>
|
||||
<div id="duration">0:00</div>
|
||||
<div id="fullscrbtn" class="right ilbtn"><i class="fa fa-expand fa-fw"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/dist/player.bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
25
webpack.config.js
Normal file
25
webpack.config.js
Normal file
@ -0,0 +1,25 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
main: './src/index.js',
|
||||
player: './src/player.js'
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: '[name].bundle.js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{ loader: "style-loader/useable" },
|
||||
{ loader: "css-loader" }
|
||||
]
|
||||
},
|
||||
{ test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url-loader?limit=10000&mimetype=application/font-woff" },
|
||||
{ test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader" }
|
||||
]
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user