Skip to content

Commit 64dac2b

Browse files
authored
[video_player] isCompleted event. (#4923)
Adds `isCompleted` event to `VideoPlayerEvent`. fixes flutter/flutter#21929
1 parent e2dac87 commit 64dac2b

File tree

4 files changed

+125
-5
lines changed

4 files changed

+125
-5
lines changed

packages/video_player/video_player/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.7.2
2+
3+
* Adds `isCompleted` event to `VideoPlayerEvent`.
4+
15
## 2.7.1
26

37
* Adds pub topics to package metadata.

packages/video_player/video_player/lib/video_player.dart

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class VideoPlayerValue {
5252
this.playbackSpeed = 1.0,
5353
this.rotationCorrection = 0,
5454
this.errorDescription,
55+
this.isCompleted = false,
5556
});
5657

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

115+
/// True if video has finished playing to end.
116+
///
117+
/// Reverts to false if video position changes, or video begins playing.
118+
/// Does not update if video is looping.
119+
final bool isCompleted;
120+
114121
/// The [size] of the currently loaded video.
115122
final Size size;
116123

@@ -158,6 +165,7 @@ class VideoPlayerValue {
158165
double? playbackSpeed,
159166
int? rotationCorrection,
160167
String? errorDescription = _defaultErrorDescription,
168+
bool? isCompleted,
161169
}) {
162170
return VideoPlayerValue(
163171
duration: duration ?? this.duration,
@@ -176,6 +184,7 @@ class VideoPlayerValue {
176184
errorDescription: errorDescription != _defaultErrorDescription
177185
? errorDescription
178186
: this.errorDescription,
187+
isCompleted: isCompleted ?? this.isCompleted,
179188
);
180189
}
181190

@@ -194,7 +203,8 @@ class VideoPlayerValue {
194203
'isBuffering: $isBuffering, '
195204
'volume: $volume, '
196205
'playbackSpeed: $playbackSpeed, '
197-
'errorDescription: $errorDescription)';
206+
'errorDescription: $errorDescription, '
207+
'isCompleted: $isCompleted),';
198208
}
199209

200210
@override
@@ -215,7 +225,8 @@ class VideoPlayerValue {
215225
errorDescription == other.errorDescription &&
216226
size == other.size &&
217227
rotationCorrection == other.rotationCorrection &&
218-
isInitialized == other.isInitialized;
228+
isInitialized == other.isInitialized &&
229+
isCompleted == other.isCompleted;
219230

220231
@override
221232
int get hashCode => Object.hash(
@@ -233,6 +244,7 @@ class VideoPlayerValue {
233244
size,
234245
rotationCorrection,
235246
isInitialized,
247+
isCompleted,
236248
);
237249
}
238250

@@ -441,6 +453,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
441453
rotationCorrection: event.rotationCorrection,
442454
isInitialized: event.duration != null,
443455
errorDescription: null,
456+
isCompleted: false,
444457
);
445458
initializingCompleter.complete(null);
446459
_applyLooping();
@@ -453,6 +466,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
453466
// we use pause() and seekTo() to ensure the platform stops playing
454467
// and seeks to the last frame of the video.
455468
pause().then((void pauseResult) => seekTo(value.duration));
469+
value = value.copyWith(isCompleted: true);
456470
break;
457471
case VideoEventType.bufferingUpdate:
458472
value = value.copyWith(buffered: event.buffered);
@@ -464,7 +478,12 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
464478
value = value.copyWith(isBuffering: false);
465479
break;
466480
case VideoEventType.isPlayingStateUpdate:
467-
value = value.copyWith(isPlaying: event.isPlaying);
481+
if (event.isPlaying ?? false) {
482+
value =
483+
value.copyWith(isPlaying: event.isPlaying, isCompleted: false);
484+
} else {
485+
value = value.copyWith(isPlaying: event.isPlaying);
486+
}
468487
break;
469488
case VideoEventType.unknown:
470489
break;
@@ -737,6 +756,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
737756
value = value.copyWith(
738757
position: position,
739758
caption: _getCaptionAt(position),
759+
isCompleted: position == value.duration,
740760
);
741761
}
742762

packages/video_player/video_player/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter
33
widgets on Android, iOS, and web.
44
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player
55
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
6-
version: 2.7.1
6+
version: 2.7.2
77

88
environment:
99
sdk: ">=2.19.0 <4.0.0"

packages/video_player/video_player/test/video_player_test.dart

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1069,7 +1069,8 @@ void main() {
10691069
'isBuffering: true, '
10701070
'volume: 0.5, '
10711071
'playbackSpeed: 1.5, '
1072-
'errorDescription: null)');
1072+
'errorDescription: null, '
1073+
'isCompleted: false),');
10731074
});
10741075

10751076
group('copyWith()', () {
@@ -1204,6 +1205,101 @@ void main() {
12041205
expect(colors.bufferedColor, bufferedColor);
12051206
expect(colors.backgroundColor, backgroundColor);
12061207
});
1208+
1209+
test('isCompleted updates on video end', () async {
1210+
final VideoPlayerController controller = VideoPlayerController.networkUrl(
1211+
_localhostUri,
1212+
videoPlayerOptions: VideoPlayerOptions(),
1213+
);
1214+
1215+
await controller.initialize();
1216+
1217+
final StreamController<VideoEvent> fakeVideoEventStream =
1218+
fakeVideoPlayerPlatform.streams[controller.textureId]!;
1219+
1220+
bool currentIsCompleted = controller.value.isCompleted;
1221+
1222+
final void Function() isCompletedTest = expectAsync0(() {});
1223+
1224+
controller.addListener(() async {
1225+
if (currentIsCompleted != controller.value.isCompleted) {
1226+
currentIsCompleted = controller.value.isCompleted;
1227+
if (controller.value.isCompleted) {
1228+
isCompletedTest();
1229+
}
1230+
}
1231+
});
1232+
1233+
fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.completed));
1234+
});
1235+
1236+
test('isCompleted updates on video play after completed', () async {
1237+
final VideoPlayerController controller = VideoPlayerController.networkUrl(
1238+
_localhostUri,
1239+
videoPlayerOptions: VideoPlayerOptions(),
1240+
);
1241+
1242+
await controller.initialize();
1243+
1244+
final StreamController<VideoEvent> fakeVideoEventStream =
1245+
fakeVideoPlayerPlatform.streams[controller.textureId]!;
1246+
1247+
bool currentIsCompleted = controller.value.isCompleted;
1248+
1249+
final void Function() isCompletedTest = expectAsync0(() {}, count: 2);
1250+
final void Function() isNoLongerCompletedTest = expectAsync0(() {});
1251+
bool hasLooped = false;
1252+
1253+
controller.addListener(() async {
1254+
if (currentIsCompleted != controller.value.isCompleted) {
1255+
currentIsCompleted = controller.value.isCompleted;
1256+
if (controller.value.isCompleted) {
1257+
isCompletedTest();
1258+
if (!hasLooped) {
1259+
fakeVideoEventStream.add(VideoEvent(
1260+
eventType: VideoEventType.isPlayingStateUpdate,
1261+
isPlaying: true));
1262+
hasLooped = !hasLooped;
1263+
}
1264+
} else {
1265+
isNoLongerCompletedTest();
1266+
}
1267+
}
1268+
});
1269+
1270+
fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.completed));
1271+
});
1272+
1273+
test('isCompleted updates on video seek to end', () async {
1274+
final VideoPlayerController controller = VideoPlayerController.networkUrl(
1275+
_localhostUri,
1276+
videoPlayerOptions: VideoPlayerOptions(),
1277+
);
1278+
1279+
await controller.initialize();
1280+
1281+
bool currentIsCompleted = controller.value.isCompleted;
1282+
1283+
final void Function() isCompletedTest = expectAsync0(() {});
1284+
1285+
controller.value =
1286+
controller.value.copyWith(duration: const Duration(seconds: 10));
1287+
1288+
controller.addListener(() async {
1289+
if (currentIsCompleted != controller.value.isCompleted) {
1290+
currentIsCompleted = controller.value.isCompleted;
1291+
if (controller.value.isCompleted) {
1292+
isCompletedTest();
1293+
}
1294+
}
1295+
});
1296+
1297+
// This call won't update isCompleted.
1298+
// The test will fail if `isCompletedTest` is called more than once.
1299+
await controller.seekTo(const Duration(seconds: 10));
1300+
1301+
await controller.seekTo(const Duration(seconds: 20));
1302+
});
12071303
}
12081304

12091305
class FakeVideoPlayerPlatform extends VideoPlayerPlatform {

0 commit comments

Comments
 (0)