dev: YouTube HD via inputstream.adaptive, DokuStreams Suche fix

This commit is contained in:
2026-03-14 23:34:09 +01:00
parent d51505e004
commit ea9ceec34c
6 changed files with 416 additions and 95 deletions

View File

@@ -3965,6 +3965,190 @@ def _resolve_stream_with_retry(plugin: BasisPlugin, link: str) -> str | None:
return final_link
def _is_inputstream_adaptive_available() -> bool:
"""Prueft ob inputstream.adaptive in Kodi installiert ist."""
try:
import xbmcaddon # type: ignore
xbmcaddon.Addon("inputstream.adaptive")
return True
except Exception:
return False
# ---------------------------------------------------------------------------
# Lokaler MPD-Manifest-Server fuer inputstream.adaptive
# ---------------------------------------------------------------------------
_mpd_server_instance = None
_mpd_server_port = 0
def _ensure_mpd_server() -> int:
"""Startet einen lokalen HTTP-Server der MPD-Manifeste serviert.
Gibt den Port zurueck. Der Server laeuft in einem Daemon-Thread.
"""
global _mpd_server_instance, _mpd_server_port
if _mpd_server_instance is not None:
return _mpd_server_port
import http.server
import socketserver
import threading
_pending_manifests: dict[str, str] = {}
class _ManifestHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self) -> None:
if "/manifest" in self.path:
key = self.path.split("key=")[-1].split("&")[0] if "key=" in self.path else ""
content = _pending_manifests.pop(key, "")
if content:
data = content.encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/dash+xml")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
return
self.send_error(404)
def log_message(self, *_args: object) -> None:
pass # kein Logging
server = socketserver.TCPServer(("127.0.0.1", 0), _ManifestHandler)
_mpd_server_port = server.server_address[1]
_mpd_server_instance = server
# pending_manifests als Attribut am Server speichern
server._pending_manifests = _pending_manifests # type: ignore[attr-defined]
t = threading.Thread(target=server.serve_forever, daemon=True)
t.start()
_log(f"MPD-Server gestartet auf Port {_mpd_server_port}", xbmc.LOGDEBUG)
return _mpd_server_port
def _register_mpd_manifest(mpd_xml: str) -> str:
"""Registriert ein MPD-Manifest und gibt die lokale URL zurueck."""
import hashlib
port = _ensure_mpd_server()
key = hashlib.md5(mpd_xml.encode()).hexdigest()[:12]
if _mpd_server_instance is not None:
_mpd_server_instance._pending_manifests[key] = mpd_xml # type: ignore[attr-defined]
return f"http://127.0.0.1:{port}/plugin.video.viewit/manifest?key={key}"
def _play_dual_stream(
video_url: str,
audio_url: str,
*,
meta: dict[str, str] | None = None,
display_title: str | None = None,
info_labels: dict[str, str] | None = None,
art: dict[str, str] | None = None,
cast: list[TmdbCastMember] | None = None,
resolve_handle: int | None = None,
trakt_media: dict[str, object] | None = None,
) -> None:
"""Spielt getrennte Video+Audio-Streams via inputstream.adaptive.
Startet einen lokalen HTTP-Server der ein generiertes MPD-Manifest
serviert (gem. inputstream.adaptive Wiki: Integration + Custom Manifest).
Fallback auf Video-only wenn inputstream.adaptive nicht installiert.
"""
if not _is_inputstream_adaptive_available():
_log("inputstream.adaptive nicht verfuegbar Video-only Wiedergabe", xbmc.LOGWARNING)
_play_final_link(
video_url, display_title=display_title, info_labels=info_labels,
art=art, cast=cast, resolve_handle=resolve_handle, trakt_media=trakt_media,
)
return
from xml.sax.saxutils import escape as xml_escape
m = meta or {}
vcodec = m.get("vc", "avc1.640028")
acodec = m.get("ac", "mp4a.40.2")
w = m.get("w", "1920")
h = m.get("h", "1080")
fps = m.get("fps", "25")
vbr = m.get("vbr", "5000000")
abr = m.get("abr", "128000")
asr = m.get("asr", "44100")
ach = m.get("ach", "2")
dur = m.get("dur", "0")
dur_attr = ""
if dur and dur != "0":
dur_attr = f' mediaPresentationDuration="PT{dur}S"'
mpd_xml = (
'<?xml version="1.0" encoding="UTF-8"?>'
'<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" type="static"'
' minBufferTime="PT2S"'
' profiles="urn:mpeg:dash:profile:isoff-on-demand:2011"'
+ dur_attr + '>'
'<Period>'
'<AdaptationSet mimeType="video/mp4" contentType="video" subsegmentAlignment="true">'
'<Representation id="video" bandwidth="' + vbr + '"'
' codecs="' + xml_escape(vcodec) + '"'
' width="' + w + '" height="' + h + '"'
' frameRate="' + fps + '">'
'<BaseURL>' + xml_escape(video_url) + '</BaseURL>'
'</Representation>'
'</AdaptationSet>'
'<AdaptationSet mimeType="audio/mp4" contentType="audio" subsegmentAlignment="true">'
'<Representation id="audio" bandwidth="' + abr + '"'
' codecs="' + xml_escape(acodec) + '"'
' audioSamplingRate="' + asr + '">'
'<AudioChannelConfiguration'
' schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011"'
' value="' + ach + '"/>'
'<BaseURL>' + xml_escape(audio_url) + '</BaseURL>'
'</Representation>'
'</AdaptationSet>'
'</Period>'
'</MPD>'
)
mpd_url = _register_mpd_manifest(mpd_xml)
_log(f"MPD-Manifest URL: {mpd_url}", xbmc.LOGDEBUG)
list_item = xbmcgui.ListItem(label=display_title or "", path=mpd_url)
list_item.setMimeType("application/dash+xml")
list_item.setContentLookup(False)
list_item.setProperty("inputstream", "inputstream.adaptive")
list_item.setProperty("inputstream.adaptive.manifest_type", "mpd")
merged_info: dict[str, object] = dict(info_labels or {})
if display_title:
merged_info["title"] = display_title
_apply_video_info(list_item, merged_info, cast)
if art:
setter = getattr(list_item, "setArt", None)
if callable(setter):
try:
setter(art)
except Exception:
pass
resolved = False
if resolve_handle is not None:
resolver = getattr(xbmcplugin, "setResolvedUrl", None)
if callable(resolver):
try:
resolver(resolve_handle, True, list_item)
resolved = True
except Exception:
pass
if not resolved:
xbmc.Player().play(item=mpd_url, listitem=list_item)
if trakt_media and _get_setting_bool("trakt_enabled", default=False):
_trakt_scrobble_start_async(trakt_media)
_trakt_monitor_playback(trakt_media)
def _play_final_link(
link: str,
*,
@@ -3975,6 +4159,25 @@ def _play_final_link(
resolve_handle: int | None = None,
trakt_media: dict[str, object] | None = None,
) -> None:
# Getrennte Video+Audio-Streams (yt-dlp): via inputstream.adaptive abspielen
audio_url = None
meta: dict[str, str] = {}
try:
from ytdlp_helper import split_video_audio
link, audio_url, meta = split_video_audio(link)
except Exception:
pass
if audio_url:
_play_dual_stream(
link, audio_url,
meta=meta,
display_title=display_title, info_labels=info_labels,
art=art, cast=cast, resolve_handle=resolve_handle,
trakt_media=trakt_media,
)
return
list_item = xbmcgui.ListItem(label=display_title or "", path=link)
try:
list_item.setProperty("IsPlayable", "true")