fix artwork race, chat-only disposal, and stall recovery in pip

- Fix Now Playing artwork: controller's mediaInfo field is now mutable
  so setMediaInfo updates it before loadUrl sends to native. Bumps fork
  which also merges artworkUrl on the native side as defense-in-depth.
- Pause native player in chat-only mode via showVideo reaction. Cancels
  latency/stall timers and aborts in-flight loads when video is hidden.
- Remove PiP-specific stall recovery path — light refresh preserves the
  PiP window, so PiP can use the same escalation ladder as normal play.
- Fix stale _initInFlight clobber on generation mismatch early-return.
- Reset _streamQualityIndex on refresh to avoid stale quality label.
- Thread-safe hasActivePlayers on SharedPlayerManager (fork).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tommy Chow 2026-04-11 23:36:56 -07:00
parent d8dad59ade
commit e4de57e0b2
2 changed files with 29 additions and 15 deletions

View file

@ -69,6 +69,7 @@ abstract class NativeVideoStoreBase with Store implements VideoPlayerInterface {
final _pip = SimplePip();
ReactionDisposer? _disposeAndroidAutoPipReaction;
ReactionDisposer? _disposeVideoModeReaction;
StreamSubscription<bool>? _pipSub;
StreamSubscription<List<NativeVideoPlayerQuality>>? _qualitiesSub;
@ -133,6 +134,23 @@ abstract class NativeVideoStoreBase with Store implements VideoPlayerInterface {
updateStreamInfo();
_initPlayer();
// Pause and release resources in chat-only mode so the AVPlayer
// isn't decoding/buffering while the video UI is hidden.
_disposeVideoModeReaction = reaction(
(_) => settingsStore.showVideo,
(showVideo) {
if (!showVideo) {
_controller?.pause();
_userPaused = true;
_latencyTimer?.cancel();
_stallRecoveryTimer?.cancel();
_initRetryTimer?.cancel();
} else {
handleRefresh();
}
},
);
if (Platform.isAndroid) {
_disposeAndroidAutoPipReaction = autorun((_) async {
if (settingsStore.showVideo && await SimplePip.isAutoPipAvailable) {
@ -181,7 +199,8 @@ abstract class NativeVideoStoreBase with Store implements VideoPlayerInterface {
await _controller!.initialize();
if (_disposed || generation != _initGeneration) {
_initInFlight = false;
// Don't reset _initInFlight here — a newer generation's _initPlayer
// may already own the flag. Only the current generation should touch it.
return;
}
@ -305,11 +324,15 @@ abstract class NativeVideoStoreBase with Store implements VideoPlayerInterface {
}
}
if (_disposed || generation != _initGeneration) return;
if (_disposed || generation != _initGeneration || !settingsStore.showVideo) {
return;
}
_hlsUrl = twitchGqlApi.buildHlsUrl(login: userLogin, token: token);
await _controller!.loadUrl(url: _hlsUrl!);
if (_disposed || generation != _initGeneration) return;
if (_disposed || generation != _initGeneration || !settingsStore.showVideo) {
return;
}
await _controller!.configureForLivePlayback();
_startLatencyPolling();
}
@ -452,17 +475,6 @@ abstract class NativeVideoStoreBase with Store implements VideoPlayerInterface {
_stallRecoveryAttempt++;
// During PiP, only attempt light recovery (seek to live edge).
// Heavy recovery (handleRefresh) disposes the controller, killing PiP.
if (_isInPipMode) {
if (_stallRecoveryAttempt > _maxRefreshAttempts) return;
debugPrint('NativeVideoStore: stall in PiP, seeking to live edge');
_controller?.seekToLiveEdge();
_controller?.play();
_startStallRecoveryTimer();
return;
}
if (_stallRecoveryAttempt <= 1) {
// Light recovery: seek to live edge and resume.
debugPrint('NativeVideoStore: stall detected, seeking to live edge');
@ -642,6 +654,7 @@ abstract class NativeVideoStoreBase with Store implements VideoPlayerInterface {
_pendingQualityIndex = null;
_isQualitySwitching = false;
_isAudioOnlyMode = false;
_streamQualityIndex = 0;
_latency = null;
_availableStreamQualities = [];
_qualityObjects = [];
@ -866,6 +879,7 @@ abstract class NativeVideoStoreBase with Store implements VideoPlayerInterface {
_pipSub?.cancel();
_qualitiesSub?.cancel();
_disposeAndroidAutoPipReaction?.call();
_disposeVideoModeReaction?.call();
_controller?.removeActivityListener(_handleActivityEvent);
_controller?.dispose();

View file

@ -102,7 +102,7 @@ packages:
description:
path: "."
ref: frosty-patches
resolved-ref: e5986defd01033cda19421d3a70d59f492ff4869
resolved-ref: "7ec3ba30f3506fbd727a8348c122bb8380fc5f88"
url: "https://github.com/tommyxchow/flutter-native-video-player.git"
source: git
version: "0.4.11"