M7 — DASH partial: MPD serves OK, segment alignment still WIP
Big strides today: - Sidecar resolve_dash op works (verified live on Pi) - MPD builder generates valid MPEG-DASH on-demand manifest with H.264 720p/1080p video reps + best AAC audio rep - ThreadingHTTPServer serves the MPD over the LAN IP (not 127.0.0.1 — curl in Kodi 20's inputstream.adaptive can't open that) - inputstream.adaptive PARSES our manifest cleanly: 'Successfully parsed manifest file (Periods: 1, Streams in first period: 2)' - Segment GETs work once we set stream_headers with User-Agent + Origin + Referer (otherwise googlevideo 403s the audio segments) Remaining issue: - Audio drifts -25s → -44s behind video within seconds of playback start. inputstream.adaptive needs explicit SegmentTimeline timing derived from each rep's sidx box to stay aligned. Plugin.video.youtube does this; we'd need to fetch+parse sidx ourselves or fork their MPD-builder. Documented as M7-blocking + upstream PR candidate. Default remains the stable yt-dlp progressive 360p path. DASH is behind dash_enabled setting OR a dash.on marker file in addon_data. Toggle on via: ssh <kodi> 'touch /storage/.kodi/userdata/addon_data/plugin.video.torttube/dash.on' Toggle off: ssh <kodi> 'rm /storage/.kodi/userdata/addon_data/plugin.video.torttube/dash.on' Addon v0.0.10. docs/upstream.md has the full segment-timing analysis.
This commit is contained in:
parent
a784321759
commit
0a289fea3a
4 changed files with 117 additions and 31 deletions
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.video.torttube"
|
||||
name="torttube"
|
||||
version="0.0.9"
|
||||
version="0.0.10"
|
||||
provider-name="Sulkta-Coop">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="3.0.0"/>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import http.server
|
|||
import json
|
||||
import os
|
||||
import re
|
||||
import socketserver
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
|
@ -112,6 +112,20 @@ def _resolved_listitem(stream_url: str, title: str | None) -> xbmcgui.ListItem:
|
|||
if ".mpd" in stream_url:
|
||||
li.setProperty("inputstream", "inputstream.adaptive")
|
||||
li.setProperty("inputstream.adaptive.manifest_type", "mpd")
|
||||
# googlevideo rejects segment GETs that don't carry an Origin/Referer
|
||||
# from www.youtube.com — 403 Forbidden otherwise. Same Mozilla UA
|
||||
# rustypipe / yt-dlp use when minting the URL.
|
||||
ytdl_ua = (
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
)
|
||||
seg_headers = (
|
||||
f"User-Agent={ytdl_ua}"
|
||||
"&Origin=https://www.youtube.com"
|
||||
"&Referer=https://www.youtube.com/"
|
||||
)
|
||||
li.setProperty("inputstream.adaptive.stream_headers", seg_headers)
|
||||
li.setProperty("inputstream.adaptive.manifest_headers", seg_headers)
|
||||
elif ".m3u8" in stream_url:
|
||||
li.setProperty("inputstream", "inputstream.adaptive")
|
||||
li.setProperty("inputstream.adaptive.manifest_type", "hls")
|
||||
|
|
@ -138,24 +152,40 @@ class _MpdHandler(http.server.BaseHTTPRequestHandler):
|
|||
return
|
||||
|
||||
|
||||
def _start_mpd_server(mpd_bytes: bytes) -> tuple[str, http.server.HTTPServer]:
|
||||
"""Spin up a one-shot localhost HTTP server that serves `mpd_bytes` at any
|
||||
path. Returns (url, server). Caller is responsible for `server.shutdown()`
|
||||
once playback ends."""
|
||||
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).
|
||||
plugin.video.youtube uses this same trick because inputstream.adaptive's
|
||||
libcurl in Kodi 20 has trouble fetching from `127.0.0.1` reliably."""
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
s.connect(("8.8.8.8", 80))
|
||||
return s.getsockname()[0]
|
||||
except Exception:
|
||||
return "127.0.0.1"
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
|
||||
def _start_mpd_server(mpd_bytes: bytes) -> tuple[str, http.server.HTTPServer]:
|
||||
"""Spin up a one-shot HTTP server that serves `mpd_bytes` at any path.
|
||||
Binds to the LAN IP so inputstream.adaptive can fetch via the same code
|
||||
path it uses for real network URLs. Returns (url, server) — caller is
|
||||
responsible for `server.shutdown()` once playback ends."""
|
||||
|
||||
# Per-server handler subclass so the closure captures the bytes for THIS
|
||||
# video without leaking state to a concurrent invocation.
|
||||
handler_cls = type(
|
||||
"_MpdHandlerInstance",
|
||||
(_MpdHandler,),
|
||||
{"mpd_bytes": mpd_bytes},
|
||||
)
|
||||
# Port 0 → kernel picks free port. ThreadingHTTPServer so multiple GETs
|
||||
# (the manifest + range requests, if any) don't serialize.
|
||||
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler_cls)
|
||||
lan_ip = _lan_ip()
|
||||
# Bind to all interfaces so the LAN-IP URL also routes through (otherwise
|
||||
# connect-by-IP on the same host can return 'connection refused' on some
|
||||
# kernel/firewall configs). Port 0 → kernel picks free port.
|
||||
server = http.server.ThreadingHTTPServer(("0.0.0.0", 0), handler_cls)
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
port = server.server_address[1]
|
||||
url = f"http://127.0.0.1:{port}/manifest.mpd"
|
||||
url = f"http://{lan_ip}:{port}/manifest.mpd"
|
||||
_log(f"MPD HTTP server up on {url}")
|
||||
return url, server
|
||||
|
||||
|
|
@ -189,16 +219,30 @@ def _build_dash_mpd(
|
|||
if duration_s <= 0:
|
||||
return None
|
||||
|
||||
# Filter video: H.264 only (avc1.*), height <= 1080 (RPi 4 H.264 ceiling).
|
||||
# Filter video: H.264 only (avc1.*), 720p <= height <= 1080p.
|
||||
# Floor at 720p so inputstream.adaptive's conservative-start chooser doesn't
|
||||
# land us on a low-quality rep first. Ceiling at 1080p because that's the
|
||||
# RPi 4's H.264 hardware-decode sweet spot.
|
||||
h264 = [
|
||||
s
|
||||
for s in video_streams
|
||||
if "avc1" in (s.get("codec") or s.get("mime", ""))
|
||||
and (s.get("height") or 0) <= 1080
|
||||
and 720 <= (s.get("height") or 0) <= 1080
|
||||
and s.get("init_range")
|
||||
and s.get("index_range")
|
||||
and s.get("url")
|
||||
]
|
||||
if not h264:
|
||||
# Fallback: drop the 720p floor if the video has no HD streams at all.
|
||||
h264 = [
|
||||
s
|
||||
for s in video_streams
|
||||
if "avc1" in (s.get("codec") or s.get("mime", ""))
|
||||
and (s.get("height") or 0) <= 1080
|
||||
and s.get("init_range")
|
||||
and s.get("index_range")
|
||||
and s.get("url")
|
||||
]
|
||||
if not h264:
|
||||
return None
|
||||
|
||||
|
|
@ -314,14 +358,26 @@ def _play(yt_id: str) -> None:
|
|||
|
||||
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
|
||||
# at /storage/.kodi/userdata/addon_data/plugin.video.torttube/dash.on as a
|
||||
# last-ditch trigger that doesn't depend on Kodi's settings cache.
|
||||
dash_enabled = False
|
||||
try:
|
||||
dash_enabled = ADDON.getSettingBool("dash_enabled")
|
||||
except Exception:
|
||||
# Setting might not exist on older configs; treat as off.
|
||||
pass
|
||||
if dash_enabled or os.environ.get("TORTTUBE_DASH") == "1":
|
||||
env_dash = os.environ.get("TORTTUBE_DASH") == "1"
|
||||
addon_data = ""
|
||||
try:
|
||||
import xbmcvfs
|
||||
addon_data = xbmcvfs.translatePath("special://profile/addon_data/plugin.video.torttube/")
|
||||
except Exception:
|
||||
pass
|
||||
file_dash = bool(addon_data) and os.path.isfile(os.path.join(addon_data, "dash.on"))
|
||||
_log(f"play id={yt_id} dash_enabled={dash_enabled} env_dash={env_dash} file_dash={file_dash}")
|
||||
if dash_enabled or env_dash or file_dash:
|
||||
mpd_bytes, dash_resp = _try_dash(yt_id)
|
||||
_log(f"_try_dash returned mpd_bytes={'<%d bytes>' % len(mpd_bytes) if mpd_bytes else None}")
|
||||
if mpd_bytes:
|
||||
details = dash_resp.get("details") or {}
|
||||
title = details.get("name")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue