torttube/MILESTONES.md
Kayos 68d07c1e2e M7 DONE via delegation — pv.youtube plays HD with audio
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=<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
2026-05-23 11:58:12 -07:00

8.2 KiB

torttube milestones

M0 — Scaffold [current]

  • Sulkta-Coop/torttube on LAN Gitea
  • Layout: addon/ (Python) + sidecar/ (Rust workspace)
  • GPL-3.0 license headers
  • crafting-table build target produces a static aarch64 sidecar binary

M1 — sidecar resolve (three-tier)

  • reads {"op":"resolve","id":"<yt-id>"} from stdin
  • Tier 1: rustypipe → {"streams":[…],"title":"…","duration_s":N,"source":"rustypipe"}
  • Tier 2: on Tier-1 failure, shell out to yt-dlp -j <url> → same JSON shape with "source":"yt-dlp"
  • Tier 3: new op {"op":"rip","id":"<yt-id>","dest":"/storage/.kodi/temp/torttube/<id>.<ext>"} invoked by addon on Tier-1+2 stream failures or 403-mid-play; yt-dlp downloads file, sidecar returns local path
  • typed errors for age-restricted / region-restricted / private (not panics)
  • sig decoding verified against a known-good video
  • DASH manifest URL or per-itag direct stream URL — whichever inputstream.adaptive prefers

M2 — SponsorBlock

  • {"op":"sponsorblock","id":"<yt-id>"} → segments array
  • SHA-256 prefix lookup (privacy-preserving — only send first 4 hex chars)
  • category filter honoured from addon settings (skip / mute / show only)
  • cache per-session

M3 — Kodi addon plays one video

  • addon.xml + main.py register as video plugin
  • main.py handles plugin://plugin.video.torttube/?action=play&id=<id> and ?url=<full-url> — wired so JSON-RPC Player.Open from any LAN client (phone, HA, curl) triggers resolve + play. See docs/remote-control.md.
  • cross-compile sidecar for aarch64, drop into bin/ of addon dir
  • install + smoke on LibreELEC RPi at 192.168.0.158
  • (later) hardcoded list of 3 test videos for in-Kodi navigation

M4 — search + channel browse [PARTIAL]

  • sidecar search op via rustypipe query().search::<VideoItem,_>()
  • root directory listing in Kodi addon (Search + Play by URL entries)
  • ?action=search accepts inline q= (for JSON-RPC) or prompts keyboard
  • result labels show title · channel · duration · view-count, with IsPlayable=true, thumbnails, video InfoLabels
  • verified via JSON-RPC: Files.GetDirectory returns 19+ formatted results for "linus tech tips"
  • channel browse → {"op":"channel_videos","id":"…"} — verified 30 videos returned for LTT's UCXuqSBlHAE6Xw-yeJA0Tunw via JSON-RPC
  • context-menu entry on every result: "Go to " → channel listing
  • playlist browse → {"op":"playlist","id":"…"} — verified with LTT's "Consumer Advocacy" playlist, returns playlist metadata (name, channel, video_count) + items array
  • paginated results (currently capped at limit=30/50/100)
  • search history

M5 — SponsorBlock skipping [DONE]

  • SponsorBlockMonitor (xbmc.Monitor subclass) polls Player().getTime() every 0.5s after playback starts; seeks past each segment exactly once (UUID dedup); exits cleanly on abort or playback stop
  • toast on skip (SponsorBlock — Skipped <category> (<duration>s))
  • only segments with actionType: skip are honored (mute/etc. ignored for now — adding those is a multi-pass M5+ pass)
  • category toggles + skip-counter in settings.xml (deferred — defaults currently: sponsor, selfpromo, interaction skipped automatically)

M6 — install + cross-compile [DONE]

  • cross-compile sidecar for aarch64-musl static via throwaway messense/rust-musl-cross:aarch64-musl container. 6.2MB stripped static binary. Builds clean from scripts/build-addon-zip.sh.
  • bundle yt-dlp's universal Python zipapp (~3MB, not the PyInstaller binary — that breaks on LibreELEC's armhf userspace because the aarch64 dynamic loader doesn't exist; Python zipapp runs on /usr/bin/python3 which is always there).
  • zip layout matches Kodi "install from zip" expectations
  • addon.zip dropped at smb://lucy/downloads/torttube/
  • install + smoke recipe documented at docs/install.md
  • installed on Livingroom Pi (192.168.0.158) via SSH + systemctl restart kodi + Addons.SetAddonEnabled
  • JSON-RPC Player.Open smoke verified — Rick Astley played end-to-end with audio + video synced (yt-dlp -f best[ext=mp4]/best picked format 18, 360p H.264+AAC progressive)
  • armv7 build for older Pis (deferred — current TV is aarch64-capable and my static sidecar runs on it even though userspace is armhf)
  • 720p+ playback (DASH WIP — see "M7 — DASH/HD" below) — itag 22 (720p progressive) is deprecated by YouTube; higher quality needs DASH manifest generation. Code path exists, gated behind TORTTUBE_DASH=1.

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=<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]

  • sidecar resolve_dash op returns rustypipe's full video_only_streams + audio_streams arrays (16+ representations)
  • addon _build_dash_mpd constructs valid on-demand MPD with H.264 video reps (filtered 720p-1080p so inputstream.adaptive's conservative chooser doesn't land below HD) + best AAC audio rep, XML-escaped URLs, proper SegmentBase indexRange + Initialization range
  • serve MPD over localhost HTTP — ThreadingHTTPServer binds to LAN IP (not 127.0.0.1 — that fails curl auth in Kodi 20). Lifecycle: server stays up until SponsorBlockMonitor exits, then server.shutdown() in finally block. Verified live: inputstream.adaptive parses the MPD cleanly.
  • inputstream.adaptive.stream_headers set with User-Agent, Origin, and Referer matching what rustypipe / yt-dlp use when minting the URL — fixes the 403 Forbidden from googlevideo on segment GETs
  • segment timing alignment — audio drifts -25 → -44s behind video within seconds. Need explicit <SegmentTimeline> per-segment timing derived from each representation's sidx box, OR presentationTimeOffset. Plugin.video.youtube derives these from sidx — we'd need to fetch + parse that. See docs/upstream.md for the upstream PR target.
  • graceful fallback to progressive — if the DASH path returns no MPD bytes (rustypipe error, no H.264 reps, etc.) the addon falls through to yt-dlp progressive 360p
  • gated behind dash_enabled setting (default off) and a dash.on marker file in addon_data (workaround for Kodi's settings cache, useful for ad-hoc testing). Stable 360p path remains the default until segment timing is solved.

Upstream PR work (parallel lane — every bug evaluated for "fix it upstream?")

This isn't a separate milestone, it's a posture. Every sidecar bug we diagnose: ask "is this rustypipe / NPE / yt-dlp's bug?" — if yes, fix lands upstream too. Log every filed PR in docs/upstream.md.

Opening shortlist:

  • rustypipe PR #77 — review + help land
  • NPE #1357 (JDoc) — smallest credible PR, opens the relationship
  • NPE #1444 (typed errors) — clean signal/contract change
  • NPE #1360 (refactor link handlers, "help wanted")
  • rustypipe ↔ NPE port — anything one project has that the other lacks