From fd4b230b5b29202193f5e0b2ca2efddc8b16e598 Mon Sep 17 00:00:00 2001 From: KyleFin Date: Wed, 17 Mar 2021 22:31:03 -0600 Subject: [PATCH 1/7] Fix https://github.com/flutter/flutter/issues/77674 --- packages/video_player/video_player/lib/video_player.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 96dd73d3adea..03d726e2d291 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -314,8 +314,7 @@ class VideoPlayerController extends ValueNotifier { _applyPlayPause(); break; case VideoEventType.completed: - value = value.copyWith(isPlaying: false, position: value.duration); - _timer?.cancel(); + pause().then((void pauseResult) => seekTo(value.duration)); break; case VideoEventType.bufferingUpdate: value = value.copyWith(buffered: event.buffered); @@ -375,6 +374,9 @@ class VideoPlayerController extends ValueNotifier { /// has been sent to the platform, not when playback itself is totally /// finished. Future play() async { + if (value.position == value.duration) { + await seekTo(const Duration()); + } value = value.copyWith(isPlaying: true); await _applyPlayPause(); } From 2c88d48a2bf0a73f8d837b9debd514b42eb8c5df Mon Sep 17 00:00:00 2001 From: KyleFin <5882840+KyleFin@users.noreply.github.com> Date: Fri, 26 Mar 2021 10:27:57 -0600 Subject: [PATCH 2/7] Add unit tests. --- .../video_player/test/video_player_test.dart | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 582012097b71..b6a61ea4dd43 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -115,7 +115,7 @@ void main() { await tester.pumpWidget(VideoPlayer(controller1)); expect( find.byWidgetPredicate( - (Widget widget) => widget is Texture && widget.textureId == 101, + (Widget widget) => widget is Texture && widget.textureId == 101, ), findsOneWidget); @@ -124,7 +124,7 @@ void main() { await tester.pumpWidget(VideoPlayer(controller2)); expect( find.byWidgetPredicate( - (Widget widget) => widget is Texture && widget.textureId == 102, + (Widget widget) => widget is Texture && widget.textureId == 102, ), findsOneWidget); }); @@ -160,18 +160,18 @@ void main() { }); testWidgets('Passes text contrast ratio guidelines', - (WidgetTester tester) async { - final String text = 'foo'; - await tester.pumpWidget(MaterialApp( - home: Scaffold( - backgroundColor: Colors.white, - body: ClosedCaption(text: text), - ), - )); - expect(find.text(text), findsOneWidget); - - await expectLater(tester, meetsGuideline(textContrastGuideline)); - }, skip: isBrowser); + (WidgetTester tester) async { + final String text = 'foo'; + await tester.pumpWidget(MaterialApp( + home: Scaffold( + backgroundColor: Colors.white, + body: ClosedCaption(text: text), + ), + )); + expect(find.text(text), findsOneWidget); + + await expectLater(tester, meetsGuideline(textContrastGuideline)); + }, skip: isBrowser); }); group('VideoPlayerController', () { @@ -235,7 +235,7 @@ void main() { test('file', () async { final VideoPlayerController controller = - VideoPlayerController.file(File('a.avi')); + VideoPlayerController.file(File('a.avi')); await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, @@ -440,7 +440,7 @@ void main() { expect(controller.value.isBuffering, false); expect(controller.value.buffered, isEmpty); final FakeVideoEventStream fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.textureId]!; + fakeVideoPlayerPlatform.streams[controller.textureId]!; fakeVideoEventStream.eventsChannel .sendEvent({'event': 'bufferingStart'}); @@ -566,17 +566,17 @@ void main() { expect( value.toString(), 'VideoPlayerValue(duration: 0:00:05.000000, ' - 'size: Size(400.0, 300.0), ' - 'position: 0:00:01.000000, ' - 'caption: Caption(number: 0, start: 0:00:00.000000, end: 0:00:00.000000, text: foo), ' - 'buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], ' - 'isInitialized: true, ' - 'isPlaying: true, ' - 'isLooping: true, ' - 'isBuffering: true, ' - 'volume: 0.5, ' - 'playbackSpeed: 1.5, ' - 'errorDescription: null)'); + 'size: Size(400.0, 300.0), ' + 'position: 0:00:01.000000, ' + 'caption: Caption(number: 0, start: 0:00:00.000000, end: 0:00:00.000000, text: foo), ' + 'buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], ' + 'isInitialized: true, ' + 'isPlaying: true, ' + 'isLooping: true, ' + 'isBuffering: true, ' + 'volume: 0.5, ' + 'playbackSpeed: 1.5, ' + 'errorDescription: null)'); }); test('copyWith()', () { From be0bd21be764ef156feccb8becdec17e1fe05957 Mon Sep 17 00:00:00 2001 From: KyleFin <5882840+KyleFin@users.noreply.github.com> Date: Fri, 26 Mar 2021 10:46:12 -0600 Subject: [PATCH 3/7] Add unit tests. --- .../video_player/test/video_player_test.dart | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 582012097b71..1e087b77a0d2 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -418,6 +418,8 @@ void main() { 'https://127.0.0.1', ); await controller.initialize(); + const Duration nonzeroDuration = Duration(milliseconds: 100); + controller.value = controller.value.copyWith(duration: nonzeroDuration); expect(controller.value.isPlaying, isFalse); await controller.play(); expect(controller.value.isPlaying, isTrue); @@ -429,7 +431,37 @@ void main() { await tester.pumpAndSettle(); expect(controller.value.isPlaying, isFalse); - expect(controller.value.position, controller.value.duration); + expect(controller.value.position, nonzeroDuration); + expect( + fakeVideoPlayerPlatform + .calls[fakeVideoPlayerPlatform.calls.length - 2], + 'pause'); + expect(fakeVideoPlayerPlatform.calls.last, 'seekTo'); + }); + + testWidgets('play after playing completed restarts from beginning', + (WidgetTester tester) async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + const Duration nonzeroDuration = Duration(milliseconds: 100); + controller.value = controller.value.copyWith(duration: nonzeroDuration); + await controller.play(); + expect(controller.value.isPlaying, isTrue); + final FakeVideoEventStream fakeVideoEventStream = + fakeVideoPlayerPlatform.streams[controller.textureId]!; + fakeVideoEventStream.eventsChannel + .sendEvent({'event': 'completed'}); + await tester.pumpAndSettle(); + expect(controller.value.isPlaying, isFalse); + expect(controller.value.position, nonzeroDuration); + + await controller.play(); + + expect(controller.value.isPlaying, isTrue); + expect(controller.value.position, Duration.zero); + await controller.pause(); }); testWidgets('buffering status', (WidgetTester tester) async { From deaae24d22f56063f4efd97376631e03a7e6bc07 Mon Sep 17 00:00:00 2001 From: KyleFin <5882840+KyleFin@users.noreply.github.com> Date: Fri, 26 Mar 2021 10:49:13 -0600 Subject: [PATCH 4/7] dartfmt --- .../video_player/test/video_player_test.dart | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index c638608b1dd2..1e087b77a0d2 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -115,7 +115,7 @@ void main() { await tester.pumpWidget(VideoPlayer(controller1)); expect( find.byWidgetPredicate( - (Widget widget) => widget is Texture && widget.textureId == 101, + (Widget widget) => widget is Texture && widget.textureId == 101, ), findsOneWidget); @@ -124,7 +124,7 @@ void main() { await tester.pumpWidget(VideoPlayer(controller2)); expect( find.byWidgetPredicate( - (Widget widget) => widget is Texture && widget.textureId == 102, + (Widget widget) => widget is Texture && widget.textureId == 102, ), findsOneWidget); }); @@ -160,18 +160,18 @@ void main() { }); testWidgets('Passes text contrast ratio guidelines', - (WidgetTester tester) async { - final String text = 'foo'; - await tester.pumpWidget(MaterialApp( - home: Scaffold( - backgroundColor: Colors.white, - body: ClosedCaption(text: text), - ), - )); - expect(find.text(text), findsOneWidget); - - await expectLater(tester, meetsGuideline(textContrastGuideline)); - }, skip: isBrowser); + (WidgetTester tester) async { + final String text = 'foo'; + await tester.pumpWidget(MaterialApp( + home: Scaffold( + backgroundColor: Colors.white, + body: ClosedCaption(text: text), + ), + )); + expect(find.text(text), findsOneWidget); + + await expectLater(tester, meetsGuideline(textContrastGuideline)); + }, skip: isBrowser); }); group('VideoPlayerController', () { @@ -235,7 +235,7 @@ void main() { test('file', () async { final VideoPlayerController controller = - VideoPlayerController.file(File('a.avi')); + VideoPlayerController.file(File('a.avi')); await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, @@ -472,7 +472,7 @@ void main() { expect(controller.value.isBuffering, false); expect(controller.value.buffered, isEmpty); final FakeVideoEventStream fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.textureId]!; + fakeVideoPlayerPlatform.streams[controller.textureId]!; fakeVideoEventStream.eventsChannel .sendEvent({'event': 'bufferingStart'}); @@ -598,17 +598,17 @@ void main() { expect( value.toString(), 'VideoPlayerValue(duration: 0:00:05.000000, ' - 'size: Size(400.0, 300.0), ' - 'position: 0:00:01.000000, ' - 'caption: Caption(number: 0, start: 0:00:00.000000, end: 0:00:00.000000, text: foo), ' - 'buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], ' - 'isInitialized: true, ' - 'isPlaying: true, ' - 'isLooping: true, ' - 'isBuffering: true, ' - 'volume: 0.5, ' - 'playbackSpeed: 1.5, ' - 'errorDescription: null)'); + 'size: Size(400.0, 300.0), ' + 'position: 0:00:01.000000, ' + 'caption: Caption(number: 0, start: 0:00:00.000000, end: 0:00:00.000000, text: foo), ' + 'buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], ' + 'isInitialized: true, ' + 'isPlaying: true, ' + 'isLooping: true, ' + 'isBuffering: true, ' + 'volume: 0.5, ' + 'playbackSpeed: 1.5, ' + 'errorDescription: null)'); }); test('copyWith()', () { From 60a65957a6ae10491fb1590360cba0e2688bf55d Mon Sep 17 00:00:00 2001 From: KyleFin <5882840+KyleFin@users.noreply.github.com> Date: Fri, 26 Mar 2021 11:27:54 -0600 Subject: [PATCH 5/7] Update play() documentation. --- packages/video_player/video_player/lib/video_player.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 03d726e2d291..e761e89a8dd7 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -370,6 +370,8 @@ class VideoPlayerController extends ValueNotifier { /// Starts playing the video. /// + /// If the video is at the end, this method starts playing from the beginning. + /// /// This method returns a future that completes as soon as the "play" command /// has been sent to the platform, not when playback itself is totally /// finished. From 46af8d4e8540f990ce3aaebcc56e955c31eca47c Mon Sep 17 00:00:00 2001 From: KyleFin <5882840+kylefin@users.noreply.github.com> Date: Wed, 31 Mar 2021 20:46:35 -0600 Subject: [PATCH 6/7] Update test cases to use integration_tests. --- .../integration_test/video_player_test.dart | 45 ++++++++++++++++++ .../video_player/test/video_player_test.dart | 47 +++++++------------ 2 files changed, 62 insertions(+), 30 deletions(-) diff --git a/packages/video_player/video_player/example/integration_test/video_player_test.dart b/packages/video_player/video_player/example/integration_test/video_player_test.dart index b39bb087a21c..124cde07d639 100644 --- a/packages/video_player/video_player/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player/example/integration_test/video_player_test.dart @@ -129,6 +129,51 @@ void main() { }, ); + testWidgets( + 'stay paused when seeking after video completed', + (WidgetTester tester) async { + await _controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await _controller.setVolume(0); + Duration tenMillisBeforeEnd = + _controller.value.duration - const Duration(milliseconds: 10); + await _controller.seekTo(tenMillisBeforeEnd); + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + expect(_controller.value.isPlaying, false); + expect(_controller.value.position, _controller.value.duration); + + await _controller.seekTo(tenMillisBeforeEnd); + await tester.pumpAndSettle(_playDuration); + + expect(_controller.value.isPlaying, false); + expect(_controller.value.position, tenMillisBeforeEnd); + }, + ); + + testWidgets( + 'do not exceed duration on play after video completed', + (WidgetTester tester) async { + await _controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await _controller.setVolume(0); + await _controller.seekTo( + _controller.value.duration - const Duration(milliseconds: 10)); + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + expect(_controller.value.isPlaying, false); + expect(_controller.value.position, _controller.value.duration); + + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + + expect(_controller.value.position, + lessThanOrEqualTo(_controller.value.duration)); + }, + ); + testWidgets('test video player view with local asset', (WidgetTester tester) async { Future started() async { diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 1e087b77a0d2..3fa89ec3776c 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -277,6 +277,23 @@ void main() { expect(fakeVideoPlayerPlatform.calls.last, 'setPlaybackSpeed'); }); + test('play restarts from beginning if video is at end', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + const Duration nonzeroDuration = Duration(milliseconds: 100); + controller.value = controller.value.copyWith(duration: nonzeroDuration); + await controller.seekTo(nonzeroDuration); + expect(controller.value.isPlaying, isFalse); + expect(controller.value.position, nonzeroDuration); + + await controller.play(); + + expect(controller.value.isPlaying, isTrue); + expect(controller.value.position, Duration.zero); + }); + test('setLooping', () async { final VideoPlayerController controller = VideoPlayerController.network( 'https://127.0.0.1', @@ -432,36 +449,6 @@ void main() { expect(controller.value.isPlaying, isFalse); expect(controller.value.position, nonzeroDuration); - expect( - fakeVideoPlayerPlatform - .calls[fakeVideoPlayerPlatform.calls.length - 2], - 'pause'); - expect(fakeVideoPlayerPlatform.calls.last, 'seekTo'); - }); - - testWidgets('play after playing completed restarts from beginning', - (WidgetTester tester) async { - final VideoPlayerController controller = VideoPlayerController.network( - 'https://127.0.0.1', - ); - await controller.initialize(); - const Duration nonzeroDuration = Duration(milliseconds: 100); - controller.value = controller.value.copyWith(duration: nonzeroDuration); - await controller.play(); - expect(controller.value.isPlaying, isTrue); - final FakeVideoEventStream fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.textureId]!; - fakeVideoEventStream.eventsChannel - .sendEvent({'event': 'completed'}); - await tester.pumpAndSettle(); - expect(controller.value.isPlaying, isFalse); - expect(controller.value.position, nonzeroDuration); - - await controller.play(); - - expect(controller.value.isPlaying, isTrue); - expect(controller.value.position, Duration.zero); - await controller.pause(); }); testWidgets('buffering status', (WidgetTester tester) async { From d88ac674e53b0cb93229e6acfe8c722cccafe6b1 Mon Sep 17 00:00:00 2001 From: KyleFin <5882840+KyleFin@users.noreply.github.com> Date: Fri, 2 Jul 2021 15:20:27 -0600 Subject: [PATCH 7/7] Update version. --- packages/video_player/video_player/CHANGELOG.md | 4 ++++ packages/video_player/video_player/lib/video_player.dart | 4 ++++ packages/video_player/video_player/pubspec.yaml | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 08a5e443149e..cfe54d32c318 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.3 + +* Ensure video pauses correctly when it finishes. + ## 2.0.2 * Fix `VideoPlayerValue` size and aspect ratio documentation diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index e761e89a8dd7..aee0dfb141a6 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -314,6 +314,10 @@ class VideoPlayerController extends ValueNotifier { _applyPlayPause(); break; case VideoEventType.completed: + // In this case we need to stop _timer, set isPlaying=false, and + // position=value.duration. Instead of setting the values directly, + // 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)); break; case VideoEventType.bufferingUpdate: diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 17442d7ec09a..30ce2635d12b 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -1,7 +1,7 @@ name: video_player description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. -version: 2.0.2 +version: 2.0.3 homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player flutter: