From 566af4a5863353ce7ca8d1c7f88b9236528f804e Mon Sep 17 00:00:00 2001 From: Sulkta 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 be250f3..5c78ae2 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 b5b9890..9604a51 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 = ["Sulkta "]