diff --git a/CHANGELOG.md b/CHANGELOG.md index 99d8805..97e48eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,3 +14,13 @@ All notable changes to this project are documented in this file. (`channels.example.yaml`, `.env.example`). - Unit test suite covering config parsing, URL expiry parsing, ffmpeg argument building, playlist generation, and broadcast fan-out. + +### Fixed + +- `/stream/{channel_id}` now forces the connection closed after each response (`Connection: close`) instead of + allowing keep-alive. Jellyfin's M3U tuner client (`SharedHttpStream`) was reusing a pooled keep-alive connection + still mid-stream from an earlier request and reading raw MPEG-TS bytes as if they were a fresh HTTP response, + failing with "Received an invalid status line". +- The yt-dlp resolver now correctly classifies which of the two `requested_formats` entries is audio when a live + stream is split into separate video-only and audio-only renditions; it previously relied on `acodec` being set, + which yt-dlp does not consistently do for audio-only formats, causing every resolve to fail. diff --git a/pyproject.toml b/pyproject.toml index 4ddecb7..a7126ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "restreamer" -version = "0.1.0" +version = "1.0.0" description = "Restreams YouTube livestreams as continuous local streams consumable by Jellyfin Live TV" authors = [ {name = "Jan Doubravsky",email = "jan.doubravsky@gmail.com"} diff --git a/src/restreamer/http_server.py b/src/restreamer/http_server.py index a2e309c..ed04f8a 100644 --- a/src/restreamer/http_server.py +++ b/src/restreamer/http_server.py @@ -42,6 +42,10 @@ async def _handle_stream(request: web.Request) -> web.StreamResponse: raise web.HTTPNotFound(text=f"Unknown channel '{channel_id}'") response = web.StreamResponse(status=200, headers={"Content-Type": MPEGTS_CONTENT_TYPE}) + # A live stream is never meant to be pooled and reused for a later request; some HTTP + # clients (e.g. Jellyfin's SharedHttpStream) get fed stale bytes from a dangling + # keep-alive connection if we let one linger here. Force a fresh connection every time. + response.force_close() await response.prepare(request) queue = broadcaster.subscribe() @@ -51,7 +55,7 @@ async def _handle_stream(request: web.Request) -> web.StreamResponse: if chunk is None: break await response.write(chunk) - except ConnectionResetError: + except (ConnectionResetError, ConnectionAbortedError): logger.debug("Client disconnected from channel '{}'", channel_id) finally: broadcaster.unsubscribe(queue)