diff --git a/packages/video_player/video_player/AUTHORS b/packages/video_player/video_player/AUTHORS index 02a9c690f33..e57e00b47c8 100644 --- a/packages/video_player/video_player/AUTHORS +++ b/packages/video_player/video_player/AUTHORS @@ -65,3 +65,4 @@ Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> Koen Van Looveren +Márton Matuz diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 67c9fc4fce0..368af5b1fd5 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.6.1 + +* Synchronizes `VideoPlayerValue.isPlaying` with underlying video player. + ## 2.6.0 * Adds option to configure HTTP headers via `VideoPlayerController` to fix access to M3U8 files on Android. diff --git a/packages/video_player/video_player/lib/src/closed_caption_file.dart b/packages/video_player/video_player/lib/src/closed_caption_file.dart index 324ffc471ff..91b44687d7d 100644 --- a/packages/video_player/video_player/lib/src/closed_caption_file.dart +++ b/packages/video_player/video_player/lib/src/closed_caption_file.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart' show objectRuntimeType; +import 'package:flutter/foundation.dart' show immutable, objectRuntimeType; import 'sub_rip.dart'; import 'web_vtt.dart'; @@ -32,6 +32,7 @@ abstract class ClosedCaptionFile { /// /// A typical closed captioning file will include several [Caption]s, each /// linked to a start and end time. +@immutable class Caption { /// Creates a new [Caption] object. /// @@ -74,4 +75,22 @@ class Caption { 'end: $end, ' 'text: $text)'; } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Caption && + runtimeType == other.runtimeType && + number == other.number && + start == other.start && + end == other.end && + text == other.text; + + @override + int get hashCode => Object.hash( + number, + start, + end, + text, + ); } diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 6a7661feacb..b2105e918eb 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -33,10 +33,11 @@ VideoPlayerPlatform get _videoPlayerPlatform { /// The duration, current position, buffering state, error state and settings /// of a [VideoPlayerController]. +@immutable class VideoPlayerValue { /// Constructs a video with the given values. Only [duration] is required. The /// rest will initialize with default values when unset. - VideoPlayerValue({ + const VideoPlayerValue({ required this.duration, this.size = Size.zero, this.position = Duration.zero, @@ -54,11 +55,11 @@ class VideoPlayerValue { }); /// Returns an instance for a video that hasn't been loaded. - VideoPlayerValue.uninitialized() + const VideoPlayerValue.uninitialized() : this(duration: Duration.zero, isInitialized: false); /// Returns an instance with the given [errorDescription]. - VideoPlayerValue.erroneous(String errorDescription) + const VideoPlayerValue.erroneous(String errorDescription) : this( duration: Duration.zero, isInitialized: false, @@ -195,6 +196,44 @@ class VideoPlayerValue { 'playbackSpeed: $playbackSpeed, ' 'errorDescription: $errorDescription)'; } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is VideoPlayerValue && + runtimeType == other.runtimeType && + duration == other.duration && + position == other.position && + caption == other.caption && + captionOffset == other.captionOffset && + listEquals(buffered, other.buffered) && + isPlaying == other.isPlaying && + isLooping == other.isLooping && + isBuffering == other.isBuffering && + volume == other.volume && + playbackSpeed == other.playbackSpeed && + errorDescription == other.errorDescription && + size == other.size && + rotationCorrection == other.rotationCorrection && + isInitialized == other.isInitialized; + + @override + int get hashCode => Object.hash( + duration, + position, + caption, + captionOffset, + buffered, + isPlaying, + isLooping, + isBuffering, + volume, + playbackSpeed, + errorDescription, + size, + rotationCorrection, + isInitialized, + ); } /// Controls a platform video player, and provides updates when the state is @@ -221,7 +260,7 @@ class VideoPlayerController extends ValueNotifier { dataSourceType = DataSourceType.asset, formatHint = null, httpHeaders = const {}, - super(VideoPlayerValue(duration: Duration.zero)); + super(const VideoPlayerValue(duration: Duration.zero)); /// Constructs a [VideoPlayerController] playing a video from obtained from /// the network. @@ -241,7 +280,7 @@ class VideoPlayerController extends ValueNotifier { }) : _closedCaptionFileFuture = closedCaptionFile, dataSourceType = DataSourceType.network, package = null, - super(VideoPlayerValue(duration: Duration.zero)); + super(const VideoPlayerValue(duration: Duration.zero)); /// Constructs a [VideoPlayerController] playing a video from a file. /// @@ -256,7 +295,7 @@ class VideoPlayerController extends ValueNotifier { dataSourceType = DataSourceType.file, package = null, formatHint = null, - super(VideoPlayerValue(duration: Duration.zero)); + super(const VideoPlayerValue(duration: Duration.zero)); /// Constructs a [VideoPlayerController] playing a video from a contentUri. /// @@ -272,7 +311,7 @@ class VideoPlayerController extends ValueNotifier { package = null, formatHint = null, httpHeaders = const {}, - super(VideoPlayerValue(duration: Duration.zero)); + super(const VideoPlayerValue(duration: Duration.zero)); /// The URI to the video file. This will be in different formats depending on /// the [DataSourceType] of the original video. @@ -372,7 +411,6 @@ class VideoPlayerController extends ValueNotifier { return; } - // ignore: missing_enum_constant_in_switch switch (event.eventType) { case VideoEventType.initialized: value = value.copyWith( @@ -403,6 +441,9 @@ class VideoPlayerController extends ValueNotifier { case VideoEventType.bufferingEnd: value = value.copyWith(isBuffering: false); break; + case VideoEventType.isPlayingStateUpdate: + value = value.copyWith(isPlaying: event.isPlaying); + break; case VideoEventType.unknown: break; } diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index ffe90c5b0b4..91d0a2ca8be 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -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.6.0 +version: 2.6.1 environment: sdk: ">=2.17.0 <4.0.0" @@ -25,7 +25,7 @@ dependencies: html: ^0.15.0 video_player_android: ^2.3.5 video_player_avfoundation: ^2.2.17 - video_player_platform_interface: ">=5.1.1 <7.0.0" + video_player_platform_interface: ">=6.1.0 <7.0.0" video_player_web: ^2.0.0 dev_dependencies: 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 a24ac843f6c..ef4f0bfa210 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -16,7 +16,7 @@ import 'package:video_player_platform_interface/video_player_platform_interface. class FakeController extends ValueNotifier implements VideoPlayerController { - FakeController() : super(VideoPlayerValue(duration: Duration.zero)); + FakeController() : super(const VideoPlayerValue(duration: Duration.zero)); FakeController.value(super.value); @@ -164,7 +164,8 @@ void main() { testWidgets('non-zero rotationCorrection value is used', (WidgetTester tester) async { final FakeController controller = FakeController.value( - VideoPlayerValue(duration: Duration.zero, rotationCorrection: 180)); + const VideoPlayerValue( + duration: Duration.zero, rotationCorrection: 180)); controller.textureId = 1; await tester.pumpWidget(VideoPlayer(controller)); final Transform actualRotationCorrection = @@ -184,7 +185,7 @@ void main() { testWidgets('no transform when rotationCorrection is zero', (WidgetTester tester) async { final FakeController controller = - FakeController.value(VideoPlayerValue(duration: Duration.zero)); + FakeController.value(const VideoPlayerValue(duration: Duration.zero)); controller.textureId = 1; await tester.pumpWidget(VideoPlayer(controller)); expect(find.byType(Transform), findsNothing); @@ -803,6 +804,30 @@ void main() { expect(controller.value.position, nonzeroDuration); }); + testWidgets('playback status', (WidgetTester tester) async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + expect(controller.value.isPlaying, isFalse); + final StreamController fakeVideoEventStream = + fakeVideoPlayerPlatform.streams[controller.textureId]!; + + fakeVideoEventStream.add(VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: true, + )); + await tester.pumpAndSettle(); + expect(controller.value.isPlaying, isTrue); + + fakeVideoEventStream.add(VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: false, + )); + await tester.pumpAndSettle(); + expect(controller.value.isPlaying, isFalse); + }); + testWidgets('buffering status', (WidgetTester tester) async { final VideoPlayerController controller = VideoPlayerController.network( 'https://127.0.0.1', @@ -865,7 +890,7 @@ void main() { group('VideoPlayerValue', () { test('uninitialized()', () { - final VideoPlayerValue uninitialized = VideoPlayerValue.uninitialized(); + const VideoPlayerValue uninitialized = VideoPlayerValue.uninitialized(); expect(uninitialized.duration, equals(Duration.zero)); expect(uninitialized.position, equals(Duration.zero)); @@ -886,7 +911,7 @@ void main() { test('erroneous()', () { const String errorMessage = 'foo'; - final VideoPlayerValue error = VideoPlayerValue.erroneous(errorMessage); + const VideoPlayerValue error = VideoPlayerValue.erroneous(errorMessage); expect(error.duration, equals(Duration.zero)); expect(error.position, equals(Duration.zero)); @@ -956,26 +981,26 @@ void main() { group('copyWith()', () { test('exact copy', () { - final VideoPlayerValue original = VideoPlayerValue.uninitialized(); + const VideoPlayerValue original = VideoPlayerValue.uninitialized(); final VideoPlayerValue exactCopy = original.copyWith(); expect(exactCopy.toString(), original.toString()); }); test('errorDescription is not persisted when copy with null', () { - final VideoPlayerValue original = VideoPlayerValue.erroneous('error'); + const VideoPlayerValue original = VideoPlayerValue.erroneous('error'); final VideoPlayerValue copy = original.copyWith(errorDescription: null); expect(copy.errorDescription, null); }); test('errorDescription is changed when copy with another error', () { - final VideoPlayerValue original = VideoPlayerValue.erroneous('error'); + const VideoPlayerValue original = VideoPlayerValue.erroneous('error'); final VideoPlayerValue copy = original.copyWith(errorDescription: 'new error'); expect(copy.errorDescription, 'new error'); }); test('errorDescription is changed when copy with error', () { - final VideoPlayerValue original = VideoPlayerValue.uninitialized(); + const VideoPlayerValue original = VideoPlayerValue.uninitialized(); final VideoPlayerValue copy = original.copyWith(errorDescription: 'new error'); @@ -985,45 +1010,45 @@ void main() { group('aspectRatio', () { test('640x480 -> 4:3', () { - final VideoPlayerValue value = VideoPlayerValue( + const VideoPlayerValue value = VideoPlayerValue( isInitialized: true, - size: const Size(640, 480), - duration: const Duration(seconds: 1), + size: Size(640, 480), + duration: Duration(seconds: 1), ); expect(value.aspectRatio, 4 / 3); }); test('no size -> 1.0', () { - final VideoPlayerValue value = VideoPlayerValue( + const VideoPlayerValue value = VideoPlayerValue( isInitialized: true, - duration: const Duration(seconds: 1), + duration: Duration(seconds: 1), ); expect(value.aspectRatio, 1.0); }); test('height = 0 -> 1.0', () { - final VideoPlayerValue value = VideoPlayerValue( + const VideoPlayerValue value = VideoPlayerValue( isInitialized: true, - size: const Size(640, 0), - duration: const Duration(seconds: 1), + size: Size(640, 0), + duration: Duration(seconds: 1), ); expect(value.aspectRatio, 1.0); }); test('width = 0 -> 1.0', () { - final VideoPlayerValue value = VideoPlayerValue( + const VideoPlayerValue value = VideoPlayerValue( isInitialized: true, - size: const Size(0, 480), - duration: const Duration(seconds: 1), + size: Size(0, 480), + duration: Duration(seconds: 1), ); expect(value.aspectRatio, 1.0); }); test('negative aspect ratio -> 1.0', () { - final VideoPlayerValue value = VideoPlayerValue( + const VideoPlayerValue value = VideoPlayerValue( isInitialized: true, - size: const Size(640, -480), - duration: const Duration(seconds: 1), + size: Size(640, -480), + duration: Duration(seconds: 1), ); expect(value.aspectRatio, 1.0); }); diff --git a/packages/video_player/video_player_android/AUTHORS b/packages/video_player/video_player_android/AUTHORS index 493a0b4ef9c..fc16c35c4c2 100644 --- a/packages/video_player/video_player_android/AUTHORS +++ b/packages/video_player/video_player_android/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Márton Matuz diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index fcb4f98f61f..7dbfe5da66e 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.4.4 +* Synchronizes `VideoPlayerValue.isPlaying` with `ExoPlayer`. * Updates minimum Flutter version to 3.3. ## 2.4.3 diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 2aed171033a..4701c7939fb 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -231,6 +231,16 @@ public void onPlayerError(final PlaybackException error) { eventSink.error("VideoError", "Video player had error " + error, null); } } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + if (eventSink != null) { + Map event = new HashMap<>(); + event.put("event", "isPlayingStateUpdate"); + event.put("isPlaying", isPlaying); + eventSink.success(event); + } + } }); } diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java index 5a646d8d41c..2ba6ee05388 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java @@ -5,10 +5,13 @@ package io.flutter.plugins.videoplayer; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -25,6 +28,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) @@ -234,4 +238,44 @@ public void sendInitializedSendsExpectedEvent_180RotationDegrees() { assertEquals(event.get("height"), 200); assertEquals(event.get("rotationCorrection"), 180); } + + @Test + public void onIsPlayingChangedSendsExpectedEvent() { + VideoPlayer videoPlayer = + new VideoPlayer( + fakeExoPlayer, + fakeEventChannel, + fakeSurfaceTextureEntry, + fakeVideoPlayerOptions, + fakeEventSink, + httpDataSourceFactorySpy); + + doAnswer( + (Answer) + invocation -> { + Map event = new HashMap<>(); + event.put("event", "isPlayingStateUpdate"); + event.put("isPlaying", (Boolean) invocation.getArguments()[0]); + fakeEventSink.success(event); + return null; + }) + .when(fakeExoPlayer) + .setPlayWhenReady(anyBoolean()); + + videoPlayer.play(); + + verify(fakeEventSink).success(eventCaptor.capture()); + HashMap event1 = eventCaptor.getValue(); + + assertEquals(event1.get("event"), "isPlayingStateUpdate"); + assertEquals(event1.get("isPlaying"), true); + + videoPlayer.pause(); + + verify(fakeEventSink, times(2)).success(eventCaptor.capture()); + HashMap event2 = eventCaptor.getValue(); + + assertEquals(event2.get("event"), "isPlayingStateUpdate"); + assertEquals(event2.get("isPlaying"), false); + } } diff --git a/packages/video_player/video_player_android/example/lib/mini_controller.dart b/packages/video_player/video_player_android/example/lib/mini_controller.dart index 55f08fef3c6..cea87ba95fa 100644 --- a/packages/video_player/video_player_android/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_android/example/lib/mini_controller.dart @@ -8,6 +8,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; @@ -24,10 +25,11 @@ VideoPlayerPlatform get _platform { /// The duration, current position, buffering state, error state and settings /// of a [MiniController]. +@immutable class VideoPlayerValue { /// Constructs a video with the given values. Only [duration] is required. The /// rest will initialize with default values when unset. - VideoPlayerValue({ + const VideoPlayerValue({ required this.duration, this.size = Size.zero, this.position = Duration.zero, @@ -40,11 +42,11 @@ class VideoPlayerValue { }); /// Returns an instance for a video that hasn't been loaded. - VideoPlayerValue.uninitialized() + const VideoPlayerValue.uninitialized() : this(duration: Duration.zero, isInitialized: false); /// Returns an instance with the given [errorDescription]. - VideoPlayerValue.erroneous(String errorDescription) + const VideoPlayerValue.erroneous(String errorDescription) : this( duration: Duration.zero, isInitialized: false, @@ -127,6 +129,34 @@ class VideoPlayerValue { errorDescription: errorDescription ?? this.errorDescription, ); } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is VideoPlayerValue && + runtimeType == other.runtimeType && + duration == other.duration && + position == other.position && + listEquals(buffered, other.buffered) && + isPlaying == other.isPlaying && + isBuffering == other.isBuffering && + playbackSpeed == other.playbackSpeed && + errorDescription == other.errorDescription && + size == other.size && + isInitialized == other.isInitialized; + + @override + int get hashCode => Object.hash( + duration, + position, + buffered, + isPlaying, + isBuffering, + playbackSpeed, + errorDescription, + size, + isInitialized, + ); } /// A very minimal version of `VideoPlayerController` for running the example @@ -139,21 +169,21 @@ class MiniController extends ValueNotifier { /// package and null otherwise. MiniController.asset(this.dataSource, {this.package}) : dataSourceType = DataSourceType.asset, - super(VideoPlayerValue(duration: Duration.zero)); + super(const VideoPlayerValue(duration: Duration.zero)); /// Constructs a [MiniController] playing a video from obtained from /// the network. MiniController.network(this.dataSource) : dataSourceType = DataSourceType.network, package = null, - super(VideoPlayerValue(duration: Duration.zero)); + super(const VideoPlayerValue(duration: Duration.zero)); /// Constructs a [MiniController] playing a video from obtained from a file. MiniController.file(File file) : dataSource = Uri.file(file.absolute.path).toString(), dataSourceType = DataSourceType.file, package = null, - super(VideoPlayerValue(duration: Duration.zero)); + super(const VideoPlayerValue(duration: Duration.zero)); /// The URI to the video file. This will be in different formats depending on /// the [DataSourceType] of the original video. @@ -219,7 +249,6 @@ class MiniController extends ValueNotifier { final Completer initializingCompleter = Completer(); void eventListener(VideoEvent event) { - // ignore: missing_enum_constant_in_switch switch (event.eventType) { case VideoEventType.initialized: value = value.copyWith( @@ -244,6 +273,9 @@ class MiniController extends ValueNotifier { case VideoEventType.bufferingEnd: value = value.copyWith(isBuffering: false); break; + case VideoEventType.isPlayingStateUpdate: + value = value.copyWith(isPlaying: event.isPlaying); + break; case VideoEventType.unknown: break; } diff --git a/packages/video_player/video_player_android/example/pubspec.yaml b/packages/video_player/video_player_android/example/pubspec.yaml index 2b4bb097905..e11d753aa8a 100644 --- a/packages/video_player/video_player_android/example/pubspec.yaml +++ b/packages/video_player/video_player_android/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - video_player_platform_interface: ">=5.1.1 <7.0.0" + video_player_platform_interface: ">=6.1.0 <7.0.0" dev_dependencies: flutter_driver: diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index c8a2b7adbcb..3d34fc8c94b 100644 --- a/packages/video_player/video_player_android/lib/src/android_video_player.dart +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -148,6 +148,11 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { return VideoEvent(eventType: VideoEventType.bufferingStart); case 'bufferingEnd': return VideoEvent(eventType: VideoEventType.bufferingEnd); + case 'isPlayingStateUpdate': + return VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: map['isPlaying'] as bool, + ); default: return VideoEvent(eventType: VideoEventType.unknown); } diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index b847a6d9c07..a77b6463f15 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.4.3 +version: 2.4.4 environment: sdk: ">=2.18.0 <4.0.0" @@ -20,7 +20,7 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ">=5.1.1 <7.0.0" + video_player_platform_interface: ">=6.1.0 <7.0.0" dev_dependencies: flutter_test: diff --git a/packages/video_player/video_player_android/test/android_video_player_test.dart b/packages/video_player/video_player_android/test/android_video_player_test.dart index 00feb274396..d30d1d4faaa 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.dart @@ -246,10 +246,11 @@ void main() { }); test('videoEventsFor', () async { + const String mockChannel = 'flutter.io/videoPlayer/videoEvents123'; _ambiguate(TestDefaultBinaryMessengerBinding.instance)! .defaultBinaryMessenger .setMockMessageHandler( - 'flutter.io/videoPlayer/videoEvents123', + mockChannel, (ByteData? message) async { final MethodCall methodCall = const StandardMethodCodec().decodeMethodCall(message); @@ -257,7 +258,7 @@ void main() { await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! .defaultBinaryMessenger .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', + mockChannel, const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'initialized', @@ -270,7 +271,7 @@ void main() { await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! .defaultBinaryMessenger .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', + mockChannel, const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'initialized', @@ -284,7 +285,7 @@ void main() { await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! .defaultBinaryMessenger .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', + mockChannel, const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'completed', @@ -294,7 +295,7 @@ void main() { await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! .defaultBinaryMessenger .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', + mockChannel, const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'bufferingUpdate', @@ -308,7 +309,7 @@ void main() { await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! .defaultBinaryMessenger .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', + mockChannel, const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'bufferingStart', @@ -318,13 +319,35 @@ void main() { await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! .defaultBinaryMessenger .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', + mockChannel, const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'bufferingEnd', }), (ByteData? data) {}); + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + mockChannel, + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'isPlayingStateUpdate', + 'isPlaying': true, + }), + (ByteData? data) {}); + + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + mockChannel, + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'isPlayingStateUpdate', + 'isPlaying': false, + }), + (ByteData? data) {}); + return const StandardMethodCodec().encodeSuccessEnvelope(null); } else if (methodCall.method == 'cancel') { return const StandardMethodCodec().encodeSuccessEnvelope(null); @@ -363,6 +386,14 @@ void main() { ]), VideoEvent(eventType: VideoEventType.bufferingStart), VideoEvent(eventType: VideoEventType.bufferingEnd), + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: true, + ), + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: false, + ), ])); }); }); diff --git a/packages/video_player/video_player_avfoundation/AUTHORS b/packages/video_player/video_player_avfoundation/AUTHORS index 493a0b4ef9c..fc16c35c4c2 100644 --- a/packages/video_player/video_player_avfoundation/AUTHORS +++ b/packages/video_player/video_player_avfoundation/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Márton Matuz diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index b2308dd0fe7..ef73b92c791 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.4.3 + +* Synchronizes `VideoPlayerValue.isPlaying` with `AVPlayer`. + ## 2.4.2 * Makes seekTo async and only complete when AVPlayer.seekTo completes. diff --git a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart index 55f08fef3c6..cea87ba95fa 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart @@ -8,6 +8,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; @@ -24,10 +25,11 @@ VideoPlayerPlatform get _platform { /// The duration, current position, buffering state, error state and settings /// of a [MiniController]. +@immutable class VideoPlayerValue { /// Constructs a video with the given values. Only [duration] is required. The /// rest will initialize with default values when unset. - VideoPlayerValue({ + const VideoPlayerValue({ required this.duration, this.size = Size.zero, this.position = Duration.zero, @@ -40,11 +42,11 @@ class VideoPlayerValue { }); /// Returns an instance for a video that hasn't been loaded. - VideoPlayerValue.uninitialized() + const VideoPlayerValue.uninitialized() : this(duration: Duration.zero, isInitialized: false); /// Returns an instance with the given [errorDescription]. - VideoPlayerValue.erroneous(String errorDescription) + const VideoPlayerValue.erroneous(String errorDescription) : this( duration: Duration.zero, isInitialized: false, @@ -127,6 +129,34 @@ class VideoPlayerValue { errorDescription: errorDescription ?? this.errorDescription, ); } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is VideoPlayerValue && + runtimeType == other.runtimeType && + duration == other.duration && + position == other.position && + listEquals(buffered, other.buffered) && + isPlaying == other.isPlaying && + isBuffering == other.isBuffering && + playbackSpeed == other.playbackSpeed && + errorDescription == other.errorDescription && + size == other.size && + isInitialized == other.isInitialized; + + @override + int get hashCode => Object.hash( + duration, + position, + buffered, + isPlaying, + isBuffering, + playbackSpeed, + errorDescription, + size, + isInitialized, + ); } /// A very minimal version of `VideoPlayerController` for running the example @@ -139,21 +169,21 @@ class MiniController extends ValueNotifier { /// package and null otherwise. MiniController.asset(this.dataSource, {this.package}) : dataSourceType = DataSourceType.asset, - super(VideoPlayerValue(duration: Duration.zero)); + super(const VideoPlayerValue(duration: Duration.zero)); /// Constructs a [MiniController] playing a video from obtained from /// the network. MiniController.network(this.dataSource) : dataSourceType = DataSourceType.network, package = null, - super(VideoPlayerValue(duration: Duration.zero)); + super(const VideoPlayerValue(duration: Duration.zero)); /// Constructs a [MiniController] playing a video from obtained from a file. MiniController.file(File file) : dataSource = Uri.file(file.absolute.path).toString(), dataSourceType = DataSourceType.file, package = null, - super(VideoPlayerValue(duration: Duration.zero)); + super(const VideoPlayerValue(duration: Duration.zero)); /// The URI to the video file. This will be in different formats depending on /// the [DataSourceType] of the original video. @@ -219,7 +249,6 @@ class MiniController extends ValueNotifier { final Completer initializingCompleter = Completer(); void eventListener(VideoEvent event) { - // ignore: missing_enum_constant_in_switch switch (event.eventType) { case VideoEventType.initialized: value = value.copyWith( @@ -244,6 +273,9 @@ class MiniController extends ValueNotifier { case VideoEventType.bufferingEnd: value = value.copyWith(isBuffering: false); break; + case VideoEventType.isPlayingStateUpdate: + value = value.copyWith(isPlaying: event.isPlaying); + break; case VideoEventType.unknown: break; } diff --git a/packages/video_player/video_player_avfoundation/example/pubspec.yaml b/packages/video_player/video_player_avfoundation/example/pubspec.yaml index f8b9ca66faf..bffa17d11e1 100644 --- a/packages/video_player/video_player_avfoundation/example/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - video_player_platform_interface: ">=4.2.0 <7.0.0" + video_player_platform_interface: ">=6.1.0 <7.0.0" dev_dependencies: flutter_driver: diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m index ce0a12154c2..586d6555bef 100644 --- a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m +++ b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m @@ -62,6 +62,7 @@ - (instancetype)initWithURL:(NSURL *)url static void *playbackLikelyToKeepUpContext = &playbackLikelyToKeepUpContext; static void *playbackBufferEmptyContext = &playbackBufferEmptyContext; static void *playbackBufferFullContext = &playbackBufferFullContext; +static void *rateContext = &rateContext; @implementation FLTVideoPlayer - (instancetype)initWithAsset:(NSString *)asset frameUpdater:(FLTFrameUpdater *)frameUpdater { @@ -69,7 +70,7 @@ - (instancetype)initWithAsset:(NSString *)asset frameUpdater:(FLTFrameUpdater *) return [self initWithURL:[NSURL fileURLWithPath:path] frameUpdater:frameUpdater httpHeaders:@{}]; } -- (void)addObservers:(AVPlayerItem *)item { +- (void)addObserversForItem:(AVPlayerItem *)item player:(AVPlayer *)player { [item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew @@ -99,6 +100,13 @@ - (void)addObservers:(AVPlayerItem *)item { options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:playbackBufferFullContext]; + // Add observer to AVPlayer instead of AVPlayerItem since the AVPlayerItem does not have a "rate" + // property + [player addObserver:self + forKeyPath:@"rate" + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:rateContext]; + // Add an observer that will respond to itemDidPlayToEndTime [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(itemDidPlayToEndTime:) @@ -252,7 +260,7 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item [self createVideoOutputAndDisplayLink:frameUpdater]; - [self addObservers:item]; + [self addObserversForItem:item player:_player]; [asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ] completionHandler:assetCompletionHandler]; @@ -317,6 +325,14 @@ - (void)observeValueForKeyPath:(NSString *)path if (_eventSink != nil) { _eventSink(@{@"event" : @"bufferingEnd"}); } + } else if (context == rateContext) { + // Important: Make sure to cast the object to AVPlayer when observing the rate property, + // as it is not available in AVPlayerItem. + AVPlayer *player = (AVPlayer *)object; + if (_eventSink != nil) { + _eventSink( + @{@"event" : @"isPlayingStateUpdate", @"isPlaying" : player.rate > 0 ? @YES : @NO}); + } } } diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index b5ebedda41e..868c5986f35 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -146,6 +146,11 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { return VideoEvent(eventType: VideoEventType.bufferingStart); case 'bufferingEnd': return VideoEvent(eventType: VideoEventType.bufferingEnd); + case 'isPlayingStateUpdate': + return VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: map['isPlaying'] as bool, + ); default: return VideoEvent(eventType: VideoEventType.unknown); } diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 9f5566cc8d3..6cb0565c18b 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_avfoundation description: iOS implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.4.2 +version: 2.4.3 environment: sdk: ">=2.18.0 <4.0.0" @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ">=4.2.0 <7.0.0" + video_player_platform_interface: ">=6.1.0 <7.0.0" dev_dependencies: flutter_test: diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart index a0f37e2175f..1b25da8bbaa 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart @@ -234,10 +234,11 @@ void main() { }); test('videoEventsFor', () async { + const String mockChannel = 'flutter.io/videoPlayer/videoEvents123'; _ambiguate(TestDefaultBinaryMessengerBinding.instance)! .defaultBinaryMessenger .setMockMessageHandler( - 'flutter.io/videoPlayer/videoEvents123', + mockChannel, (ByteData? message) async { final MethodCall methodCall = const StandardMethodCodec().decodeMethodCall(message); @@ -245,7 +246,7 @@ void main() { await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! .defaultBinaryMessenger .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', + mockChannel, const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'initialized', @@ -258,7 +259,7 @@ void main() { await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! .defaultBinaryMessenger .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', + mockChannel, const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'completed', @@ -268,7 +269,7 @@ void main() { await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! .defaultBinaryMessenger .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', + mockChannel, const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'bufferingUpdate', @@ -282,7 +283,7 @@ void main() { await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! .defaultBinaryMessenger .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', + mockChannel, const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'bufferingStart', @@ -292,13 +293,35 @@ void main() { await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! .defaultBinaryMessenger .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', + mockChannel, const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'bufferingEnd', }), (ByteData? data) {}); + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + mockChannel, + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'isPlayingStateUpdate', + 'isPlaying': true, + }), + (ByteData? data) {}); + + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + mockChannel, + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'isPlayingStateUpdate', + 'isPlaying': false, + }), + (ByteData? data) {}); + return const StandardMethodCodec().encodeSuccessEnvelope(null); } else if (methodCall.method == 'cancel') { return const StandardMethodCodec().encodeSuccessEnvelope(null); @@ -330,6 +353,14 @@ void main() { ]), VideoEvent(eventType: VideoEventType.bufferingStart), VideoEvent(eventType: VideoEventType.bufferingEnd), + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: true, + ), + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: false, + ), ])); }); }); diff --git a/packages/video_player/video_player_web/AUTHORS b/packages/video_player/video_player_web/AUTHORS index 493a0b4ef9c..fc16c35c4c2 100644 --- a/packages/video_player/video_player_web/AUTHORS +++ b/packages/video_player/video_player_web/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Márton Matuz diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 160c1548539..19f5bb927f2 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.16 + +* Synchronizes `VideoPlayerValue.isPlaying` with `VideoElement`. + ## 2.0.15 * Clarifies explanation of endorsement in README. diff --git a/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart b/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart index 5053ea6e5b0..7d742239309 100644 --- a/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart +++ b/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart @@ -169,13 +169,47 @@ void main() { expect(VideoPlayerPlatform.instance.setMixWithOthers(false), completes); }); + testWidgets( + 'double call to play will emit a single isPlayingStateUpdate event', + (WidgetTester tester) async { + final int videoPlayerId = await textureId; + final Stream eventStream = + VideoPlayerPlatform.instance.videoEventsFor(videoPlayerId); + + final Future> stream = eventStream.timeout( + const Duration(seconds: 2), + onTimeout: (EventSink sink) { + sink.close(); + }, + ).toList(); + + await VideoPlayerPlatform.instance.setVolume(videoPlayerId, 0); + await VideoPlayerPlatform.instance.play(videoPlayerId); + await VideoPlayerPlatform.instance.play(videoPlayerId); + + // Let the video play, until we stop seeing events for two seconds + final List events = await stream; + + await VideoPlayerPlatform.instance.pause(videoPlayerId); + + expect( + events.where((VideoEvent e) => + e.eventType == VideoEventType.isPlayingStateUpdate), + equals([ + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: true, + ) + ])); + }); + testWidgets('video playback lifecycle', (WidgetTester tester) async { final int videoPlayerId = await textureId; final Stream eventStream = VideoPlayerPlatform.instance.videoEventsFor(videoPlayerId); final Future> stream = eventStream.timeout( - const Duration(seconds: 1), + const Duration(seconds: 2), onTimeout: (EventSink sink) { sink.close(); }, @@ -184,23 +218,25 @@ void main() { await VideoPlayerPlatform.instance.setVolume(videoPlayerId, 0); await VideoPlayerPlatform.instance.play(videoPlayerId); - // Let the video play, until we stop seeing events for a second + // Let the video play, until we stop seeing events for two seconds final List events = await stream; await VideoPlayerPlatform.instance.pause(videoPlayerId); // The expected list of event types should look like this: - // 1. bufferingStart, - // 2. bufferingUpdate (videoElement.onWaiting), - // 3. initialized (videoElement.onCanPlay), - // 4. bufferingEnd (videoElement.onCanPlayThrough), + // 1. isPlayingStateUpdate (videoElement.onPlaying) + // 2. bufferingStart, + // 3. bufferingUpdate (videoElement.onWaiting), + // 4. initialized (videoElement.onCanPlay), + // 5. bufferingEnd (videoElement.onCanPlayThrough), expect( events.map((VideoEvent e) => e.eventType), equals([ + VideoEventType.isPlayingStateUpdate, VideoEventType.bufferingStart, VideoEventType.bufferingUpdate, VideoEventType.initialized, - VideoEventType.bufferingEnd + VideoEventType.bufferingEnd, ])); }); }); diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml index 3114335127c..4cb1d4d7fa2 100644 --- a/packages/video_player/video_player_web/example/pubspec.yaml +++ b/packages/video_player/video_player_web/example/pubspec.yaml @@ -9,7 +9,7 @@ dependencies: flutter: sdk: flutter js: ^0.6.0 - video_player_platform_interface: ">=4.2.0 <7.0.0" + video_player_platform_interface: ">=6.1.0 <7.0.0" video_player_web: path: ../ diff --git a/packages/video_player/video_player_web/lib/src/video_player.dart b/packages/video_player/video_player_web/lib/src/video_player.dart index 02ead1fdf93..bc0021dee34 100644 --- a/packages/video_player/video_player_web/lib/src/video_player.dart +++ b/packages/video_player/video_player_web/lib/src/video_player.dart @@ -102,6 +102,20 @@ class VideoPlayer { )); }); + _videoElement.onPlay.listen((dynamic _) { + _eventController.add(VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: true, + )); + }); + + _videoElement.onPause.listen((dynamic _) { + _eventController.add(VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: false, + )); + }); + _videoElement.onEnded.listen((dynamic _) { setBuffering(false); _eventController.add(VideoEvent(eventType: VideoEventType.completed)); diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index 07d9b8437c6..70434ef882d 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_web description: Web platform implementation of video_player. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.0.15 +version: 2.0.16 environment: sdk: ">=2.17.0 <4.0.0" @@ -21,7 +21,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - video_player_platform_interface: ">=4.2.0 <7.0.0" + video_player_platform_interface: ">=6.1.0 <7.0.0" dev_dependencies: flutter_test: