Steps to reproduce
- Open a video player in fullscreen (landscape)
- Background the app (lock screen / home button)
- Wait for Android to trigger LOW_MEMORY (~40%) or iOS to reclaim AVPlayer resources
- Return to app → CONFIGURATION_CHANGED fires (orientation change)
_VideoPlayerState.build() → buildViewWithOptions(playerId) → _playerWith(id) throws StateError
Expected results
buildViewWithOptions should handle missing players gracefully (return empty widget) instead of throwing.
Actual results
StateError: Bad state: No active player with ID 4.
iOS stack trace:
avfoundation_video_player.dart:327 in AVFoundationVideoPlayer._playerWith
avfoundation_video_player.dart:304 in AVFoundationVideoPlayer.buildViewWithOptions
video_player.dart:1143 in _VideoPlayerState.build
Android stack trace:
android_video_player.dart:298 in AndroidVideoPlayer._playerWith
Impact
- 3,400+ events (2.2K iOS + 1.2K Android) from ~500 users in 30 days
- Devices under memory pressure: OPPO Reno3 (MediaTek), Samsung A13, iPad Air 5th gen
- Level:
Error — app doesn't crash but video widget becomes unrecoverable
Root Cause
_playerWith throws when player ID is not in _players map:
// android_video_player.dart:296-298 / avfoundation_video_player.dart:325-327
_PlayerInstance _playerWith({required int id}) {
final _PlayerInstance? player = _players[id];
return player ?? (throw StateError('No active player with ID $id.'));
}
The race: dispose() removes from _players, but the VideoPlayer widget is still mounted (AnimatedSwitcher transition / rebuild during CONFIGURATION_CHANGED). build() calls buildViewWithOptions → throws.
Proposed Fix
Guard buildViewWithOptions to return empty widget when player is gone:
android_video_player.dart:
@override
Widget buildViewWithOptions(VideoViewOptions options) {
final int playerId = options.playerId;
final _PlayerInstance? player = _players[playerId];
if (player == null) return const SizedBox.shrink();
return _buildExoPlayerView(playerId);
}
avfoundation_video_player.dart:
@override
Widget buildViewWithOptions(VideoViewOptions options) {
final int playerId = options.playerId;
final _PlayerInstance? player = _players[playerId];
if (player == null) return const SizedBox.shrink();
final VideoPlayerViewState viewState = player.viewState;
return switch (viewState) {
VideoPlayerTextureViewState(:final int textureId) => Texture(textureId: textureId),
VideoPlayerPlatformViewState() => _buildPlatformView(playerId),
};
}
Environment
Flutter 3.44.0-3.44.6 (stable), Dart 3.12.0-3.12.2
video_player: 2.13.0, video_player_android: 2.11.0, video_player_avfoundation: 2.11.0
Tested on: OPPO Reno3 (Android 12, MediaTek MT6779), iPad Air 5th gen (iOS 26.6), iPhone 13 (iOS 26.5.2)
Steps to reproduce
_VideoPlayerState.build()→buildViewWithOptions(playerId)→_playerWith(id)throwsStateErrorExpected results
buildViewWithOptionsshould handle missing players gracefully (return empty widget) instead of throwing.Actual results
iOS stack trace:
Android stack trace:
Impact
Error— app doesn't crash but video widget becomes unrecoverableRoot Cause
_playerWiththrows when player ID is not in_playersmap:The race:
dispose()removes from_players, but theVideoPlayerwidget is still mounted (AnimatedSwitcher transition / rebuild during CONFIGURATION_CHANGED).build()callsbuildViewWithOptions→ throws.Proposed Fix
Guard
buildViewWithOptionsto return empty widget when player is gone:android_video_player.dart:
avfoundation_video_player.dart:
Environment
Tested on: OPPO Reno3 (Android 12, MediaTek MT6779), iPad Air 5th gen (iOS 26.6), iPhone 13 (iOS 26.5.2)