This commit is contained in:
Evert Prants 2018-08-06 16:16:24 +03:00
commit 164439d3a7
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
14 changed files with 6606 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/config.ini
/node_modules/
/dist/
/venv/
*.db
*.pyc

280
app.py Normal file
View 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

File diff suppressed because it is too large Load Diff

25
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 &copy; 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
View 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
View 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" }
]
}
};