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 <noreply@anthropic.com>
This commit is contained in:
Tommy Chow 2026-03-07 23:22:44 -08:00
parent 929d525c13
commit 9cec89a67c

View file

@ -38,6 +38,12 @@ abstract class VideoStoreBase with Store {
var _firstTimeSettingQuality = true; 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. /// The video web view params used for enabling auto play.
late final PlatformWebViewControllerCreationParams _videoWebViewParams; late final PlatformWebViewControllerCreationParams _videoWebViewParams;
@ -126,18 +132,8 @@ abstract class VideoStoreBase with Store {
NavigationDelegate( NavigationDelegate(
onPageFinished: (url) async { onPageFinished: (url) async {
if (url != videoUrl) return; if (url != videoUrl) return;
// Safe evaluation of JavaScript boolean result if (!_needsInit) return;
final result = await videoWebViewController _needsInit = false;
.runJavaScriptReturningResult(
'window._injected ? true : false',
);
final injected = result is bool
? result
: (result.toString().toLowerCase() == 'true');
if (injected) return;
await videoWebViewController.runJavaScript(
'window._injected = true;',
);
await initVideo(); await initVideo();
_acceptContentWarning(); _acceptContentWarning();
}, },
@ -810,6 +806,9 @@ abstract class VideoStoreBase with Store {
videoElement.textTracks[0].mode = "hidden"; videoElement.textTracks[0].mode = "hidden";
} }
} }
// Explicitly attempt autoplay in case the player loaded paused
try { await videoElement.play(); } catch (e) {}
}); });
'''); ''');
if (settingsStore.showOverlay) { if (settingsStore.showOverlay) {
@ -991,27 +990,24 @@ abstract class VideoStoreBase with Store {
} }
/// Refreshes the stream webview and updates the stream info. /// 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 @action
Future<void> handleRefresh() async { Future<void> handleRefresh() async {
HapticFeedback.lightImpact(); HapticFeedback.lightImpact();
_paused = true; _paused = true;
_firstTimeSettingQuality = true; _firstTimeSettingQuality = true;
_isInPipMode = false; _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 { try {
videoWebViewController.runJavaScript('window._latencyTracker?.stop()'); videoWebViewController.runJavaScript('window._latencyTracker?.stop()');
} catch (e) { } catch (e) {
// Ignore - may not exist yet // 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)); await videoWebViewController.loadRequest(Uri.parse(videoUrl));
updateStreamInfo(); updateStreamInfo();