diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index dd8b750a0c3..6c433bb3c34 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.7.13 + +* When `AndroidVideoPlayer` attempts to operate on a `textureId` that is not + active (i.e. it was previously disposed or never created), the resulting + platform exception is more informative than a "NullPointerException". + ## 2.7.12 * Fixes a [bug](https://github.com/flutter/flutter/issues/156451) where diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index b7a14e45a0a..de2bf8cada6 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -140,34 +140,50 @@ public void initialize() { return new TextureMessage.Builder().setTextureId(handle.id()).build(); } + @NonNull + private VideoPlayer getPlayer(long textureId) { + VideoPlayer player = videoPlayers.get(textureId); + + // Avoid a very ugly un-debuggable NPE that results in returning a null player. + if (player == null) { + String message = "No player found with textureId <" + textureId + ">"; + if (videoPlayers.size() == 0) { + message += " and no active players created by the plugin."; + } + throw new IllegalStateException(message); + } + + return player; + } + public void dispose(@NonNull TextureMessage arg) { - VideoPlayer player = videoPlayers.get(arg.getTextureId()); + VideoPlayer player = getPlayer(arg.getTextureId()); player.dispose(); videoPlayers.remove(arg.getTextureId()); } public void setLooping(@NonNull LoopingMessage arg) { - VideoPlayer player = videoPlayers.get(arg.getTextureId()); + VideoPlayer player = getPlayer(arg.getTextureId()); player.setLooping(arg.getIsLooping()); } public void setVolume(@NonNull VolumeMessage arg) { - VideoPlayer player = videoPlayers.get(arg.getTextureId()); + VideoPlayer player = getPlayer(arg.getTextureId()); player.setVolume(arg.getVolume()); } public void setPlaybackSpeed(@NonNull PlaybackSpeedMessage arg) { - VideoPlayer player = videoPlayers.get(arg.getTextureId()); + VideoPlayer player = getPlayer(arg.getTextureId()); player.setPlaybackSpeed(arg.getSpeed()); } public void play(@NonNull TextureMessage arg) { - VideoPlayer player = videoPlayers.get(arg.getTextureId()); + VideoPlayer player = getPlayer(arg.getTextureId()); player.play(); } public @NonNull PositionMessage position(@NonNull TextureMessage arg) { - VideoPlayer player = videoPlayers.get(arg.getTextureId()); + VideoPlayer player = getPlayer(arg.getTextureId()); PositionMessage result = new PositionMessage.Builder() .setPosition(player.getPosition()) @@ -178,12 +194,12 @@ public void play(@NonNull TextureMessage arg) { } public void seekTo(@NonNull PositionMessage arg) { - VideoPlayer player = videoPlayers.get(arg.getTextureId()); + VideoPlayer player = getPlayer(arg.getTextureId()); player.seekTo(arg.getPosition().intValue()); } public void pause(@NonNull TextureMessage arg) { - VideoPlayer player = videoPlayers.get(arg.getTextureId()); + VideoPlayer player = getPlayer(arg.getTextureId()); player.pause(); } diff --git a/packages/video_player/video_player_android/example/integration_test/video_player_test.dart b/packages/video_player/video_player_android/example/integration_test/video_player_test.dart index b1abb5aed21..fc69114c63f 100644 --- a/packages/video_player/video_player_android/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player_android/example/integration_test/video_player_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; @@ -11,16 +10,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path_provider/path_provider.dart'; import 'package:video_player_android/video_player_android.dart'; -// TODO(stuartmorgan): Remove the use of MiniController in tests, as that is -// testing test code; tests should instead be written directly against the -// platform interface. (These tests were copied from the app-facing package -// during federation and minimally modified, which is why they currently use the -// controller.) -import 'package:video_player_example/mini_controller.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; const Duration _playDuration = Duration(seconds: 1); - const String _videoAssetKey = 'assets/Butterfly-209.mp4'; // Returns the URL to load an asset from this example app as a network source. @@ -38,132 +30,139 @@ String getUrlForAssetAsNetworkSource(String assetKey) { void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + late AndroidVideoPlayer player; + + setUp(() async { + player = AndroidVideoPlayer(); + await player.init(); + }); - late MiniController controller; - tearDown(() async => controller.dispose()); + testWidgets('registers expected implementation', (_) async { + AndroidVideoPlayer.registerWith(); + expect(VideoPlayerPlatform.instance, isA()); + }); + + testWidgets('initializes at the start', (_) async { + final int textureId = (await player.create(DataSource( + sourceType: DataSourceType.asset, + asset: _videoAssetKey, + )))!; - group('asset videos', () { - setUp(() { - controller = MiniController.asset(_videoAssetKey); - }); + expect( + await _getDuration(player, textureId), + const Duration(seconds: 7, milliseconds: 540), + ); - testWidgets('registers expected implementation', - (WidgetTester tester) async { - AndroidVideoPlayer.registerWith(); - expect(VideoPlayerPlatform.instance, isA()); - }); + await player.dispose(textureId); + }); - testWidgets('can be initialized', (WidgetTester tester) async { - await controller.initialize(); + testWidgets('can be played', (WidgetTester tester) async { + final int textureId = (await player.create(DataSource( + sourceType: DataSourceType.asset, + asset: _videoAssetKey, + )))!; - expect(controller.value.isInitialized, true); - expect(await controller.position, Duration.zero); - expect(controller.value.duration, - const Duration(seconds: 7, milliseconds: 540)); - }); + await player.play(textureId); + await tester.pumpAndSettle(_playDuration); - testWidgets('can be played', (WidgetTester tester) async { - await controller.initialize(); + expect(await player.getPosition(textureId), greaterThan(Duration.zero)); + await player.dispose(textureId); + }); - await controller.play(); - await tester.pumpAndSettle(_playDuration); + testWidgets('can seek', (WidgetTester tester) async { + final int textureId = (await player.create(DataSource( + sourceType: DataSourceType.asset, + asset: _videoAssetKey, + )))!; - expect(await controller.position, greaterThan(Duration.zero)); - }); + await player.seekTo(textureId, const Duration(seconds: 3)); + await tester.pumpAndSettle(_playDuration); - testWidgets('can seek', (WidgetTester tester) async { - await controller.initialize(); + expect( + await player.getPosition(textureId), + greaterThanOrEqualTo(const Duration(seconds: 3)), + ); + await player.dispose(textureId); + }); - await controller.seekTo(const Duration(seconds: 3)); + testWidgets('can pause', (WidgetTester tester) async { + final int textureId = (await player.create(DataSource( + sourceType: DataSourceType.asset, + asset: _videoAssetKey, + )))!; - expect(await controller.position, const Duration(seconds: 3)); - }); + await player.play(textureId); + await tester.pumpAndSettle(_playDuration); - testWidgets('can be paused', (WidgetTester tester) async { - await controller.initialize(); + await player.pause(textureId); + final Duration pausedDuration = await player.getPosition(textureId); + await tester.pumpAndSettle(_playDuration); - // Play for a second, then pause, and then wait a second. - await controller.play(); - await tester.pumpAndSettle(_playDuration); - await controller.pause(); - await tester.pumpAndSettle(_playDuration); - final Duration pausedPosition = (await controller.position)!; - await tester.pumpAndSettle(_playDuration); + expect(await player.getPosition(textureId), pausedDuration); + await player.dispose(textureId); + }); - // Verify that we stopped playing after the pause. - expect(await controller.position, pausedPosition); - }); + testWidgets('can play a video from a file', (WidgetTester tester) async { + final Directory directory = await getTemporaryDirectory(); + final File file = File('${directory.path}/video.mp4'); + await file.writeAsBytes( + Uint8List.fromList( + (await rootBundle.load(_videoAssetKey)).buffer.asUint8List(), + ), + ); + + final int textureId = (await player.create(DataSource( + sourceType: DataSourceType.file, + uri: file.path, + )))!; + + await player.play(textureId); + await tester.pumpAndSettle(_playDuration); + + expect(await player.getPosition(textureId), greaterThan(Duration.zero)); + await directory.delete(recursive: true); + await player.dispose(textureId); }); - group('file-based videos', () { - setUp(() async { - // Load the data from the asset. - final String tempDir = (await getTemporaryDirectory()).path; - final ByteData bytes = await rootBundle.load(_videoAssetKey); + testWidgets('can play a video from network', (WidgetTester tester) async { + final int textureId = (await player.create(DataSource( + sourceType: DataSourceType.network, + uri: getUrlForAssetAsNetworkSource(_videoAssetKey), + )))!; - // Write it to a file to use as a source. - final String filename = _videoAssetKey.split('/').last; - final File file = File('$tempDir/$filename'); - await file.writeAsBytes(bytes.buffer.asInt8List()); + await player.play(textureId); + await player.seekTo(textureId, const Duration(seconds: 5)); + await tester.pumpAndSettle(_playDuration); + await player.pause(textureId); - controller = MiniController.file(file); - }); + expect(await player.getPosition(textureId), greaterThan(Duration.zero)); - testWidgets('test video player using static file() method as constructor', - (WidgetTester tester) async { - await controller.initialize(); + final DurationRange range = await _getBufferingRange(player, textureId); + expect(range.start, Duration.zero); + expect(range.end, greaterThan(Duration.zero)); - await controller.play(); - await tester.pumpAndSettle(_playDuration); + await player.dispose(textureId); + }); +} - expect(await controller.position, greaterThan(Duration.zero)); - }); +Future _getDuration( + AndroidVideoPlayer player, + int textureId, +) { + return player.videoEventsFor(textureId).firstWhere((VideoEvent event) { + return event.eventType == VideoEventType.initialized; + }).then((VideoEvent event) { + return event.duration!; }); +} - group('network videos', () { - setUp(() { - final String videoUrl = getUrlForAssetAsNetworkSource(_videoAssetKey); - controller = MiniController.network(videoUrl); - }); - - testWidgets('reports buffering status', (WidgetTester tester) async { - await controller.initialize(); - - final Completer started = Completer(); - final Completer ended = Completer(); - controller.addListener(() { - if (!started.isCompleted && controller.value.isBuffering) { - started.complete(); - } - if (started.isCompleted && - !controller.value.isBuffering && - !ended.isCompleted) { - ended.complete(); - } - }); - - await controller.play(); - await controller.seekTo(const Duration(seconds: 5)); - await tester.pumpAndSettle(_playDuration); - await controller.pause(); - - expect(await controller.position, greaterThan(Duration.zero)); - - await expectLater(started.future, completes); - await expectLater(ended.future, completes); - }); - - testWidgets('live stream duration != 0', (WidgetTester tester) async { - final MiniController livestreamController = MiniController.network( - 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8', - ); - await livestreamController.initialize(); - - expect(livestreamController.value.isInitialized, true); - // Live streams should have either a positive duration or C.TIME_UNSET if the duration is unknown - // See https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#getDuration-- - expect(livestreamController.value.duration, - (Duration duration) => duration != Duration.zero); - }); +Future _getBufferingRange( + AndroidVideoPlayer player, + int textureId, +) { + return player.videoEventsFor(textureId).firstWhere((VideoEvent event) { + return event.eventType == VideoEventType.bufferingUpdate; + }).then((VideoEvent event) { + return event.buffered!.first; }); } diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index ee5601369a2..f22ffcb6e0e 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.7.12 +version: 2.7.13 environment: sdk: ^3.5.0