Skip to content
Snippets Groups Projects
Commit 224ab350 authored by David Huss's avatar David Huss :speech_balloon:
Browse files

Implement basic single page autoload/autostop

parent 175b5254
No related branches found
No related tags found
No related merge requests found
...@@ -245,6 +245,13 @@ header h1 a:not(:first-of-type) { ...@@ -245,6 +245,13 @@ header h1 a:not(:first-of-type) {
margin-top: 24%; margin-top: 24%;
} }
body.inactive #stream{
background-color: black;
border: 1em solid black;
box-sizing: border-box;
min-height: 26em;
}
@keyframes slideInFromLeft { @keyframes slideInFromLeft {
0% { 0% {
transform: translateX(-100%); transform: translateX(-100%);
......
var socket = io(); var socket = io();
let hasEverRun = false;
let player = null;
// Extract foobar from the .stream-foobar key of an element // Extract foobar from the .stream-foobar key of an element
function extractStreamKey(e) { function extractStreamKey(e) {
...@@ -20,10 +22,23 @@ function hasStream(streamlist, k) { ...@@ -20,10 +22,23 @@ function hasStream(streamlist, k) {
return streamlist.some(({ key }) => key === k); return streamlist.some(({ key }) => key === k);
} }
// Returns the Stream from the streamlist
function getStream(streamlist, k) {
return streamlist.find(({ key }) => key === k);
}
// Send a message to the server when the socket is established // Send a message to the server when the socket is established
socket.on('connect', function() { socket.on('connect', function() {
let key = getStreamKey() let key = getStreamKey()
socket.emit('join', {"key" : key}); socket.emit('join', {"key" : key});
// Ask about the state of the stream
socket.emit('stream_info', {"key" : key});
});
// After initial connect, receive a streamlist
socket.on('stream_info', function(data) {
var stream = JSON.parse(data)
updateStream(stream, "update");
}); });
// Send a message to the server when the socket is established // Send a message to the server when the socket is established
...@@ -50,16 +65,21 @@ socket.on('stream_added', function(data) { ...@@ -50,16 +65,21 @@ socket.on('stream_added', function(data) {
var streamlist = JSON.parse(data["list"]) var streamlist = JSON.parse(data["list"])
let streamkey = getStreamKey(); let streamkey = getStreamKey();
if (hasStream(streamlist, streamkey)) { if (hasStream(streamlist, streamkey)) {
updateStream("activate", streamkey); let stream = getStream(streamlist, streamkey);
updateStream(stream, "added");
}else{
console.log("..but "+streamkey+" was not in streamlist");
console.log(streamlist);
} }
}); });
// New streamlist arrives here when webserver gets notivied of a stream removal // New streamlist arrives here when webserver gets notivied of a stream removal
socket.on('stream_removed', function(data) { socket.on('stream_removed', function(data) {
console.log('Stream ' + data['key'] + ' removed.'); console.log('Stream ' + data['key'] + ' removed.');
var streamlist = JSON.parse(data["list"])
let streamkey = getStreamKey(); let streamkey = getStreamKey();
updateStream("deactivate", streamkey); if (streamkey == data['key']) {
updateStream(streamkey, "removed");
}
}); });
...@@ -76,35 +96,227 @@ function updateViewCount(viewercount) { ...@@ -76,35 +96,227 @@ function updateViewCount(viewercount) {
} }
// Update the stream when it has been added // Update the stream when it has been added
function updateStream(what, streamkey) { function updateStream(stream, what) {
if (what == "update") {
console.log("Stream "+stream.key+" updated")
if (!stream.active) {
deactivateStream(stream.key);
}else{
hasEverRun = true;
}
} else if (what == "added") {
console.log("Stream "+stream.key+" has started");
hasEverRun = true;
activateStream(stream);
}else if (what == "removed") {
console.log("Stream "+stream+" has stopped");
deactivateStream(stream);
}
}
function deactivateStream(streamkey) {
// Add inactive class to body
document.body.classList.add("inactive");
// Pause the player
if (player !== null) {
player.pause();
}
// Get width and height from player
var videoPlayer = document.getElementById("stream");
var w = videoPlayer.offsetWidth;
var h = videoPlayer.offsetHeight;
if (what == "activate") { // Remove video player
console.log("Stream "+streamkey+" has started"); if (document.getElementById("stream") !== null) {
}else if (what == "deactivate") { document.querySelectorAll('#stream').forEach(e => e.remove());
console.log("Stream "+streamkey+" has stopped");
} }
if (player !== null) {
player.dispose();
} }
// Get parent element
let content = document.getElementsByTagName("content")[0];
// Create a div
let div = document.createElement("div");
div.id = "stream";
div.classList.add("stream-"+streamkey)
div.style.height = h+"px";
div.style.width = w+"px";
// Add a notice to it
let h2 = document.createElement("h2");
if (hasEverRun) {
h2.textContent = "The stream has ended"
h2.classList.add("stopped");
document.body.classList.add("stopped");
}else{
h2.textContent = "The stream hasn't started yet (or it doesn't exist)"
h2.classList.add("not_started");
document.body.classList.add("not-started");
}
h2.id = "no-stream-notice";
div.appendChild(h2);
content.prepend(div);
}
function activateStream(stream) {
document.body.classList.remove("inactive");
document.body.classList.remove("not-started");
document.body.classList.remove("stopped");
// Remove div placeholder
if (document.getElementById("stream") !== null) {
document.querySelectorAll('#stream').forEach(e => e.remove());
}
// TODO: Add description, ...
addPlayer(stream.key);
player = initializePlayer();
player.load();
player.play();
updateDescription(stream);
}
function updateDescription(stream) {
if (stream.description !== null && stream.description !== "") {
let descriptions = document.querySelectorAll('.description');
if (descriptions.length === 0) {
buildDescriptionBlock();
}
let description = document.querySelectorAll('.description')[0];
// TODO: handle markdown...?
description.textContent = stream.description;
}else{
// There was formerly a description which is now gone, so destroy description
document.querySelectorAll('.description').forEach(e => e.remove());
}
}
function buildDescriptionBlock() {
let content = document.getElementsByTagName("content")[0];
let section = document.createElement("section");
section.classList.add("description");
content.appendChild(section);
}
// When the window is loaded check for errors
window.onload = function() { window.onload = function() {
if (!document.body.classList.contains("inactive")){
player = initializePlayer();
}
// Run this block with a delay
setTimeout(function() { setTimeout(function() {
var player = document.getElementById("stream"); var videoPlayer = document.getElementById("stream");
if (player.classList.contains("vjs-error")) {
console.log("Contained Error"); if (!document.body.classList.contains("inactive")){
var intervalId = setInterval(checkIfStillErrored, 2000); // If there is an error try every two seconds if the player now finds the
// video. Once it is found, remove the interval
if (videoPlayer.classList.contains("vjs-error")) {
var intervalId = setInterval(function() {
checkIfStillErrored(intervalId) ;
}, 2000);
}
// Autoplay with delay if possible
player.play();
}else{
deactivateStream(getStreamKey());
}
}, 200);
}
function checkIfStillErrored(intervalId) {
var videoPlayer = document.getElementById("stream");
if (videoPlayer.classList.contains("vjs-error")) {
console.log("Stream still errored, trying to reload it");
player.pause();
player.load();
}else{
console.log("Error seems resolved");
player.load();
player.play();
clearInterval(intervalId); clearInterval(intervalId);
} }
}, 1000);
} }
function addPlayer(streamkey) {
// Get parent element
let content = document.getElementsByTagName("content")[0];
// Create player
let videojs = document.createElement("video-js");
videojs.id = "stream";
videojs.classList.add("vjs-default-skin", "stream-"+streamkey, );
videojs.setAttribute("data-setup", '{"fluid": true, "liveui": true}');
videojs.toggleAttribute('controls');
let source = document.createElement("source");
source.src = "../hls/"+streamkey+".m3u8";
source.type = "application/x-mpegURL"
videojs.prepend(source);
content.prepend(videojs)
}
function initializePlayer() {
var player = videojs('stream');
player.autoplay('any');
function checkIfStillErrored() { function displayMuteifNeeded(player) {
var player = document.getElementById("stream"); if (player.muted()){
if (player.classList.contains("vjs-error")) { let title = document.querySelector("#page_title");
console.log("Still errored"); if (title.querySelector(".muted") == null) {
location.reload(); let a = document.createElement("a");
let img = document.createElement("img");
img.src = "/static/mute.svg";
a.classList.add("muted");
a.onclick = function() {
player.muted(false);
displayMuteifNeeded(player);
};
a.appendChild(img);
title.appendChild(a);
}
}else{ }else{
console.log("Not errored anymore"); let title = document.querySelector("#page_title");
title.querySelectorAll('.muted').forEach(e => e.remove());
}
} }
player.on('play', () => {
displayMuteifNeeded(player);
});
player.on("volumechange",function(){
displayMuteifNeeded(player);
});
// player.on('error', () => {
// player.createModal('Retrying connection');
// if (player.error().code === 4) {
// this.player.retryLock = setTimeout(() => {
// player.src({
// src: data.url
// });
// player.load();
// }, 2000);
// }
// });
return player;
} }
\ No newline at end of file
...@@ -10,7 +10,7 @@ from flaskext.markdown import Markdown ...@@ -10,7 +10,7 @@ from flaskext.markdown import Markdown
from flask_socketio import SocketIO, join_room, leave_room from flask_socketio import SocketIO, join_room, leave_room
from .config import initialize_config, APPLICATION_NAME, DEFAULT_CONFIG from .config import initialize_config, APPLICATION_NAME, DEFAULT_CONFIG
from .streams import Stream, StreamList, value_to_flag from .streams import Stream, StreamList, value_to_flag, key_if_not_None
# Initialization # Initialization
...@@ -67,17 +67,21 @@ def stream(streamkey): ...@@ -67,17 +67,21 @@ def stream(streamkey):
# Strip potential trailing slashes # Strip potential trailing slashes
streamkey = streamkey.rstrip("/") streamkey = streamkey.rstrip("/")
stream = streamlist.get_stream(streamkey) stream = streamlist.get_stream(streamkey)
streamkey = key_if_not_None(stream, "key", that=streamkey)
description = key_if_not_None(stream, "description")
# Render a different Template if the stream is missing # Render a different Template if the stream is missing
if stream is None: if stream is None:
existed = False
# Stream was Missing, log warning # Stream was Missing, log warning
app.logger.warning("Looking for stream {}, but it didn't exist".format(streamkey)) running_since = None
return render_template("stream_missing.html", application_name=APPLICATION_NAME, page_title=config["application"]["page_title"], streamkey=streamkey, list_streams=config["application"]["list_streams"]), 404 app.logger.info("Client {} looked for non-existent stream {}".format(request.remote_addr, streamkey))
else: else:
app.logger.debug("Looking for {}/{}.m3u8".format(config["application"]["hls_path"], streamkey)) existed = True
app.logger.debug("Client requests stream {} ({}/{}.m3u8)".format(streamkey, config["application"]["hls_path"], streamkey))
running_since = humanize.naturaldelta(dt.timedelta(seconds=stream.active_since())) running_since = humanize.naturaldelta(dt.timedelta(seconds=stream.active_since()))
# Everything ok, return Stream # Everything ok, return Stream
return render_template('stream.html', application_name=APPLICATION_NAME, page_title=config["application"]["page_title"], hls_path=config["application"]["hls_path"], streamkey=stream.key, description=stream.description, running_since=running_since) return render_template('stream.html', application_name=APPLICATION_NAME, page_title=config["application"]["page_title"], hls_path=config["application"]["hls_path"], streamkey=streamkey, description=description, running_since=running_since, existed=existed)
@app.route('/', methods = ['GET']) @app.route('/', methods = ['GET'])
...@@ -168,17 +172,26 @@ def client_list_connected(): ...@@ -168,17 +172,26 @@ def client_list_connected():
socketio.emit('stream_list', {'list': json_list}) socketio.emit('stream_list', {'list': json_list})
@socketio.on('connect_single')
def client_single_connected(data):
if data is not None:
app.logger.info('Client (single page > {}) connected via socket.io'.format(data))
@socketio.on('stream_list') @socketio.on('stream_list')
def send_streamlist(): def send_streamlist():
json_list = streamlist.json_list() json_list = streamlist.json_list()
socketio.emit('stream_list', {'list': json_list}) socketio.emit('stream_list', {'list': json_list})
@socketio.on('stream_info')
def send_streaminfo(data):
if type(data) is dict and "key" in data.keys():
app.logger.info('Client wants info about stream {}'.format(data['key']))
key = data["key"]
stream = streamlist.get_stream(key)
if stream is not None:
json = stream.to_json()
app.logger.debug('Sending Stream info\n{}'.format(json))
socketio.emit('stream_info', json)
else:
app.logger.warning('Client {} asked for info on non-existing stream {}'.format(request.remote_addr, data['key']))
@socketio.on('join') @socketio.on('join')
def on_join(data): def on_join(data):
app.logger.info('Client connected to stream {}'.format(data['key'])) app.logger.info('Client connected to stream {}'.format(data['key']))
...@@ -187,6 +200,7 @@ def on_join(data): ...@@ -187,6 +200,7 @@ def on_join(data):
count = streamlist.add_viewer(key) count = streamlist.add_viewer(key)
socketio.emit('viewercount', {'count': count, 'direction': 'up'}, room=key) socketio.emit('viewercount', {'count': count, 'direction': 'up'}, room=key)
@socketio.on('leave') @socketio.on('leave')
def on_leave(data): def on_leave(data):
app.logger.info('Client left to stream {}'.format(data['key'])) app.logger.info('Client left to stream {}'.format(data['key']))
...@@ -195,5 +209,6 @@ def on_leave(data): ...@@ -195,5 +209,6 @@ def on_leave(data):
count = streamlist.remove_viewer(key) count = streamlist.remove_viewer(key)
socketio.emit('viewercount', {'count': count, 'direction': 'down'}, room=key) socketio.emit('viewercount', {'count': count, 'direction': 'down'}, room=key)
if __name__ == '__main__': if __name__ == '__main__':
socketio.run(app) socketio.run(app)
\ No newline at end of file
#!/usr/bin/env python #!/usr/bin/env python
#-*- coding: utf-8 -*- #-*- coding: utf-8 -*-
import json import json
import copy
from typing import Optional, NewType, List, Any from typing import Optional, NewType, List, Any
import datetime as dt import datetime as dt
...@@ -38,6 +39,25 @@ def none_if_no_key_value_otherwise(d: dict, key: str, that=None) -> Optional[Any ...@@ -38,6 +39,25 @@ def none_if_no_key_value_otherwise(d: dict, key: str, that=None) -> Optional[Any
return d[key] return d[key]
def key_if_not_None(o, key: str, that=None):
"""
Return the object.key or object["key"] if it exists,
otherwise return that (per default None)
"""
if o is None:
return that
elif type(o) is dict:
if key in o.keys():
return o[key]
else:
return that
else:
try:
return getattr(o, key)
except TypeError:
return that
def value_to_flag(value) -> bool: def value_to_flag(value) -> bool:
""" """
Return False if the value was None, otherwise return wether it was in the list Return False if the value was None, otherwise return wether it was in the list
...@@ -94,8 +114,19 @@ class Stream(): ...@@ -94,8 +114,19 @@ class Stream():
return "{} ({})".format(self.key, ", ".join(attributes)) return "{} ({})".format(self.key, ", ".join(attributes))
return "{}".format(self.key) return "{}".format(self.key)
def __iter__(self):
clone = copy.deepcopy(self)
del clone.protected
del clone.password
del clone.unlisted
for key in clone.__dict__:
yield key, getattr(clone, key)
def to_dict(self) -> dict:
return dict(self)
def to_json(self): def to_json(self):
return json.dumps(self, default=jsonconverter, return json.dumps(self.to_dict(), default=jsonconverter,
sort_keys=True, indent=4) sort_keys=True, indent=4)
@property @property
...@@ -480,10 +511,12 @@ class StreamList(): ...@@ -480,10 +511,12 @@ class StreamList():
return self return self
def jsonconverter(o): def jsonconverter(o):
if isinstance(o, dt.datetime): o = copy.deepcopy(o)
if isinstance(o, Stream):
# We use to_dict() here, to keep fields like password private : )
return o.to_dict()
elif isinstance(o, dt.datetime):
return o.__str__() return o.__str__()
else: else:
return o.__dict__ return o.__dict__
\ No newline at end of file
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if existed %}
<video-js id="stream" class="vjs-default-skin stream-{{ streamkey }}" data-setup='{"fluid": true, "liveui": true}' controls> <video-js id="stream" class="vjs-default-skin stream-{{ streamkey }}" data-setup='{"fluid": true, "liveui": true}' controls>
<source src="../hls/{{ streamkey }}.m3u8" type="application/x-mpegURL"> <source src="../hls/{{ streamkey }}.m3u8" type="application/x-mpegURL">
</video-js> </video-js>
...@@ -22,44 +23,21 @@ ...@@ -22,44 +23,21 @@
{{ description|markdown }} {{ description|markdown }}
</section> </section>
{% endif %} {% endif %}
{% else %}
<div id="stream" class="stream-{{ streamkey }}">
<h2 class="stopped" id="no-stream-notice">The stream hasn't started yet (or it doesn't exist)</h2>
</div>
{% endif %}
{% endblock %} {% endblock %}
{% block footer %} {% block footer %}
{{ super() }} {{ super() }}
<script src="{{ url_for('static', filename='video.min.js') }}"></script> {% if not existed %}
<script src="{{ url_for('static', filename='videojs-http-streaming.min.js') }}"></script>
<script> <script>
let player = videojs('stream'); document.body.classList.add("inactive");
player.autoplay('any');
function displayMuteifNeeded(player) {
if (player.muted()){
let title = document.querySelector("#page_title");
if (title.querySelector(".muted") == null) {
let a = document.createElement("a");
let img = document.createElement("img");
img.src = "{{ url_for('static', filename='mute.svg') }}";
a.classList.add("muted");
a.onclick = function() {
player.muted(false);
displayMuteifNeeded(player);
};
a.appendChild(img);
title.appendChild(a);
}
}else{
let title = document.querySelector("#page_title");
title.querySelectorAll('.muted').forEach(e => e.remove());
}
}
player.on('play', () => {
displayMuteifNeeded(player);
});
player.on("volumechange",function(){
displayMuteifNeeded(player);
});
</script> </script>
{% endif %}
<script src="{{ url_for('static', filename='video.min.js') }}"></script>
<script src="{{ url_for('static', filename='videojs-http-streaming.min.js') }}"></script>
<script src="{{ url_for('static', filename='sync-stream.js') }}"></script> <script src="{{ url_for('static', filename='sync-stream.js') }}"></script>
{% endblock %} {% endblock %}
{% extends "layout.html" %}
{% block title %}{{ page_title }}/{{ streamkey }}{% endblock %}
{% block head %}
<link href="{{ url_for('static', filename='video-js.css') }}" rel="stylesheet">
{{ super() }}
{% endblock %}
{% block header %}
<h1 id="page_title"><a href="/">{{ page_title }}</a>/<s>{{ streamkey }}</s></h1>
{% endblock %}
{% block content %}
<h2 class="404">Uh-oh!</h2>
<h2>The stream {{ streamkey }} doesn't seem to be here (anymore?)</h2>
{% if list_streams %}
<p>Have a look at the <a href="/streams">list of current streams</a></p>
{% endif %}
{% endblock %}
{% block footer %}
{{ super() }}
{% endblock %}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment