addon: switch play action to resolve_play (yt-dlp combined format)

Realized during M6 packaging that the rustypipe path returns separate
audio + video DASH streams (Opus 251 + AV1 401 on the smoke video). Kodi
can't sync those without an inputstream.adaptive DASH manifest, which
would need server-side manifest generation — M3+ territory.

Stopgap for shippable M3: new sidecar op resolve_play that asks yt-dlp
for -f best[ext=mp4]/best — one combined audio+video URL Kodi plays as
plain HTTP. ~3-5s overhead vs rustypipe but reliable sync.

main.py _play() now calls resolve_play. resolve still exists for
metadata + browse paths (M4 will use it).

Rebuilt aarch64-musl binary, repackaged plugin.video.torttube-0.0.1.zip
(38.7MB, md5 f2c08aed130b1c1bd231a9b6cbfac93c). Live at:
  smb://fileserver/downloads/torttube/plugin.video.torttube-0.0.1.zip
This commit is contained in:
Sulkta 2026-05-23 08:59:25 -07:00
parent 2df2c0622a
commit 55f8fc7c06
3 changed files with 76 additions and 33 deletions

View file

@ -105,12 +105,15 @@ def _resolved_listitem(stream_url: str, title: str | None) -> xbmcgui.ListItem:
def _play(yt_id: str) -> None:
"""Resolve via sidecar, hand the URL to Kodi's player."""
"""Resolve via sidecar (yt-dlp combined-format path), hand URL to Kodi."""
_log(f"play id={yt_id}")
try:
resp = _call_sidecar({"op": "resolve", "id": yt_id})
# resolve_play returns ONE combined audio+video URL — guaranteed
# to play in Kodi without needing inputstream.adaptive/DASH.
# ~3-5s overhead vs rustypipe but reliable audio sync.
resp = _call_sidecar({"op": "resolve_play", "id": yt_id}, timeout_s=45)
except Exception as e:
_log(f"sidecar resolve failed: {e}", xbmc.LOGERROR)
_log(f"sidecar resolve_play failed: {e}", xbmc.LOGERROR)
xbmcgui.Dialog().notification(
"torttube", f"resolve failed: {e}", xbmcgui.NOTIFICATION_ERROR, 5000
)
@ -127,34 +130,8 @@ def _play(yt_id: str) -> None:
xbmcplugin.setResolvedUrl(_HANDLE, False, xbmcgui.ListItem())
return
# Pick a URL. Preference order for M3:
# 1. video_stream.url if it has embedded audio (yt-dlp combined formats)
# 2. video_stream.url (will need audio mux in a later milestone)
# 3. audio_stream.url (audio-only playback)
stream_url = None
title = None
source = resp.get("source", "?")
if source == "rustypipe":
details = resp.get("details") or {}
title = details.get("name") or details.get("title")
vs = resp.get("video_stream")
as_ = resp.get("audio_stream")
# rustypipe separates audio + video. For M3 we play video_stream;
# M3+ will wire a DASH manifest or merged-format selection for sync.
if vs and vs.get("url"):
stream_url = vs["url"]
elif as_ and as_.get("url"):
stream_url = as_["url"]
else: # yt-dlp tier 2
title = resp.get("title")
streams = resp.get("streams") or []
# yt-dlp's combined formats come back as entries with both audio + video.
combined = [s for s in streams if not s.get("is_audio_only") and not s.get("is_video_only")]
candidates = combined or streams
if candidates:
stream_url = candidates[0].get("url")
stream_url = resp.get("stream_url")
title = resp.get("title")
if not stream_url:
_log("no usable stream URL in sidecar response", xbmc.LOGERROR)
xbmcgui.Dialog().notification(
@ -163,7 +140,7 @@ def _play(yt_id: str) -> None:
xbmcplugin.setResolvedUrl(_HANDLE, False, xbmcgui.ListItem())
return
_log(f"resolved via {source}, playing")
_log(f"resolved via {resp.get('source')}, playing")
xbmcplugin.setResolvedUrl(_HANDLE, True, _resolved_listitem(stream_url, title))