diff --git a/README.md b/README.md index c2b03e4..b2a89c9 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ source venv/bin/activate python -m pip install -r requirements.txt ``` -Before you run it might want to edit [/config.toml][config]: +Before you run it you should edit [/config.toml][config], e.g. these +options: * `secret_key`: used for cryptography, make it any long random string @@ -36,11 +37,11 @@ Before you run it might want to edit [/config.toml][config]: * `segments/directory`: directory containing stream segments, the default is `stream/` in - the project root + the cloned repository * `title/file`: location of the stream title, the default is `title.txt` in the - project root + cloned repository * `captcha/fonts`: locations of fonts for the captcha, leaving it blank will use the @@ -53,16 +54,16 @@ python -m uvicorn app:app --port 5051 This will start a webserver listening on localhost port 5051. -If you go to `http://localhost:5051` in a regular web browser now -you should see the interface. When you started the webserver some -credentials were printed in the terminal; you can log in with those at +If you go to `http://localhost:5051` in a web browser now you should see +the site. When you started the webserver some credentials were +printed in the terminal; you can log in with those at `http://localhost:5051/login` (requires cookies). The only things left are (1) streaming, and (2) letting other people -access your stream. [/STREAMING.md][streaming] has instructions for -setting up OBS Studio and a Tor onion service. The instructions will -be useful even if you want to use different streaming software and put -your stream on the Internet some other way. +access your stream. [/STREAMING.md][streaming] has instructions for +setting up OBS Studio and a Tor onion service. If you want to use +different streaming software and put your stream on the Internet some +other way, still read those instructions and copy the gist. ## Copying @@ -102,10 +103,10 @@ anonstream is AGPL 3.0 or later, see * werkzeug ([BSD 3-Clause][werkzeug]) -[config]: https://git.076.ne.jp/ninya9k/anonstream/src/branch/master/config.toml -[licence]: https://git.076.ne.jp/ninya9k/anonstream/src/branch/master/LICENSES/AGPL-3.0-or-later.md +[config]: https://git.076.ne.jp/ninya9k/anonstream/src/branch/master/config.toml +[licence]: https://git.076.ne.jp/ninya9k/anonstream/src/branch/master/LICENSES/AGPL-3.0-or-later.md [settings.svg]: https://git.076.ne.jp/ninya9k/anonstream/src/branch/master/anonstream/static/settings.svg -[streaming]: https://git.076.ne.jp/ninya9k/anonstream/src/branch/master/STREAMING.md +[streaming]: https://git.076.ne.jp/ninya9k/anonstream/src/branch/master/STREAMING.md [aiofiles]: https://github.com/Tinche/aiofiles/blob/master/LICENSE [captcha]: https://github.com/lepture/captcha/blob/master/LICENSE diff --git a/STREAMING.md b/STREAMING.md index 4f82e34..91be24c 100644 --- a/STREAMING.md +++ b/STREAMING.md @@ -1,56 +1,199 @@ ### Tor -Install tor and include these lines in your [torrc][torrc]: +Install tor. On Linux you can probably install a package called `tor` and +be done, otherwise [compile it][tor]. On Windows download this binary: +. +Find your [torrc][torrc]. On Linux it is probably at `/etc/tor/torrc`. +On Windows it might be somewhere in `%appdata%\tor` or something. + +#### Background + +A Tor hidden service is a regular TCP service that you talk to via a +6-hop circuit created inside the Tor network. You initiate the creation +of this circuit by providing tor with the service's hostname, which is a +long base32-encoded string ending in ".onion". This hostname is derived +from a pair of cryptographic keys generated by the hidden service +operator. + +A TCP service is a computer program you interact with over the Internet +using TCP. TCP is a low-level networking protocol that sits above IP +and creates a reliable so-called "connection" between two computers. It +handles the reordering and resending of packets that are shuffled or +lost in transit on the Internet, such that the bytes sent from one +computer will match exactly the bytes that arrive at the other computer +(barring active interference (MITM), TCP is not secure). Getting +reliability for free greatly simplifies the creation of network +applications, and for this reason and other historical reasons TCP is +ubiquitous on the Internet to this day. Many applications use TCP, for +example IRC, SSH, RTMP, Minecraft, and HTTP (like us here). + +#### Configuration + +We are now going to create a hidden service. We need to give tor a +directory to store the keys it generates, the location of our existing +TCP service, and a virtual TCP port to listen on. There are two +directives we have to add to our torrc: `HiddenServiceDir` and +`HiddenServicePort`. (There is a commented-out section in the default +torrc for hidden services, you may wish to make these changes there.) + +##### `HiddenServiceDir` + +`HiddenServiceDir` sets the directory for the hidden service's keys and +other data. You could choose any directory, but you should make sure +it's owned by the user the tor daemon runs as, and the directory's +permissions are `0700/drwx------` (`rwx` for user, `---` for group and +everyone else). + +If you configure this in a way tor doesn't like, tor will kill itself +and complain in one of these two ways: +``` +Jun 11 23:21:17.000 [warn] Directory /home/n9k/projects/anonstream/hidden_service cannot be read: Permission denied +``` +``` +Jun 12 02:37:51.036 [warn] Permissions on directory /var/lib/tor/anonstream are too permissive. +``` + +The simplest option is to copy the examples provided in the torrc, on +Linux that would probably be a directory inside `/var/lib/tor`, e.g. +`HiddenServiceDir /var/lib/tor/anonstream`. tor will create this +directory itself with the uid, gid, and permissions that it likes, which +for me are these: +``` +Access: (0700/drwx------) Uid: ( 42/ tor) Gid: ( 42/ tor) +``` + +###### `HiddenServiceDir` troubleshooting + +If you created the directory yourself and gave it the wrong permissions +or uid or gid, delete the directory and let tor create it itself, or do +this: +``` +# chown -R tor:tor /var/lib/tor/anonstream +# chmod 0700 /var/lib/tor/anonstream +# chmod 0600 /var/lib/tor/anonstream/* +# chmod 0700 /var/lib/tor/anonstream/*/ +``` + +If the user and group `tor` do not exist, your tor daemon runs as some +other user. There may be a `User` directive in your torrc or in a file +included by your torrc, for example on Debian it's `User debian-tor`. +This means that a tor process running as root will immediately drop +privileges by switching to the user `debian-tor`. The user's primary +group should have the same name, but you can check as root like this: +`# id debian-tor`. + +On Linux, if tor is already running you can see what user and group it is +running as like this: +``` +$ ps -C tor -o uid,gid,cmd +UID GID CMD + 42 42 tor --quiet --runasdaemon 0 +$ cat /etc/passwd | grep :42: | cut -f 1 -d : # 42 is the UID here +tor +$ cat /etc/group | grep :42: | cut -f 1 -d : # 42 is the GID here +tor +``` + +Alternatively you could specify a directory inside the cloned +repository, e.g. `/home/delphine/Documents/anonstream/hidden_service` +or something like that. This will only work if the tor daemon has `rwx` +permissions on the directory and at least `r-x` permissions on all the +directories above it. This is probably not the case for you since your +home folder might have `0700/drwx------` permissions. If you +installed tor as a package, the daemon probably runs as its own user +(e.g. `debian-tor` on Debian, `tor` on Arch/Gentoo). If you want to +figure this out yourself go ahead. I would advise just using +`/var/lib/tor/anonstream` though. + +##### `HiddenServicePort` + +Include this line verbatim directly below the `HiddenServiceDir` line: ``` -HiddenServiceDir $PROJECT_ROOT/hidden_service HiddenServicePort 80 127.0.0.1:5051 ``` -but replace `$PROJECT_ROOT` with the folder you cloned the git repo -into. -Then reload tor. If everything went well, the directory will have been -created and your onion address will be in -`$PROJECT_ROOT/hidden_service/hostname`. +tor will listen for connections to our onion address at virtual port +80 (this is the conventional HTTP port), and it will forward that +traffic to our TCP service at 127.0.0.1:5051, which is our webserver. + +##### Finish + +Example configuration: +``` +HiddenServiceDir /var/lib/tor/anonstream +HiddenServicePort 80 127.0.0.1:5051 +``` + +Reload tor to make it reread its torrc: `# pkill -HUP tor`. With +systemd you can alternatively do `# systemctl reload tor`. If +everything went well, the directory will have been created and your +onion address will be in `$HIDDEN_SERVICE_DIR/hostname`. ### OBS Studio -Install OBS Studio. If the autoconfiguration wizard prompts you to -choose a third-party service, ignore it since we're not gonna be doing -that. +Install OBS Studio. If the autoconfiguration wizard prompts you to +choose a third-party service, ignore it since we're not going to be +using a third-party service. Click `Settings` and set these: * Advanced * Recording * Filename Formatting: `stream` + * Overwrite if file exists: yes * Video - * Output (Scaled) Resolution: `960x540` or lower + * Output (Scaled) Resolution: `960x540` or lower, or whatever you want + * Common FPS Values: any integer framerate (e.g. 30 or 60) * Output * Output Mode: `Advanced` * Recording: - | | | - |----------------------------|------------------------------------------------------------------------------------------------| - | Type | `Custom Output (FFmpeg)` | - | FFmpeg Output Type | `Output to File` | - | File path or URL | same as config.toml: `segments/directory` (but should be an absolute path) | - | Container Format | `hls` | - | Muxer Settings (if any) | `hls_init_time=0 hls_time=2 hls_list_size=120 hls_flags=delete_segments hls_segment_type=fmp4` | - | Video bitrate | `420 Kbps` or lower | - | Keyframe interval (frames) | `30` (same as the framerate, or exactly half) | - | Video Encoder | libx264, or an H.264 hardware encoder (e.g. `h264_nvenc` for Nvidia, [see here][ffmpeg]) | - | Audio Bitrate | `96 Kbps` | - | Audio Encoder | `aac` | + ``` + +----------------------------+-------------------------------------+ + | Field | Value | + +============================+=====================================+ + | Type | `Custom Output (FFmpeg)` | + +----------------------------+-------------------------------------+ + | FFmpeg Output Type | `Output to File` | + +----------------------------+-------------------------------------+ + | File path or URL | same as the `segments/directory` | + | | option in config.toml, but make it | + | | an absolute path | + +----------------------------+-------------------------------------+ + | Container Format | `hls` | + +----------------------------+-------------------------------------+ + | Muxer Settings (if any) | `hls_init_time=0 hls_time=2 ` | + | | `hls_list_size=120 ` | + | | `hls_flags=delete_segments ` | + | | `hls_segment_type=fmp4` | + +----------------------------+-------------------------------------+ + | Video bitrate | `420 Kbps` or lower, or whatever | + | | you want | + +----------------------------+-------------------------------------+ + | Keyframe interval (frames) | `framerate` * `hls_time`, e.g. for | + | | 60fps and an `hls_time` of 2 | + | | seconds, set this to 120 | + +----------------------------+-------------------------------------+ + | Video Encoder | libx264, or an H.264 hardware | + | | encoder (e.g. `h264_nvenc` for | + | | Nvidia, [see here][ffmpeg]) | + +----------------------------+-------------------------------------+ + | Audio Bitrate | `96 Kbps`, or whatever you want | + +----------------------------+-------------------------------------+ + | Audio Encoder | `aac` | + +----------------------------+-------------------------------------+ + ``` -Then click `OK`. +To start streaming click `Start Recording`. -That's it. To start streaming click `Start Recording`. - -Because of the muxer settings we used, segments older than four -minutes will be constantly deleted. When you stop streaming, the last -four minutes worth of segments will remain the segments directory. -You can delete them if you want. When you're not streaming you can -delete everything in the segments directory and it'll be fine. +When it is recording, segments older than four minutes will be regularly +deleted, and when it stops recording the last four minutes worth of +segments will remain the segments directory. (You can change the number +of kept segments by modifying the `hls_list_size` option in the muxer +settings.) When it is not recording, you can delete the files in the +segments directory without consequence. Old segments will never be sent +over the network even if they are not deleted. +[tor]: https://gitlab.torproject.org/tpo/core/tor [torrc]: https://support.torproject.org/#tbb-editing-torrc [ffmpeg]: https://trac.ffmpeg.org/wiki/HWAccelIntro diff --git a/anonstream/__init__.py b/anonstream/__init__.py index 8c8a358..563ad18 100644 --- a/anonstream/__init__.py +++ b/anonstream/__init__.py @@ -6,13 +6,16 @@ import secrets import toml from collections import OrderedDict -from quart import Quart +from quart_compress import Compress from werkzeug.security import generate_password_hash +from anonstream.quart import Quart from anonstream.utils.captcha import create_captcha_factory, create_captcha_signer from anonstream.utils.colour import color_to_colour from anonstream.utils.user import generate_token +compress = Compress() + def create_app(config_file): with open(config_file) as fp: config = toml.load(fp) @@ -50,6 +53,8 @@ def create_app(config_file): 'MAX_CHAT_SCROLLBACK': config['memory']['chat_scrollback'], 'TASK_PERIOD_ROTATE_USERS': config['tasks']['rotate_users'], 'TASK_PERIOD_ROTATE_CAPTCHAS': config['tasks']['rotate_captchas'], + 'TASK_PERIOD_ROTATE_WEBSOCKETS': config['tasks']['rotate_websockets'], + 'TASK_PERIOD_BROADCAST_PING': config['tasks']['broadcast_ping'], 'TASK_PERIOD_BROADCAST_USERS_UPDATE': config['tasks']['broadcast_users_update'], 'TASK_PERIOD_BROADCAST_STREAM_INFO_UPDATE': config['tasks']['broadcast_stream_info_update'], 'THRESHOLD_USER_NOTWATCHING': config['thresholds']['user_notwatching'], @@ -61,8 +66,10 @@ def create_app(config_file): 'CHAT_NAME_MIN_CONTRAST': config['chat']['min_name_contrast'], 'CHAT_BACKGROUND_COLOUR': color_to_colour(config['chat']['background_color']), 'CHAT_LEGACY_TRIPCODE_ALGORITHM': config['chat']['legacy_tripcode_algorithm'], - 'FLOOD_DURATION': config['flood']['duration'], - 'FLOOD_THRESHOLD': config['flood']['threshold'], + 'FLOOD_MESSAGE_DURATION': config['flood']['messages']['duration'], + 'FLOOD_MESSAGE_THRESHOLD': config['flood']['messages']['threshold'], + 'FLOOD_LINE_DURATION': config['flood']['lines']['duration'], + 'FLOOD_LINE_THRESHOLD': config['flood']['lines']['threshold'], 'CAPTCHA_LIFETIME': config['captcha']['lifetime'], 'CAPTCHA_FONTS': config['captcha']['fonts'], 'CAPTCHA_ALPHABET': config['captcha']['alphabet'], @@ -112,4 +119,11 @@ def create_app(config_file): import anonstream.routes import anonstream.tasks + # Compress some responses + compress.init_app(app) + app.config.update({ + "COMPRESS_MIN_SIZE": 2048, + "COMPRESS_LEVEL": 9, + }) + return app diff --git a/anonstream/chat.py b/anonstream/chat.py index 9c789e8..473d9b5 100644 --- a/anonstream/chat.py +++ b/anonstream/chat.py @@ -8,7 +8,7 @@ from quart import current_app, escape from anonstream.broadcast import broadcast, broadcast_users_update from anonstream.helpers.chat import generate_nonce_hash, get_scrollback -from anonstream.utils.chat import get_message_for_websocket +from anonstream.utils.chat import get_message_for_websocket, get_approx_linespan CONFIG = current_app.config MESSAGES_BY_ID = current_app.messages_by_id @@ -33,18 +33,48 @@ def add_chat_message(user, nonce, comment, ignore_empty=False): if ignore_empty and len(comment) == 0: return False + timestamp_ms = time.time_ns() // 1_000_000 + timestamp = timestamp_ms // 1000 + + # Check user + while user['linespan']: + linespan_timestamp, _ = user['linespan'][0] + if timestamp - linespan_timestamp >= CONFIG['FLOOD_LINE_DURATION']: + user['linespan'].popleft() + else: + break + total_recent_linespan = sum(map( + lambda linespan_tuple: linespan_tuple[1], + user['linespan'], + )) + if total_recent_linespan > CONFIG['FLOOD_LINE_THRESHOLD']: + raise Rejected( + f'Chat overuse in the last ' + f'{CONFIG["FLOOD_LINE_DURATION"]:.0f} seconds' + ) + # Check message message_id = generate_nonce_hash(nonce) if message_id in MESSAGES_BY_ID: raise Rejected('Discarded suspected duplicate message') if len(comment) == 0: raise Rejected('Message was empty') + if len(comment.strip()) == 0: + raise Rejected('Message was practically empty') if len(comment) > 512: raise Rejected('Message exceeded 512 chars') + if comment.count('\n') + 1 > 12: + raise Rejected('Message exceeded 12 lines') + + linespan = get_approx_linespan(comment) + if linespan > 12: + raise Rejected('Message would span too many lines') + + # Record linespan + linespan_tuple = (timestamp, linespan) + user['linespan'].append(linespan_tuple) # Create and add message - timestamp_ms = time.time_ns() // 1_000_000 - timestamp = timestamp_ms // 1000 try: last_message = next(reversed(MESSAGES)) except StopIteration: diff --git a/anonstream/helpers/user.py b/anonstream/helpers/user.py index 08f897b..82442c5 100644 --- a/anonstream/helpers/user.py +++ b/anonstream/helpers/user.py @@ -3,7 +3,7 @@ import hashlib import base64 -from collections import OrderedDict +from collections import deque, OrderedDict from math import inf from quart import current_app @@ -35,7 +35,7 @@ def generate_user(timestamp, token, broadcaster, presence): 'tag': tag, 'broadcaster': broadcaster, 'verified': broadcaster, - 'websockets': set(), + 'websockets': {}, 'name': None, 'color': colour_to_color(colour), 'tripcode': None, @@ -45,6 +45,7 @@ def generate_user(timestamp, token, broadcaster, presence): 'watching': -inf, }, 'presence': presence, + 'linespan': deque(), } def get_default_name(user): diff --git a/anonstream/quart.py b/anonstream/quart.py new file mode 100644 index 0000000..5761e86 --- /dev/null +++ b/anonstream/quart.py @@ -0,0 +1,51 @@ +import asyncio + +from werkzeug.wrappers import Response as WerkzeugResponse +from quart.app import Quart as Quart_ +from quart.asgi import ASGIHTTPConnection as ASGIHTTPConnection_ +from quart.utils import encode_headers + + +RESPONSE_ITERATOR_TIMEOUT = 10 + + +class ASGIHTTPConnection(ASGIHTTPConnection_): + async def _send_response(self, send, response): + await send({ + "type": "http.response.start", + "status": response.status_code, + "headers": encode_headers(response.headers), + }) + + if isinstance(response, WerkzeugResponse): + for data in response.response: + body = data.encode(response.charset) if isinstance(data, str) else data + await asyncio.wait_for( + send({ + "type": "http.response.body", + "body": body, + "more_body": True, + }), + timeout=RESPONSE_ITERATOR_TIMEOUT, + ) + else: + async with response.response as response_body: + async for data in response_body: + body = data.encode(response.charset) if isinstance(data, str) else data + await asyncio.wait_for( + send({ + "type": "http.response.body", + "body": body, + "more_body": True, + }), + timeout=RESPONSE_ITERATOR_TIMEOUT, + ) + await send({ + "type": "http.response.body", + "body": b"", + "more_body": False, + }) + + +class Quart(Quart_): + asgi_http_class = ASGIHTTPConnection diff --git a/anonstream/routes/nojs.py b/anonstream/routes/nojs.py index c1b7d59..967f894 100644 --- a/anonstream/routes/nojs.py +++ b/anonstream/routes/nojs.py @@ -5,7 +5,7 @@ from quart import current_app, request, render_template, redirect, url_for, esca from anonstream.captcha import get_random_captcha_digest_for from anonstream.chat import add_chat_message, Rejected -from anonstream.stream import get_stream_title, get_stream_uptime_and_viewership +from anonstream.stream import is_online, get_stream_title, get_stream_uptime_and_viewership from anonstream.user import add_state, pop_state, try_change_appearance, update_presence, get_users_by_presence, Presence, verify, deverify, BadCaptcha from anonstream.routes.wrappers import with_user_from, render_template_with_etag from anonstream.helpers.chat import get_scrollback @@ -24,6 +24,7 @@ async def nojs_stream(user): 'nojs_stream.html', csp=generate_csp(), user=user, + online=is_online(), ) @current_app.route('/info.html') diff --git a/anonstream/routes/websocket.py b/anonstream/routes/websocket.py index 1734f21..933c9c8 100644 --- a/anonstream/routes/websocket.py +++ b/anonstream/routes/websocket.py @@ -3,8 +3,11 @@ import asyncio +from math import inf + from quart import current_app, websocket +from anonstream.user import see from anonstream.websocket import websocket_outbound, websocket_inbound from anonstream.routes.wrappers import with_user_from @@ -12,11 +15,12 @@ from anonstream.routes.wrappers import with_user_from @with_user_from(websocket) async def live(user): queue = asyncio.Queue(maxsize=0) - user['websockets'].add(queue) + user['websockets'][queue] = -inf producer = websocket_outbound(queue, user) consumer = websocket_inbound(queue, user) try: await asyncio.gather(producer, consumer) finally: - user['websockets'].remove(queue) + see(user) + user['websockets'].pop(queue) diff --git a/anonstream/routes/wrappers.py b/anonstream/routes/wrappers.py index ad8da2b..8a4484d 100644 --- a/anonstream/routes/wrappers.py +++ b/anonstream/routes/wrappers.py @@ -33,14 +33,21 @@ def auth_required(f): async def wrapper(*args, **kwargs): if check_auth(request): return await f(*args, **kwargs) - hint = 'The broadcaster should log in with the credentials printed ' \ - 'in their terminal.' - body = ( - f'

{hint}

' - if request.authorization is None else - '

Wrong username or password. Refresh the page to try again.

' - f'

{hint}

' + hint = ( + 'The broadcaster should log in with the credentials printed in ' + 'their terminal.' ) + if request.authorization is None: + body = ( + f'\n' + f'

{hint}

\n' + ) + else: + body = ( + f'\n' + f'

Wrong username or password. Refresh the page to try again.

\n' + f'

{hint}

\n' + ) return body, 401, {'WWW-Authenticate': 'Basic'} return wrapper diff --git a/anonstream/segments.py b/anonstream/segments.py index ba9089f..cf7c429 100644 --- a/anonstream/segments.py +++ b/anonstream/segments.py @@ -25,11 +25,12 @@ class UnsafePath(Exception): def get_mtime(): try: mtime = os.path.getmtime(CONFIG['SEGMENT_PLAYLIST']) - except FileNotFoundError as e: - raise Stale from e + except OSError as e: + raise Stale(f"couldn't stat playlist: {e}") from e else: - if time.time() - mtime >= CONFIG['SEGMENT_PLAYLIST_STALE_THRESHOLD']: - raise Stale + mtime_ago = time.time() - mtime + if mtime_ago >= CONFIG['SEGMENT_PLAYLIST_STALE_THRESHOLD']: + raise Stale(f'last modified {mtime_ago:.1f}s ago') return mtime @ttl_cache(CONFIG['SEGMENT_PLAYLIST_CACHE_LIFETIME']) @@ -38,13 +39,18 @@ def get_playlist(): try: mtime = get_mtime() except Stale as e: - raise Offline from e + reason, *_ = e.args + raise Offline(f'stale playlist: {reason}') from e else: - playlist = m3u8._load_from_file(CONFIG['SEGMENT_PLAYLIST']) - if playlist.is_endlist: - raise Offline - if len(playlist.segments) == 0: - raise Offline + try: + playlist = m3u8._load_from_file(CONFIG['SEGMENT_PLAYLIST']) + except OSError: + raise Offline(f"couldn't read playlist: {e}") from e + else: + if playlist.is_endlist: + raise Offline('playlist ended') + if len(playlist.segments) == 0: + raise Offline('empty playlist') return playlist, mtime @@ -76,12 +82,18 @@ def get_next_segment(uri): segment = None return segment -async def get_segment_uris(): +async def get_segment_uris(token): try: segment = get_starting_segment() - except Offline: + except Offline as e: + reason, *_ = e.args + print( + f'[debug @ {time.time():.3f}: {token=}] ' + f'stream went offline before we could find any segments ({reason})' + ) return - else: + + if segment.init_section is not None: yield segment.init_section.uri while True: @@ -91,13 +103,25 @@ async def get_segment_uris(): while True: try: next_segment = get_next_segment(segment.uri) - except Offline: + except Offline as e: + reason, *_ = e.args + print( + f'[debug @ {time.time():.3f}: {token=}] ' + f'stream went offline while looking for the ' + f'segment following {segment.uri!r} ({reason})' + ) return else: if next_segment is not None: segment = next_segment break elif time.monotonic() - t0 >= CONFIG['SEGMENT_SEARCH_TIMEOUT']: + print( + f'[debug @ {time.time():.3f}: {token=}] ' + f'timed out looking for the segment following ' + f'{segment.uri!r} ' + f'(timeout={CONFIG["SEGMENT_SEARCH_TIMEOUT"]}s)' + ) return else: await asyncio.sleep(CONFIG['SEGMENT_SEARCH_COOLDOWN']) @@ -112,8 +136,7 @@ def path_for(uri): async def segments(segment_read_hook=lambda uri: None, token=None): print(f'[debug @ {time.time():.3f}: {token=}] entering segment generator') - uri = None - async for uri in get_segment_uris(): + async for uri in get_segment_uris(token): #print(f'[debug @ {time.time():.3f}: {token=}] {uri=}') try: path = path_for(uri) @@ -121,7 +144,7 @@ async def segments(segment_read_hook=lambda uri: None, token=None): unsafe_path, *_ = e.args print( f'[debug @ {time.time():.3f}: {token=}] ' - f'segment {uri=} has unsafe {path=}' + f'segment {uri=} has {unsafe_path=}' ) break @@ -136,10 +159,10 @@ async def segments(segment_read_hook=lambda uri: None, token=None): f'segment {uri=} at {path=} unexpectedly does not exist' ) break - else: - print( - f'[debug @ {time.time():.3f}: {token=}] ' - f'could not find segment following {uri=} after at least ' - f'{CONFIG["SEGMENT_SEARCH_TIMEOUT"]} seconds' - ) + except OSError as e: + print( + f'[debug @ {time.time():.3f}: {token=}] ' + f'segment {uri=} at {path=} cannot be read: {e}' + ) + break print(f'[debug @ {time.time():.3f}: {token=}] exiting segment generator') diff --git a/anonstream/static/anonstream.js b/anonstream/static/anonstream.js index 2e45164..b74dcc7 100644 --- a/anonstream/static/anonstream.js +++ b/anonstream/static/anonstream.js @@ -11,14 +11,17 @@ const TOKEN_HASH = document.body.dataset.tokenHash; const CSP = document.body.dataset.csp; /* insert js-only markup */ -const jsmarkup_stream = `` +const jsmarkup_stream_video = '' +const jsmarkup_stream_offline = '

[offline]

' const jsmarkup_info = '
'; const jsmarkup_info_float = ''; -const jsmarkup_info_float_button = ''; +const jsmarkup_info_float_button = ''; const jsmarkup_info_float_viewership = '
'; const jsmarkup_info_float_uptime = '
'; const jsmarkup_info_title = '
'; -const jsmarkup_chat_messages = '
    '; +const jsmarkup_chat_messages = `\ +
      +`; const jsmarkup_chat_users = `\
      @@ -38,11 +41,11 @@ const jsmarkup_chat_form = `\ × + -