make js play nice when videojs is disabled
このコミットが含まれているのは:
コミット
c6583aaf7d
|
@ -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 -->
|
||||
|
|
読み込み中…
新しいイシューから参照