From 9ed0aae2d00be9b9ee1000e9eafce360f1044027 Mon Sep 17 00:00:00 2001 From: Kayos Date: Sat, 23 May 2026 11:58:12 -0700 Subject: [PATCH] =?UTF-8?q?M7=20DONE=20via=20delegation=20=E2=80=94=20pv.y?= =?UTF-8?q?outube=20plays=20HD=20with=20audio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After hitting the segment-timing wall on our hand-rolled DASH MPD (audio drifted -25s -> -44s behind video on long content), pivoted to delegating playback to plugin.video.youtube v7.4.3 which already has years of sidx-parsed SegmentTimeline + multi-client fallback work. torttube._play() now: 1. Tries _delegate_to_pv_youtube(yt_id) — sets a resolved URL of 'plugin://plugin.video.youtube/play/?video_id='. Kodi chain-resolves to pv.youtube which builds the proper MPD and hands inputstream.adaptive a correctly-aligned manifest. Default. 2. Falls back to our DASH builder (still in code, gated by 'dash_enabled' setting + dash.on marker) if pv.youtube is absent. 3. Falls through to yt-dlp progressive 360p as the final safety net. When delegating, we skip our SponsorBlock monitor — pv.youtube has its own and would double-skip otherwise. Cobb-verified live on Livingroom Pi: LTT 'Trump Phone' (which crashed our DASH with audio sync errors growing to -44s) now plays HD with audio synced. 'Please sign in' message in log is from the tv_unplugged Innertube client; pv.youtube falls back to a working client automatically — no user account required. Settings: prefer_pv_youtube boolean (default true). Addon v0.0.11. Reference: https://kodi.wiki/view/Add-on:YouTube --- MILESTONES.md | 25 ++++++- addon/plugin.video.torttube/addon.xml | 2 +- addon/plugin.video.torttube/main.py | 70 ++++++++++++++++--- .../resources/settings.xml | 5 +- docs/upstream.md | 12 ++++ 5 files changed, 102 insertions(+), 12 deletions(-) diff --git a/MILESTONES.md b/MILESTONES.md index 14991e1..2333cf9 100644 --- a/MILESTONES.md +++ b/MILESTONES.md @@ -86,7 +86,30 @@ progressive) is deprecated by YouTube; higher quality needs DASH manifest generation. Code path exists, gated behind `TORTTUBE_DASH=1`. -## M7 — DASH / HD playback [WIP — close on segment timing] +## M7 — HD playback [DONE via delegation] + +**Strategy pivoted 2026-05-23.** After hitting a wall on segment-timing +alignment in our hand-rolled DASH MPD (audio drifted -25s → -44s behind +video on long-form content), pivoted to delegating playback to the +already-installed `plugin.video.youtube` v7.4.3. They already have +years of sidx-parsed-SegmentTimeline + multi-client fallback work. +Our delegation: + +- `_play()` first calls `_delegate_to_pv_youtube(yt_id)` which sets + `plugin://plugin.video.youtube/play/?video_id=` via setResolvedUrl +- Kodi chain-resolves to pv.youtube, which builds the proper MPD +- inputstream.adaptive plays 1080p H.264 cleanly, audio in sync +- Multi-client Innertube fallback handles "Please sign in" rejection + on the `tv_unplugged` client — succeeds on the next client without + needing the user to link an account + +Verified live on Livingroom Pi 2026-05-23 — LTT 'Trump Phone' video +played at 1080p with audio, fullscreen. + +Settings: `prefer_pv_youtube` (default true). Disable to fall through +to our native DASH/progressive paths. + +## M7-rejected — native DASH builder [PARKED] - [x] sidecar `resolve_dash` op returns rustypipe's full `video_only_streams` + `audio_streams` arrays (16+ representations) diff --git a/addon/plugin.video.torttube/addon.xml b/addon/plugin.video.torttube/addon.xml index 87167b2..ba0fd69 100644 --- a/addon/plugin.video.torttube/addon.xml +++ b/addon/plugin.video.torttube/addon.xml @@ -1,7 +1,7 @@ diff --git a/addon/plugin.video.torttube/main.py b/addon/plugin.video.torttube/main.py index e2e1ad0..67165a4 100644 --- a/addon/plugin.video.torttube/main.py +++ b/addon/plugin.video.torttube/main.py @@ -152,6 +152,16 @@ class _MpdHandler(http.server.BaseHTTPRequestHandler): return +def _has_setting(setting_id: str) -> bool: + """Best-effort check that a setting exists in resources/settings.xml. + Returns False if the lookup throws (older builds, missing schema).""" + try: + ADDON.getSetting(setting_id) + return True + except Exception: + return False + + def _lan_ip() -> str: """Detect this host's LAN IP by opening a UDP socket toward an external address (no packets actually sent — just lets the kernel pick the source IP). @@ -295,9 +305,14 @@ def _build_dash_mpd( parts.append( f' ' ) + # NOTE: NOT setting audioSamplingRate here on purpose. rustypipe doesn't + # expose the sample rate, and hard-coding 44100 caused a ~9% playback-rate + # mismatch (= growing audio-vs-video desync) for content at 48000 Hz. + # inputstream.adaptive reads the actual rate from the audio init segment's + # mdhd box when this attribute is omitted, which is correct for any source. parts.append( f' ' + f' bandwidth="{a.get("bitrate", 0)}" startWithSAP="1">' ) parts.append( ' tuple[bytes | None, dict[str, Any]]: return mpd.encode("utf-8"), resp -def _play(yt_id: str) -> None: - """Resolve via DASH (rustypipe, up to 1080p H.264) with progressive - yt-dlp fallback (360p). +def _delegate_to_pv_youtube(yt_id: str) -> bool: + """Hand playback off to plugin.video.youtube via its play URL. They have + the proper SegmentTimeline-aware MPD construction (sidx-parsed) that + unlocks HD without the audio-sync drift our naive MPD has. Returns True + if delegation succeeded (Kodi will chain-resolve).""" + if not _pv_youtube_installed(): + return False + target = f"plugin://plugin.video.youtube/play/?video_id={yt_id}" + _log(f"delegating playback to plugin.video.youtube: {target}") + li = xbmcgui.ListItem(label=yt_id) + li.setPath(target) + li.setProperty("IsPlayable", "true") + xbmcplugin.setResolvedUrl(_HANDLE, True, li) + return True - DASH path is gated behind TORTTUBE_DASH=1 while the manifest-serving - HTTP server + inputstream.adaptive integration is being stabilized - (file:// URLs don't work, and rapid retries via a port-0 HTTP server - can trigger Kodi's 'two concurrent busydialogs' fatal). Default OFF - until that's solid — progressive yt-dlp path is reliable. + +def _pv_youtube_installed() -> bool: + """Check whether plugin.video.youtube is installed + enabled. We don't + enable it ourselves — if the user removed it, we fall back to our own + paths.""" + try: + return bool(xbmc.getCondVisibility("System.HasAddon(plugin.video.youtube)")) + except Exception: + return False + + +def _play(yt_id: str) -> None: + """Resolve playback in order of preference: + 1. plugin.video.youtube delegation — HD via their proven DASH MPD with + sidx-parsed SegmentTimeline. Default, when available. + 2. Our DASH path — only if `dash_enabled` setting / dash.on marker / env + are set (WIP, partial — see M7 milestone). + 3. yt-dlp progressive — last-resort 360p, always works. + SponsorBlock attaches regardless of which path we took. """ _log(f"play id={yt_id}") + # Tier 0: delegate to plugin.video.youtube if installed. Don't run our + # SponsorBlock monitor in this path — pv.youtube has its own and would + # double-skip if both fire. + use_pv_youtube = True + try: + use_pv_youtube = ADDON.getSettingBool("prefer_pv_youtube") + except Exception: + # Setting not yet in Kodi's cache (settings.xml just changed) — default on. + pass + if use_pv_youtube and _delegate_to_pv_youtube(yt_id): + return + mpd_bytes: bytes | None = None dash_resp: dict[str, Any] = {} # DASH path: read setting first; fall back to env-var; OR honor a magic file diff --git a/addon/plugin.video.torttube/resources/settings.xml b/addon/plugin.video.torttube/resources/settings.xml index 01d5095..0749b6a 100644 --- a/addon/plugin.video.torttube/resources/settings.xml +++ b/addon/plugin.video.torttube/resources/settings.xml @@ -3,7 +3,10 @@
- + + true + + false diff --git a/docs/upstream.md b/docs/upstream.md index e085255..dc304f0 100644 --- a/docs/upstream.md +++ b/docs/upstream.md @@ -25,6 +25,18 @@ _(none yet — opens with M1 development)_ to setResolvedUrl. plugin.video.youtube uses this pattern via a long-lived service addon. Worth filing an enhancement to either accept `file://` or document the LAN-IP HTTP-server pattern in the inputstream.adaptive docs. +- **Architecture resolution: torttube delegates playback to plugin.video.youtube** + 2026-05-23. After spending an iteration on a native DASH MPD builder + HTTP + manifest server, conceded that solving segment-timing alignment correctly + (sidx-parse, SegmentTimeline emission, audio-rate detection, init-segment + presentationTimeOffset) is multiple-week work. plugin.video.youtube has + years of that work already. torttube now does what it's faster at (Rust + rustypipe search/browse, SponsorBlock skip with fewer false positives) + and hands the video_id to pv.youtube for actual playback. This is the + "stand on giants' shoulders" call rather than reinvent. Native DASH code + path stays in the addon as a fallback when pv.youtube is absent (or as a + testbed for someday revisiting). Reference: https://kodi.wiki/view/Add-on:YouTube + - **DASH segment timing for googlevideo SegmentBase URLs** — Hit 2026-05-23. My MPD with one Representation per video/audio (using SegmentBase with indexRange to the sidx box of the static MP4) parses cleanly and segments