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:
edde746
2026-02-27 21:23:31 +01:00
parent 4cebff4367
commit 389a9622c3
2 changed files with 16 additions and 2 deletions
+12 -2
View File
@@ -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;
+4
View File
@@ -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();
}