diff --git a/config.toml b/config.toml index 93689e1..22f9abc 100644 --- a/config.toml +++ b/config.toml @@ -6,5 +6,12 @@ fonts = [ "/usr/share/fonts/truetype/tlwg/TlwgMono.ttf" ] +[chat] +host_default_name = "Broadcaster" +anon_default_name = "Anonymous" + +[secrets] +tripcode_salt = "" # put a random value here for secure tripcodes + [stream] hls_time = 2 diff --git a/website/concatenate.py b/website/concatenate.py index 40ceb1c..8813e43 100644 --- a/website/concatenate.py +++ b/website/concatenate.py @@ -5,7 +5,7 @@ from website.utils.stream import _is_segment, _segment_number, get_segments, is_ SEGMENT = 'stream{number}.m4s' CORRUPTING_SEGMENT = 'corrupt.m4s' -STREAM_TIMEOUT = lambda: CONFIG['stream']['hls_time'] * 2 # consider the stream offline after this many seconds without a new segment +STREAM_TIMEOUT = lambda: CONFIG['stream']['hls_time'] * 2 + 2 # consider the stream offline after this many seconds without a new segment def resolve_segment_offset(segment_offset=1): ''' @@ -23,7 +23,7 @@ def get_next_segment(after, start_segment): raise SegmentUnavailable(f'stream went offline') start = time.time() while True: - time.sleep(1) + time.sleep(0.5) segments = get_segments() if after == None: try: @@ -70,7 +70,7 @@ class ConcatenatedSegments: 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 + # run this function before sending each segment (if we do it after then if someone gets the most of a segment but then stops, that wouldn't be counted, before = 0 viewers means nobody is retrieving the stream, after = slightly more accurate viewer count but 0 viewers doesn't necessarily mean nobody is retrieving the stream) self.segment_hook = segment_hook or (lambda n: None) # run this function when we send the corrupting segment self.corrupt_hook = corrupt_hook or (lambda: None) @@ -83,6 +83,7 @@ class ConcatenatedSegments: self._closed = False self.segment_read_offset = 0 self.segment = next(self.segments) + self.segment_hook(_segment_number(self.segment)) def _read(self, n): chunk = b'' @@ -104,14 +105,9 @@ class ConcatenatedSegments: break self.segment_read_offset = 0 - try: - next_segment = next(self.segments) - except SegmentUnavailable: - self.segment_hook(_segment_number(self.segment)) - raise - else: - self.segment_hook(_segment_number(self.segment)) - self.segment = next_segment + next_segment = next(self.segments) + self.segment_hook(_segment_number(next_segment)) + self.segment = next_segment return chunk def read(self, n): diff --git a/website/constants.py b/website/constants.py index 46aed72..2a5d8bd 100644 --- a/website/constants.py +++ b/website/constants.py @@ -27,9 +27,6 @@ CHAT_TIMEOUT = 3 # seconds between chat messages FLOOD_PERIOD = 20 # seconds FLOOD_THRESHOLD = 4 # messages in FLOOD_PERIOD seconds -HOST_DEFAULT_NICKNAME = 'Broadcaster' -ANON_DEFAULT_NICKNAME = 'Anonymous' - CHAT_MAX_STORAGE = 1024 CHAT_SCROLLBACK = 100 MESSAGE_MAX_LENGTH = 256 @@ -39,7 +36,7 @@ CAPTCHA_LIFETIME = 3600 VIEWER_ABSENT_THRESHOLD = 86400 -BACKGROUND_COLOUR = b'\x22\x22\x22' +BACKGROUND_COLOUR = b'\x23\x23\x23' # the same as in platform.css BROADCASTER_COLOUR = b'\xff\x82\x80' SEGMENT_INIT = 'init.mp4' diff --git a/website/routes.py b/website/routes.py index 24abc3c..13ffab1 100644 --- a/website/routes.py +++ b/website/routes.py @@ -1,4 +1,4 @@ -from flask import current_app, render_template, send_from_directory, request, abort, Response, redirect, url_for, make_response, send_file +from flask import current_app, render_template, send_from_directory, request, abort, redirect, url_for, make_response, send_file from werkzeug import wrap_file import os import time @@ -11,7 +11,7 @@ import toml 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, VIDEOJS_ENABLED_BY_DEFAULT, SEGMENT_INIT, CHAT_SCROLLBACK, BROADCASTER_COLOUR, BROADCASTER_TOKEN, SEGMENTS_DIR, VIEW_COUNTING_PERIOD, CONFIG, CONFIG_FILE, NOTES, N_NONE, MESSAGE_MAX_LENGTH +from website.constants import DIR_STATIC, DIR_STATIC_EXTERNAL, VIDEOJS_ENABLED_BY_DEFAULT, SEGMENT_INIT, CHAT_SCROLLBACK, BROADCASTER_COLOUR, BROADCASTER_TOKEN, SEGMENTS_DIR, VIEW_COUNTING_PERIOD, CONFIG, CONFIG_FILE, NOTES, N_NONE, MESSAGE_MAX_LENGTH, BACKGROUND_COLOUR from website.concatenate import ConcatenatedSegments, resolve_segment_offset RE_WHITESPACE = re.compile(r'\s+') @@ -46,7 +46,7 @@ def index(token=None): online=online, start_number=resolve_segment_offset() if online else 0, hls_time=CONFIG['stream']['hls_time']) - response = Response(response) # TODO: add a view of the chat only, either as an arg here or another route + response = make_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 @@ -101,6 +101,11 @@ def segment_arbitrary(n): viewership.video_was_corrupted.remove(token) except KeyError: pass + + # only send segments that are listed in stream.m3u8 + # this stops old segments from previous streams being sent + if f'stream{n}.m4s' not in stream.get_segments(): + return abort(404) viewership.view_segment(n, token) response = send_from_directory(SEGMENTS_DIR, f'stream{n}.m4s', add_etags=False) response.headers['Cache-Control'] = 'no-cache' @@ -128,7 +133,13 @@ def segments(): except FileNotFoundError: return abort(404) - response = send_file(concatenated_segments, mimetype='video/mp4', add_etags=False) + def generate(): + while True: + chunk = concatenated_segments.read(8192) + if chunk == b'': + return + yield chunk + response = current_app.response_class(generate(), mimetype='video/mp4') response.headers['Cache-Control'] = 'no-store' response.set_cookie('token', token) return response @@ -153,6 +164,7 @@ def chat_iframe(): default_nickname=viewership.default_nickname, broadcaster=token == BROADCASTER_TOKEN, broadcaster_colour=BROADCASTER_COLOUR, + background_colour=BACKGROUND_COLOUR, debug=request.args.get('debug'), RE_WHITESPACE=RE_WHITESPACE, len=len, @@ -205,7 +217,7 @@ def comment_iframe(token=None): nickname=viewers[token]['nickname'], viewer=viewers[token], show_settings=preset.get('show_settings', False)) - response = Response(response) + response = make_response(response) response.set_cookie('token', token) return response @@ -297,6 +309,7 @@ def users(): broadcaster=token == BROADCASTER_TOKEN, debug=request.args.get('debug'), broadcaster_colour=BROADCASTER_COLOUR, + background_colour=BACKGROUND_COLOUR, len=len) @current_app.route('/static/radial.apng') diff --git a/website/templates/chat-iframe.html b/website/templates/chat-iframe.html index 1011c96..89e172e 100644 --- a/website/templates/chat-iframe.html +++ b/website/templates/chat-iframe.html @@ -11,11 +11,20 @@ .rotate {transform: rotate(-180deg);} .reverse {direction: rtl;} - .comment {color: white;padding:4px 0;margin-top:-4px;margin-bottom:calc(-4px + 0.375em);overflow:hidden;} - .date {color:gray;font-size:75%;text-align:center;border-bottom:1px solid #333;margin:0.5em 0 0.75em 0;} + .comment {color:white;padding:3px 2px;overflow:hidden;} + .comment:hover{background-color:#333;border-radius:4px;} + .date {color:gray;font-size:75%;text-align:center;border-bottom:1px solid #333;margin:0.5em 0 0.75em 0;cursor:default;} - .name {font-weight: bold;unicode-bidi: isolate;} - sup {margin: 0 -0.25em 0 0.125em;font-size: 90%;font-family:monospace;} + .name {color:var(--name-color);font-weight:bold;unicode-bidi:isolate;} + .name:hover{ + background-color: var(--name-bg-color); + text-shadow: 0 0 1px {{ background_colour }}; + border-radius: 2px; + padding: 2px; + margin: -2px; + cursor: default; + } + sup {margin-right: 0.125em;font-size: 90%;font-family:monospace;} .tripcode { margin-left: 0.5em; padding: 0 5px; @@ -28,16 +37,16 @@ .barrier {display:inline-block;margin-right:0.5em;} .message {overflow-wrap: break-word;unicode-bidi: isolate;} .camera {transform: scaleX(-1);text-shadow: 0px 0px 6px #{{ broadcaster_colour.hex() }};cursor: help;margin-right:0.25em;word-break: keep-all;} - .time {font-size: 80%;color: gray;vertical-align:middle;} + .time {font-size: 80%;color: gray;vertical-align:middle;cursor:default;} {% if include_user_list %} #users {color:white;} .group {margin-bottom:1.5em;} .group-name {margin-bottom:0.25em;} - .person {margin-left: 0.5em;} + .person {margin: 0 0 2px 0.5em;} {% endif %} - input[type="submit"] {padding-left:0.25em;padding-right:0.25em;margin-bottom:0.5em;} + input[type="submit"] {padding:0 0.25em;margin-bottom:0.5em;} .refresh { color: white; background-color: gray; @@ -51,6 +60,8 @@ box-shadow: 0px 2px 4px black; margin: 0; } + .refresh::after {content: "Manual refresh required";} + input[type="checkbox"]{vertical-align:middle;} .unhide { animation: unhide 0s forwards 30s; height: 0; @@ -78,6 +89,12 @@ margin-bottom: 1.25em; } } + td{width:100vw;} + + /* for text-based browsers */ + .textonly { + display: none; + }
@@ -90,42 +107,47 @@ {% endif %} - - +
{% endif %} - +