make js play nice when videojs is disabled

このコミットが含まれているのは:
n9k 2021-04-16 05:53:25 +00:00
コミット c6583aaf7d
5個のファイルの変更130行の追加71行の削除

ファイルの表示

@ -1,9 +1,11 @@
import os
import time
from website.constants import SEGMENTS_DIR, SEGMENT_INIT
from website.constants import HLS_TIME, SEGMENTS_DIR, SEGMENT_INIT, VIEW_COUNTING_PERIOD
from website.utils.stream import _is_segment, _segment_number, _get_segments
SEGMENT = 'stream{number}.m4s'
CORRUPTING_SEGMENT = 'corrupt.m4s'
STREAM_TIMEOUT = HLS_TIME + 2 # consider the stream offline after this many seconds without a new segment
# TODO: uncommment this if it becomes useful
#
@ -98,7 +100,18 @@ CORRUPTING_SEGMENT = 'corrupt.m4s'
# return chunk
def get_next_segment(after, segment_offset, stream_timeout):
def resolve_segment_offset(segment_offset=max(VIEW_COUNTING_PERIOD // HLS_TIME, 2)):
'''
Returns the number of the segment at `segment_offset` (1 is most recent segment)
'''
segments = _get_segments(sort=True)
try:
segment = segments[-min(segment_offset, len(segments))]
except IndexError:
raise FileNotFoundError
return _segment_number(segment)
def get_next_segment(after, start_segment):
start = time.time()
while True:
time.sleep(1)
@ -106,16 +119,13 @@ def get_next_segment(after, segment_offset, stream_timeout):
if after == None:
return SEGMENT_INIT
elif after == SEGMENT_INIT:
try:
return segments[-min(segment_offset, len(segments))]
except IndexError:
pass
return start_segment
else:
segments = filter(lambda segment: _segment_number(segment) > _segment_number(after), segments)
try:
return min(segments, key=_segment_number)
except ValueError:
if time.time() - start >= stream_timeout:
if time.time() - start >= STREAM_TIMEOUT:
print(f'SegmentUnavailable in get_next_segment; {after=}')
raise SegmentUnavailable
@ -124,25 +134,22 @@ class SegmentUnavailable(Exception):
class SegmentsIterator:
def __init__(self, segment_offset, stream_timeout, skip_init_segment=False):
self.segment_offset = segment_offset
self.stream_timeout = stream_timeout
def __init__(self, start_segment, skip_init_segment=False):
self.start_segment = start_segment
self.segment = SEGMENT_INIT if skip_init_segment else None
def __iter__(self):
return self
def __next__(self):
self.segment = get_next_segment(self.segment, self.segment_offset, self.stream_timeout)
self.segment = get_next_segment(self.segment, self.start_segment)
return self.segment
class ConcatenatedSegments:
def __init__(self, segment_offset=4, stream_timeout=24, segment_hook=None, corrupt_hook=None, should_close_connection=None):
# start this many segments back from now (1 is most recent segment)
self.segment_offset = segment_offset
# consider the stream offline after this many seconds without a new segment
self.stream_timeout = stream_timeout
def __init__(self, start_number, segment_hook=None, corrupt_hook=None, should_close_connection=None):
# start at this segment, after SEGMENT_INIT
self.start_number = start_number
# run this function after sending each segment
self.segment_hook = segment_hook or (lambda n: None)
# run this function when we send the corrupting segment
@ -150,16 +157,15 @@ class ConcatenatedSegments:
# run this function before reading files; if it returns True, then stop
self.should_close_connection = should_close_connection or (lambda: None)
self.segments = SegmentsIterator(segment_offset=self.segment_offset,
stream_timeout=self.stream_timeout)
start_segment = SEGMENT.format(number=start_number)
self.segments = SegmentsIterator(start_segment=start_segment)
self._closed = False
self.segment_read_offset = 0
try:
self.segment = next(self.segments)
except SegmentUnavailable:
print('SegmentUnavailable in ConcatenatedSegments.__init__')
self.close()
self.segment = next(self.segments)
if not os.path.isfile(os.path.join(SEGMENTS_DIR, start_segment)):
raise FileNotFoundError
def _read(self, n):
chunk = b''

ファイルの表示

@ -10,7 +10,7 @@ import website.chat as chat
import website.viewership as viewership
import website.utils.stream as stream
from website.constants import DIR_STATIC, DIR_STATIC_EXTERNAL, SEGMENT_INIT, CHAT_SCROLLBACK, BROADCASTER_COLOUR, BROADCASTER_TOKEN, SEGMENTS_DIR, VIEW_COUNTING_PERIOD, HLS_TIME, NOTES, N_NONE
from website.concatenate import ConcatenatedSegments
from website.concatenate import ConcatenatedSegments, resolve_segment_offset
viewers = viewership.viewers
@ -34,7 +34,12 @@ def index(token=None):
pass
use_videojs = bool(request.args.get('videojs', default=1, type=int))
viewership.made_request(token)
response = Response(render_template('index.html', token=token, use_videojs=use_videojs)) # TODO: add a view of the chat only, either as an arg here or another route
response = render_template('index.html',
token=token,
use_videojs=use_videojs,
start_number=resolve_segment_offset())
response = Response(response) # TODO: add a view of the chat only, either as an arg here or another route
response.set_cookie('token', token)
return response
@ -120,11 +125,22 @@ def segments():
viewership.video_was_corrupted.remove(token)
except KeyError:
pass
concatenated_segments = ConcatenatedSegments(segment_offset=max(VIEW_COUNTING_PERIOD // HLS_TIME, 2),
stream_timeout=HLS_TIME + 2,
segment_hook=lambda n: viewership.view_segment(n, token, check_exists=False),
corrupt_hook=lambda: viewership.video_was_corrupted.add(token), # lock?
should_close_connection=lambda: not stream.is_online())
start_number = request.args.get('segment', type=int)
if start_number == None:
try:
start_number = resolve_segment_offset()
except FileNotFoundError:
return abort(404)
try:
concatenated_segments = ConcatenatedSegments(start_number=start_number,
segment_hook=lambda n: viewership.view_segment(n, token, check_exists=False),
corrupt_hook=lambda: viewership.video_was_corrupted.add(token), # lock?
should_close_connection=lambda: not stream.is_online())
except FileNotFoundError:
return abort(404)
file_wrapper = wrap_file(request.environ, concatenated_segments)
response = Response(file_wrapper, mimetype='video/mp4')
response.headers['Cache-Control'] = 'no-store'
@ -160,12 +176,17 @@ def heartbeat():
viewership.made_request(token)
online = stream.is_online()
start_abs, start_rel = stream.get_start(absolute=True, relative=True)
return {'viewers': viewership.count(),
'online': online,
'current_segment': stream.current_segment(),
'title': stream.get_title(),
'start_abs': start_abs if online else None,
'start_rel': start_rel if online else None}
response = {'viewers': viewership.count(),
'online': online,
'current_segment': stream.current_segment(),
'title': stream.get_title(),
'start_abs': start_abs if online else None,
'start_rel': start_rel if online else None}
if token in viewership.video_was_corrupted:
response['corrupted'] = True
return response
@current_app.route('/comment-box')
def comment_iframe(token=None):

ファイルの表示

@ -7,6 +7,14 @@ const segmentThreshold = latencyThreshold / segmentDuration;
const heartbeatPeriod = 20.0; // seconds between heartbeats
const video = document.querySelector("video");
const videojsEnabled = parseInt(document.getElementById("videojs-enabled").value);
let firstSegment;
if ( !videojsEnabled ) {
firstSegment = parseInt(/segment=(\d+)/.exec(video.src)[1]);
}
let token, streamTitle, viewerCount, streamStatus, streamLight, refreshButton, radialLoader;
let streamAbsoluteStart, streamRelativeStart, streamTimer, streamTimerLastUpdated;
@ -25,9 +33,11 @@ streamInfoFrame.addEventListener("load", function() {
streamStatus = streamInfo.getElementById("stream-status");
streamLight = streamInfo.getElementById("stream-light");
refreshButton = streamInfo.getElementById("refresh-button");
refreshStreamButton = streamInfo.getElementById("refresh-stream-button");
refreshPageButton = streamInfo.getElementById("refresh-page-button");
refreshButton.onclick = function() { return window.location.reload(true); };
refreshStreamButton.onclick = function() { refreshStreamButton.style.display = "none"; return video.load(); };
refreshPageButton.onclick = function() { return window.location.reload(true); };
streamTimer = streamInfo.getElementById("uptime");
streamAbsoluteStart = streamInfoFrame.contentWindow.streamAbsoluteStart;
@ -47,27 +57,46 @@ streamInfoFrame.addEventListener("load", function() {
});
function currentSegment() {
try {
let player = videojs.players.vjs_video_3;
if ( player == null ) {
player = videojs.players.videojs;
if ( videojsEnabled ) {
try {
let player = videojs.players.vjs_video_3;
if ( player == null ) {
player = videojs.players.videojs;
}
const tracks = player.textTracks();
const cues = tracks[0].cues;
const uri = cues[cues.length - 1].value.uri;
return parseInt(uri.split("/")[3].slice(6));
} catch ( error ) {
return null;
}
const tracks = player.textTracks();
const cues = tracks[0].cues;
const uri = cues[cues.length - 1].value.uri;
return parseInt(uri.split("/")[3].slice(6));
} catch ( error ) {
return null;
} else {
if ( video.readyState != video.HAVE_ENOUGH_DATA || video.networkState == video.NETWORK_IDLE ) {
return null;
}
return firstSegment + Math.floor(video.duration / segmentDuration);
}
}
function updateStreamStatus(msg, backgroundColor, showRefreshButton) {
function updateStreamStatus(msg, backgroundColor, refreshStream, refreshPage) {
// TODO: figure out why when the colour is yellow the stream light moves down a few pixels
streamStatus.innerHTML = msg;
streamLight.style.backgroundColor = backgroundColor;
if ( showRefreshButton ) {
refreshButton.style.display = null;
// doesn't work with videojs: there are errors in the console; probably there is a workaround
// could work with html5 video but we'd need to find what segment to start at; too complicated for now
refreshPage = refreshPage | refreshStream;
refreshStream = false;
if ( refreshStream ) {
refreshStreamButton.style.display = null;
} else {
refreshButton.style.display = "none";
refreshStreamButton.style.display = "none";
}
if ( refreshPage ) {
refreshPageButton.style.display = null;
} else {
refreshPageButton.style.display = "none";
}
}
@ -129,7 +158,7 @@ function heartbeat() {
xhr.onerror = function(e) {
heartIsBeating = false;
console.log(e);
updateStreamStatus("The stream was unreachable. Try refreshing the page.", "yellow", true);
updateStreamStatus("The stream was unreachable. Try refreshing the page.", "yellow", false, true);
}
xhr.ontimeout = xhr.onerror;
xhr.onload = function(e) {
@ -156,16 +185,16 @@ function heartbeat() {
// update stream status
if ( !response.online ) {
return updateStreamStatus("The stream has ended.", "red", false);
return updateStreamStatus("The stream has ended.", "red", false, false);
}
const serverSegment = response.current_segment;
if ( !Number.isInteger(serverSegment) ) {
return updateStreamStatus("The stream restarted. Refresh the page.", "yellow", true);
return updateStreamStatus("The stream restarted. Reload the stream.", "yellow", true, false);
}
if ( oldStreamAbsoluteStart != response.start_abs ) {
return updateStreamStatus("The stream restarted. Refresh the page.", "yellow", true);
return updateStreamStatus("The stream restarted. Reload the stream.", "yellow", true, false);
}
// when the page is first loaded clientSegment may be null
@ -173,16 +202,16 @@ function heartbeat() {
if ( Number.isInteger(clientSegment) ) {
const diff = serverSegment - clientSegment;
if ( diff >= segmentThreshold ) {
return updateStreamStatus(`You're more than ${latencyThreshold} seconds behind the stream. Refresh the page.`, "yellow", true);
return updateStreamStatus(`You're more than ${latencyThreshold} seconds behind the stream. Reload the stream.`, "yellow", true, false);
} else if ( diff < 0 ) {
return updateStreamStatus("The stream restarted. Refresh the page.", "yellow", true);
return updateStreamStatus("The stream restarted. Reload the stream.", "yellow", true, false);
}
} else if (Date.now() / 1000 - t0 >= playbackTimeout) {
return updateStreamStatus("The stream is online but you're not receiving it. Try refreshing the page.", "yellow", true);
return updateStreamStatus("The stream is online but you're not receiving it. Try refreshing the page.", "yellow", false, true);
}
// otherwise
return updateStreamStatus("The stream is online.", "green", false);
return updateStreamStatus("The stream is online.", "green", false, false);
}
xhr.send();

ファイルの表示

@ -49,6 +49,7 @@
<!-- TODO: choose which elements get borders more cleverly -->
<div id="stream" class="border">
{% if use_videojs %}
<input id="videojs-enabled" type="hidden" value="1">
<!-- https://stackoverflow.com/questions/41014197/how-can-i-play-a-m3u8-file-video-using-the-html5-video-element -->
<video id="videojs" style="width: 100%;height: 100%;" class="video-js vjs-default-skin vjs-big-play-centered" data-setup='{"controls": true, "autoplay": true }'>
<source src="{{ url_for('playlist', token=token) }}" type="application/x-mpegURL">
@ -57,7 +58,8 @@
<video style="width: 100%;height: 100%;" controls autoplay src="{{ url_for('segments', token=token) }}">
</noscript>
{% else %}
<video style="width: 100%;height: 100%;" controls autoplay src="{{ url_for('segments', token=token) }}">
<input id="videojs-enabled" type="hidden" value="0">
<video style="width: 100%;height: 100%;" controls autoplay src="{{ url_for('segments') }}?segment={{ start_number }}&token={{ token }}">
{% endif %}
</div>
<div id="stream-info-container"><noscript><iframe id="stream-info" src="{{ url_for('stream_info') }}?token={{ token }}&embed=1"></iframe></noscript></div>
@ -133,4 +135,4 @@
</script>
<script src="/static/platform.js"></script>
</body>
</html>
</html>

ファイルの表示

@ -14,7 +14,8 @@
margin-right:0.125em;
}
#stream-status {vertical-align: text-bottom;}
#refresh-button {font-weight:bold;margin-left:0.25em;padding: 0.25em 0.375em 0.25em 0.375em;color: white;}
.refresh-button {font-weight:bold;margin-left:0.25em;padding: 0.25em 0.375em 0.25em 0.375em;color: white;}
.hue-rotate {filter: hue-rotate(136deg);}
.icon {
margin-top:-1pt;
}
@ -85,6 +86,9 @@
.radial-animation {
animation: radial 20s linear forwards;
}
.radial-animation-duration {
animation-duration: 20s;
}
@keyframes radial {
from {stroke-dashoffset: 0;}
to {stroke-dashoffset: 32;}
@ -175,19 +179,16 @@
<div>
{% if not online %}
<span id="stream-light" style="background-color:red"></span>
<span id="stream-status">
The stream has ended.
<span id="stream-status">The stream has ended.</span>
{% elif video_was_corrupted %}
<span id="stream-light" style="background-color:yellow"></span>
<span id="stream-status">
The stream is online but you're not receiving it. Try refreshing the page.
<span id="stream-status">The stream is online but you're not receiving it. Try refreshing the page.</span>
{% else %}
<span id="stream-light" style="background-color:green"></span>
<span id="stream-status">
The stream is online.
<span id="stream-status">The stream is online.</span>
{% endif %}
</span>
<a id="refresh-button" class="pure-button pure-button-primary" style="display: none;">Refresh</a>
<a id="refresh-stream-button" class="refresh-button pure-button pure-button-primary" style="display: none;">Reload</a>
<a id="refresh-page-button" class="refresh-button hue-rotate pure-button pure-button-primary" style="display: none;">Refresh</a>
</div>
{% if embed_images %}
<!-- embedding this animated png messes with the animation -->