From 63962b29b5ebd5c1d40cf56844dcbf1f770245a1 Mon Sep 17 00:00:00 2001 From: Kayos Date: Sat, 23 May 2026 16:31:59 -0700 Subject: [PATCH] =?UTF-8?q?v1.0.4=20=E2=80=94=20fix=20mid-play=20swap:=20d?= =?UTF-8?q?on't=20block=20Player=20callback=20thread?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit onAVStarted was calling _fetch_sb_segments synchronously, which subprocess.run()'s our sidecar — up to 8s of blocking on Kodi's serialized player event thread. When the user started a new video while one was playing, pv.youtube's stream resolve for the new video raced with our blocked callback and the new play got dropped as "unplayable item" before pv.youtube could finish. Moved the segment fetch + skip loop into a background thread that starts from onAVStarted and returns instantly. Player callbacks now clear in microseconds. --- addon/plugin.video.torttube/addon.xml | 2 +- addon/plugin.video.torttube/service.py | 33 ++++++++++++++++---------- sidecar/Cargo.toml | 2 +- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/addon/plugin.video.torttube/addon.xml b/addon/plugin.video.torttube/addon.xml index 9a2a599..7cb27f7 100644 --- a/addon/plugin.video.torttube/addon.xml +++ b/addon/plugin.video.torttube/addon.xml @@ -1,7 +1,7 @@ diff --git a/addon/plugin.video.torttube/service.py b/addon/plugin.video.torttube/service.py index eda9e30..fb50565 100644 --- a/addon/plugin.video.torttube/service.py +++ b/addon/plugin.video.torttube/service.py @@ -89,6 +89,10 @@ class TorttubePlayerMonitor(xbmc.Player): self._stop_event = threading.Event() def onAVStarted(self) -> None: + # Must return fast — Kodi's player event dispatch is serialized and + # blocking here while pv.youtube is mid-resolve can drop the next + # play. The actual segment fetch + skip loop runs in a background + # thread. try: playing_file = self.getPlayingFile() except Exception: @@ -98,26 +102,29 @@ class TorttubePlayerMonitor(xbmc.Player): return with self._lock: if yt_id == self._current_id: - # Same video — segments already armed, skip thread alive. return self._stop_event.set() self._current_id = yt_id + stop_event = threading.Event() + self._stop_event = stop_event + self._skip_thread = threading.Thread( + target=self._arm_and_skip, + args=(yt_id, playing_file, stop_event), + daemon=True, + ) + self._skip_thread.start() + def _arm_and_skip( + self, + yt_id: str, + playing_file: str, + stop_event: threading.Event, + ) -> None: segments = _fetch_sb_segments(yt_id) _log(f"armed {len(segments)} segments for {yt_id} (file={playing_file[:80]})") - if not segments: + if not segments or stop_event.is_set(): return - - stop_event = threading.Event() - thread = threading.Thread( - target=self._skip_loop, - args=(yt_id, segments, stop_event), - daemon=True, - ) - with self._lock: - self._stop_event = stop_event - self._skip_thread = thread - thread.start() + self._skip_loop(yt_id, segments, stop_event) def onPlayBackStopped(self) -> None: self._stop_event.set() diff --git a/sidecar/Cargo.toml b/sidecar/Cargo.toml index ba12993..b60e6ff 100644 --- a/sidecar/Cargo.toml +++ b/sidecar/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/torttube-sidecar"] [workspace.package] -version = "1.0.3" +version = "1.0.4" edition = "2021" license = "GPL-3.0-or-later" authors = ["Cobb "]