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
153 lines
8.2 KiB
Markdown
153 lines
8.2 KiB
Markdown
# torttube milestones
|
|
|
|
## M0 — Scaffold [current]
|
|
|
|
- [x] `Sulkta-Coop/torttube` on LAN Gitea
|
|
- [x] Layout: `addon/` (Python) + `sidecar/` (Rust workspace)
|
|
- [x] 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
|
|
- [x] `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]
|
|
|
|
- [x] sidecar `search` op via rustypipe `query().search::<VideoItem,_>()`
|
|
- [x] root directory listing in Kodi addon (Search + Play by URL entries)
|
|
- [x] `?action=search` accepts inline `q=` (for JSON-RPC) or prompts keyboard
|
|
- [x] result labels show title · channel · duration · view-count, with
|
|
`IsPlayable=true`, thumbnails, video InfoLabels
|
|
- [x] verified via JSON-RPC: `Files.GetDirectory` returns 19+ formatted
|
|
results for "linus tech tips"
|
|
- [x] channel browse → `{"op":"channel_videos","id":"…"}` — verified 30
|
|
videos returned for LTT's UCXuqSBlHAE6Xw-yeJA0Tunw via JSON-RPC
|
|
- [x] context-menu entry on every result: "Go to <channel>" → channel listing
|
|
- [x] 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]
|
|
|
|
- [x] `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
|
|
- [x] toast on skip (`SponsorBlock — Skipped <category> (<duration>s)`)
|
|
- [x] 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]
|
|
|
|
- [x] 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`.
|
|
- [x] 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).
|
|
- [x] zip layout matches Kodi "install from zip" expectations
|
|
- [x] addon.zip dropped at `smb://lucy/downloads/torttube/`
|
|
- [x] install + smoke recipe documented at `docs/install.md`
|
|
- [x] **installed on Livingroom Pi** (`192.168.0.158`) via SSH +
|
|
`systemctl restart kodi` + `Addons.SetAddonEnabled`
|
|
- [x] **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]
|
|
|
|
- [x] sidecar `resolve_dash` op returns rustypipe's full
|
|
`video_only_streams` + `audio_streams` arrays (16+ representations)
|
|
- [x] 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`
|
|
- [x] **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**.
|
|
- [x] `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.
|
|
- [x] 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
|
|
- [x] **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](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
|