From 9cec89a67c3c24489fc3c9c5b74513bfdbcebf61 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Sat, 7 Mar 2026 23:22:44 -0800 Subject: [PATCH] fix video refresh race condition causing black screen with play button Replace JS-side `window._injected` guard with a Dart-controlled `_needsInit` flag so `initVideo()` reliably runs after every refresh. The previous approach loaded `about:blank` then `videoUrl` back-to-back, but `loadRequest` resolves on navigation start (not page load), so the JS context often survived and `onPageFinished` skipped re-initialization. Also adds an explicit `play()` call to handle cases where the Twitch player loads in a paused state. Co-Authored-By: Claude Opus 4.6 --- lib/screens/channel/video/video_store.dart | 36 ++++++++++------------ 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/lib/screens/channel/video/video_store.dart b/lib/screens/channel/video/video_store.dart index df9559ac..f84cda42 100644 --- a/lib/screens/channel/video/video_store.dart +++ b/lib/screens/channel/video/video_store.dart @@ -38,6 +38,12 @@ abstract class VideoStoreBase with Store { var _firstTimeSettingQuality = true; + /// Whether [initVideo] should run on the next [onPageFinished]. + /// + /// Dart-controlled instead of JS-side (`window._injected`) to avoid race + /// conditions when `loadRequest` fires before the previous page fully loads. + var _needsInit = true; + /// The video web view params used for enabling auto play. late final PlatformWebViewControllerCreationParams _videoWebViewParams; @@ -126,18 +132,8 @@ abstract class VideoStoreBase with Store { NavigationDelegate( onPageFinished: (url) async { if (url != videoUrl) return; - // Safe evaluation of JavaScript boolean result - final result = await videoWebViewController - .runJavaScriptReturningResult( - 'window._injected ? true : false', - ); - final injected = result is bool - ? result - : (result.toString().toLowerCase() == 'true'); - if (injected) return; - await videoWebViewController.runJavaScript( - 'window._injected = true;', - ); + if (!_needsInit) return; + _needsInit = false; await initVideo(); _acceptContentWarning(); }, @@ -810,6 +806,9 @@ abstract class VideoStoreBase with Store { videoElement.textTracks[0].mode = "hidden"; } } + + // Explicitly attempt autoplay in case the player loaded paused + try { await videoElement.play(); } catch (e) {} }); '''); if (settingsStore.showOverlay) { @@ -991,27 +990,24 @@ abstract class VideoStoreBase with Store { } /// Refreshes the stream webview and updates the stream info. - /// - /// Performs a hard refresh by loading about:blank first to completely clear - /// the webview state, then immediately reloads the video URL. This is more - /// reliable than a soft reload when the webview is sluggish. @action Future handleRefresh() async { HapticFeedback.lightImpact(); _paused = true; _firstTimeSettingQuality = true; _isInPipMode = false; + _latency = null; + _availableStreamQualities = []; + + // Signal that initVideo() should run on the next onPageFinished + _needsInit = true; - // Stop latency tracker immediately to free up JS main thread for refresh try { videoWebViewController.runJavaScript('window._latencyTracker?.stop()'); } catch (e) { // Ignore - may not exist yet } - // Hard refresh: clear everything by loading blank page first - await videoWebViewController.loadRequest(Uri.parse('about:blank')); - // Then immediately load the video URL for a fresh start await videoWebViewController.loadRequest(Uri.parse(videoUrl)); updateStreamInfo();