diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index 26f60a5c2b71..af95e915d0e4 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.12.1 + +* Fixes a [bug](https://github.com/flutter/flutter/issues/190739) where building the view for a disposed player threw a `StateError` instead of building an empty view. + ## 2.12.0 * Fixes a [bug](https://github.com/flutter/flutter/issues/176575) where some videos report an incorrect duration when initialized without a video duration. diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index 2024f18c5e97..bd0e6631ff86 100644 --- a/packages/video_player/video_player_android/lib/src/android_video_player.dart +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -198,9 +198,16 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { @override Widget buildViewWithOptions(VideoViewOptions options) { final int playerId = options.playerId; - final VideoPlayerViewState viewState = _playerWith(id: playerId).viewState; + // Disposal and the next frame are not synchronized: a widget holding this player can be rebuilt + // after the player is gone (an AnimatedSwitcher transition, or a rebuild triggered by a + // configuration change once the platform has reclaimed the player). Build an empty view rather + // than throwing, since a build method that throws leaves the widget permanently broken. + final _PlayerInstance? player = _players[playerId]; + if (player == null) { + return const SizedBox.shrink(); + } - return switch (viewState) { + return switch (player.viewState) { VideoPlayerTextureViewState(:final int textureId) => Texture(textureId: textureId), VideoPlayerPlatformViewState() => PlatformViewPlayer(playerId: playerId), }; diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index 896c612d45e5..165e3c060895 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_android description: Android implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.12.0 +version: 2.12.1 environment: sdk: ^3.12.0 diff --git a/packages/video_player/video_player_android/test/android_video_player_test.dart b/packages/video_player/video_player_android/test/android_video_player_test.dart index 3a09bb033490..e56f42cd7d19 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.dart @@ -107,6 +107,16 @@ void main() { verify(api.dispose(1)); }); + test('buildViewWithOptions returns an empty widget after the player is disposed', () async { + final (AndroidVideoPlayer player, _, _) = setUpMockPlayer(playerId: 1, textureId: 100); + await player.dispose(1); + + // A disposed player can still be built: the widget stays mounted through the frame that + // follows disposal (an AnimatedSwitcher transition, or a rebuild from a configuration + // change), so building must degrade to an empty view rather than throw. + expect(player.buildViewWithOptions(const VideoViewOptions(playerId: 1)), isA()); + }); + test('create with asset', () async { final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer( playerId: 1, diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index f97f9f1c5e41..50ce4d477ff7 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.11.1 + +* Fixes a [bug](https://github.com/flutter/flutter/issues/190739) where building the view for a disposed player threw a `StateError` instead of building an empty view. + ## 2.11.0 * Implements `setPreventsDisplaySleepDuringVideoPlayback` using diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index c98edfc15ec8..08f284206a22 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -301,9 +301,16 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { @override Widget buildViewWithOptions(VideoViewOptions options) { final int playerId = options.playerId; - final VideoPlayerViewState viewState = _playerWith(id: playerId).viewState; + // Disposal and the next frame are not synchronized: a widget holding this player can be rebuilt + // after the player is gone (an AnimatedSwitcher transition, or a rebuild triggered by a + // configuration change once the platform has reclaimed the player). Build an empty view rather + // than throwing, since a build method that throws leaves the widget permanently broken. + final _PlayerInstance? player = _players[playerId]; + if (player == null) { + return const SizedBox.shrink(); + } - return switch (viewState) { + return switch (player.viewState) { VideoPlayerTextureViewState(:final int textureId) => Texture(textureId: textureId), VideoPlayerPlatformViewState() => _buildPlatformView(playerId), }; diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 9a1e29eab6d1..5be3b9ddebe5 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_avfoundation description: iOS and macOS implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.11.0 +version: 2.11.1 environment: sdk: ^3.10.0 diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart index 16dfd7e9d881..fc9ec035c956 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart @@ -60,6 +60,16 @@ void main() { verify(playerApi.dispose()); }); + test('buildViewWithOptions returns an empty widget after the player is disposed', () async { + final (AVFoundationVideoPlayer player, _, _) = setUpMockPlayer(playerId: 1, textureId: 101); + await player.dispose(1); + + // A disposed player can still be built: the widget stays mounted through the frame that + // follows disposal (an AnimatedSwitcher transition, or a rebuild from a configuration + // change), so building must degrade to an empty view rather than throw. + expect(player.buildViewWithOptions(const VideoViewOptions(playerId: 1)), isA()); + }); + test('create with asset', () async { final (AVFoundationVideoPlayer player, MockAVFoundationVideoPlayerApi api, _) = setUpMockPlayer(playerId: 1, textureId: 101);