fix: prevent OOM crash from infinite focus reclaim loop
Add re-entrancy guard to VideoPlayerScreen._onScreenFocusChanged() and replace busy-wait spin loop with Completer in MediaDetailScreen.
This commit is contained in:
@@ -65,6 +65,7 @@ class MediaDetailScreen extends StatefulWidget {
|
||||
class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAware, DeletionAware {
|
||||
List<PlexMetadata> _seasons = [];
|
||||
bool _isLoadingSeasons = false;
|
||||
Completer<void>? _seasonsCompleter;
|
||||
PlexMetadata? _fullMetadata;
|
||||
PlexMetadata? _onDeckEpisode;
|
||||
PlexVideoPlaybackData? _playbackData;
|
||||
@@ -1025,6 +1026,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
|
||||
}
|
||||
|
||||
Future<void> _loadSeasons() async {
|
||||
_seasonsCompleter = Completer<void>();
|
||||
setState(() {
|
||||
_isLoadingSeasons = true;
|
||||
});
|
||||
@@ -1048,11 +1050,16 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
|
||||
setState(() {
|
||||
_isLoadingSeasons = false;
|
||||
});
|
||||
} finally {
|
||||
if (!(_seasonsCompleter?.isCompleted ?? true)) {
|
||||
_seasonsCompleter?.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load seasons from downloaded episodes (offline mode)
|
||||
void _loadSeasonsFromDownloads() {
|
||||
_seasonsCompleter = Completer<void>();
|
||||
setState(() {
|
||||
_isLoadingSeasons = true;
|
||||
});
|
||||
@@ -1087,6 +1094,9 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
|
||||
_seasons = seasons;
|
||||
_isLoadingSeasons = false;
|
||||
});
|
||||
if (!(_seasonsCompleter?.isCompleted ?? true)) {
|
||||
_seasonsCompleter?.complete();
|
||||
}
|
||||
}
|
||||
|
||||
/// Load extras (trailers, behind-the-scenes, etc.)
|
||||
@@ -1654,8 +1664,8 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
|
||||
}
|
||||
|
||||
// Wait for seasons to finish loading if they're currently loading
|
||||
while (_isLoadingSeasons) {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
if (_isLoadingSeasons && _seasonsCompleter != null) {
|
||||
await _seasonsCompleter!.future.timeout(const Duration(seconds: 10), onTimeout: () {});
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -173,6 +173,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
// Screen-level focus node: persists across loading/initialized phases so
|
||||
// key events never escape the video player route.
|
||||
late final FocusNode _screenFocusNode;
|
||||
bool _reclaimingFocus = false;
|
||||
|
||||
// Cached setting: when false on Windows/Linux, ESC should not exit the player
|
||||
bool _videoPlayerNavigationEnabled = false;
|
||||
@@ -1723,8 +1724,11 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
/// descendant has focus, so internal movement between child controls
|
||||
/// does NOT trigger this.
|
||||
void _onScreenFocusChanged() {
|
||||
if (_reclaimingFocus) return;
|
||||
if (!_screenFocusNode.hasFocus && mounted && !_isExiting.value) {
|
||||
_reclaimingFocus = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_reclaimingFocus = false;
|
||||
if (mounted && !_isExiting.value && !_screenFocusNode.hasFocus) {
|
||||
_screenFocusNode.requestFocus();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user