Skip to content

Commit

Permalink
[video_player] isCompleted event. (#4923)
Browse files Browse the repository at this point in the history
Adds `isCompleted` event to `VideoPlayerEvent`.

fixes flutter/flutter#21929
  • Loading branch information
tarrinneal committed Sep 18, 2023
1 parent e2dac87 commit 64dac2b
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 5 deletions.
4 changes: 4 additions & 0 deletions packages/video_player/video_player/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.7.2

* Adds `isCompleted` event to `VideoPlayerEvent`.

## 2.7.1

* Adds pub topics to package metadata.
Expand Down
26 changes: 23 additions & 3 deletions packages/video_player/video_player/lib/video_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class VideoPlayerValue {
this.playbackSpeed = 1.0,
this.rotationCorrection = 0,
this.errorDescription,
this.isCompleted = false,
});

/// Returns an instance for a video that hasn't been loaded.
Expand Down Expand Up @@ -111,6 +112,12 @@ class VideoPlayerValue {
/// If [hasError] is false this is `null`.
final String? errorDescription;

/// True if video has finished playing to end.
///
/// Reverts to false if video position changes, or video begins playing.
/// Does not update if video is looping.
final bool isCompleted;

/// The [size] of the currently loaded video.
final Size size;

Expand Down Expand Up @@ -158,6 +165,7 @@ class VideoPlayerValue {
double? playbackSpeed,
int? rotationCorrection,
String? errorDescription = _defaultErrorDescription,
bool? isCompleted,
}) {
return VideoPlayerValue(
duration: duration ?? this.duration,
Expand All @@ -176,6 +184,7 @@ class VideoPlayerValue {
errorDescription: errorDescription != _defaultErrorDescription
? errorDescription
: this.errorDescription,
isCompleted: isCompleted ?? this.isCompleted,
);
}

Expand All @@ -194,7 +203,8 @@ class VideoPlayerValue {
'isBuffering: $isBuffering, '
'volume: $volume, '
'playbackSpeed: $playbackSpeed, '
'errorDescription: $errorDescription)';
'errorDescription: $errorDescription, '
'isCompleted: $isCompleted),';
}

@override
Expand All @@ -215,7 +225,8 @@ class VideoPlayerValue {
errorDescription == other.errorDescription &&
size == other.size &&
rotationCorrection == other.rotationCorrection &&
isInitialized == other.isInitialized;
isInitialized == other.isInitialized &&
isCompleted == other.isCompleted;

@override
int get hashCode => Object.hash(
Expand All @@ -233,6 +244,7 @@ class VideoPlayerValue {
size,
rotationCorrection,
isInitialized,
isCompleted,
);
}

Expand Down Expand Up @@ -441,6 +453,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
rotationCorrection: event.rotationCorrection,
isInitialized: event.duration != null,
errorDescription: null,
isCompleted: false,
);
initializingCompleter.complete(null);
_applyLooping();
Expand All @@ -453,6 +466,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
// we use pause() and seekTo() to ensure the platform stops playing
// and seeks to the last frame of the video.
pause().then((void pauseResult) => seekTo(value.duration));
value = value.copyWith(isCompleted: true);
break;
case VideoEventType.bufferingUpdate:
value = value.copyWith(buffered: event.buffered);
Expand All @@ -464,7 +478,12 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
value = value.copyWith(isBuffering: false);
break;
case VideoEventType.isPlayingStateUpdate:
value = value.copyWith(isPlaying: event.isPlaying);
if (event.isPlaying ?? false) {
value =
value.copyWith(isPlaying: event.isPlaying, isCompleted: false);
} else {
value = value.copyWith(isPlaying: event.isPlaying);
}
break;
case VideoEventType.unknown:
break;
Expand Down Expand Up @@ -737,6 +756,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
value = value.copyWith(
position: position,
caption: _getCaptionAt(position),
isCompleted: position == value.duration,
);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/video_player/video_player/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter
widgets on Android, iOS, and web.
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
version: 2.7.1
version: 2.7.2

environment:
sdk: ">=2.19.0 <4.0.0"
Expand Down
98 changes: 97 additions & 1 deletion packages/video_player/video_player/test/video_player_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1069,7 +1069,8 @@ void main() {
'isBuffering: true, '
'volume: 0.5, '
'playbackSpeed: 1.5, '
'errorDescription: null)');
'errorDescription: null, '
'isCompleted: false),');
});

group('copyWith()', () {
Expand Down Expand Up @@ -1204,6 +1205,101 @@ void main() {
expect(colors.bufferedColor, bufferedColor);
expect(colors.backgroundColor, backgroundColor);
});

test('isCompleted updates on video end', () async {
final VideoPlayerController controller = VideoPlayerController.networkUrl(
_localhostUri,
videoPlayerOptions: VideoPlayerOptions(),
);

await controller.initialize();

final StreamController<VideoEvent> fakeVideoEventStream =
fakeVideoPlayerPlatform.streams[controller.textureId]!;

bool currentIsCompleted = controller.value.isCompleted;

final void Function() isCompletedTest = expectAsync0(() {});

controller.addListener(() async {
if (currentIsCompleted != controller.value.isCompleted) {
currentIsCompleted = controller.value.isCompleted;
if (controller.value.isCompleted) {
isCompletedTest();
}
}
});

fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.completed));
});

test('isCompleted updates on video play after completed', () async {
final VideoPlayerController controller = VideoPlayerController.networkUrl(
_localhostUri,
videoPlayerOptions: VideoPlayerOptions(),
);

await controller.initialize();

final StreamController<VideoEvent> fakeVideoEventStream =
fakeVideoPlayerPlatform.streams[controller.textureId]!;

bool currentIsCompleted = controller.value.isCompleted;

final void Function() isCompletedTest = expectAsync0(() {}, count: 2);
final void Function() isNoLongerCompletedTest = expectAsync0(() {});
bool hasLooped = false;

controller.addListener(() async {
if (currentIsCompleted != controller.value.isCompleted) {
currentIsCompleted = controller.value.isCompleted;
if (controller.value.isCompleted) {
isCompletedTest();
if (!hasLooped) {
fakeVideoEventStream.add(VideoEvent(
eventType: VideoEventType.isPlayingStateUpdate,
isPlaying: true));
hasLooped = !hasLooped;
}
} else {
isNoLongerCompletedTest();
}
}
});

fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.completed));
});

test('isCompleted updates on video seek to end', () async {
final VideoPlayerController controller = VideoPlayerController.networkUrl(
_localhostUri,
videoPlayerOptions: VideoPlayerOptions(),
);

await controller.initialize();

bool currentIsCompleted = controller.value.isCompleted;

final void Function() isCompletedTest = expectAsync0(() {});

controller.value =
controller.value.copyWith(duration: const Duration(seconds: 10));

controller.addListener(() async {
if (currentIsCompleted != controller.value.isCompleted) {
currentIsCompleted = controller.value.isCompleted;
if (controller.value.isCompleted) {
isCompletedTest();
}
}
});

// This call won't update isCompleted.
// The test will fail if `isCompletedTest` is called more than once.
await controller.seekTo(const Duration(seconds: 10));

await controller.seekTo(const Duration(seconds: 20));
});
}

class FakeVideoPlayerPlatform extends VideoPlayerPlatform {
Expand Down

0 comments on commit 64dac2b

Please sign in to comment.