Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/video_player/video_player_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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.
Expand All @@ -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<AndroidVideoPlayer>());
});

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<AndroidVideoPlayer>());
});
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add back a test for pausing like this one that was removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

F. Yup, that was a booboo - re-added. Thanks for calling this out! Done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like there was an additional call to await tester.pumpAndSettle(_playDuration) after pause was called in the original test. Maybe that explains the slight difference in player.getPosition(textureId) and pausedDuration causing the test failure?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, let's try it :)

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<Duration> _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<void> started = Completer<void>();
final Completer<void> ended = Completer<void>();
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<DurationRange> _getBufferingRange(
AndroidVideoPlayer player,
int textureId,
) {
return player.videoEventsFor(textureId).firstWhere((VideoEvent event) {
return event.eventType == VideoEventType.bufferingUpdate;
}).then((VideoEvent event) {
return event.buffered!.first;
});
}
2 changes: 1 addition & 1 deletion packages/video_player/video_player_android/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down