diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 8e9e9f21f73..cac6a397996 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.3.0 + +* Migrates package and tests to `package:web``. +* Fixes infinite event loop caused by `seekTo` when the video ends. + ## 2.2.0 * Updates SDK version to Dart `^3.3.0`. Flutter `^3.19.0`. diff --git a/packages/video_player/video_player_web/example/integration_test/pkg_web_tweaks.dart b/packages/video_player/video_player_web/example/integration_test/pkg_web_tweaks.dart new file mode 100644 index 00000000000..ccf5b32e5c6 --- /dev/null +++ b/packages/video_player/video_player_web/example/integration_test/pkg_web_tweaks.dart @@ -0,0 +1,75 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@JS() +library video_player_web_integration_test_pkg_web_tweaks; + +import 'dart:js_interop'; +import 'package:web/web.dart' as web; + +/// Adds a `controlsList` and `disablePictureInPicture` getters. +extension NonStandardGettersOnVideoElement on web.HTMLVideoElement { + external web.DOMTokenList? get controlsList; + external JSBoolean get disablePictureInPicture; +} + +/// Adds a `disableRemotePlayback` getter. +extension NonStandardGettersOnMediaElement on web.HTMLMediaElement { + external JSBoolean get disableRemotePlayback; +} + +/// Defines JS interop to access static methods from `Object`. +@JS('Object') +extension type DomObject._(JSAny _) { + @JS('defineProperty') + external static void _defineProperty( + JSAny? object, JSString property, Descriptor value); + + /// `Object.defineProperty`. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty + static void defineProperty( + JSObject object, String property, Descriptor descriptor) { + return _defineProperty(object, property.toJS, descriptor); + } +} + +/// The descriptor for the property being defined or modified with `defineProperty`. +/// +/// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description +extension type Descriptor._(JSObject _) implements JSObject { + /// Builds a "data descriptor". + factory Descriptor.data({ + bool? writable, + JSAny? value, + }) => + Descriptor._data( + writable: writable?.toJS, + value: value.jsify(), + ); + + /// Builds an "accessor descriptor". + factory Descriptor.accessor({ + void Function(JSAny? value)? set, + JSAny? Function()? get, + }) => + Descriptor._accessor( + set: set?.toJS, + get: get?.toJS, + ); + + external factory Descriptor._accessor({ + // JSBoolean configurable, + // JSBoolean enumerable, + JSFunction? set, + JSFunction? get, + }); + + external factory Descriptor._data({ + // JSBoolean configurable, + // JSBoolean enumerable, + JSBoolean? writable, + JSAny? value, + }); +} diff --git a/packages/video_player/video_player_web/example/integration_test/utils.dart b/packages/video_player/video_player_web/example/integration_test/utils.dart index 75af525d410..ba15c39c57d 100644 --- a/packages/video_player/video_player_web/example/integration_test/utils.dart +++ b/packages/video_player/video_player_web/example/integration_test/utils.dart @@ -3,8 +3,9 @@ // found in the LICENSE file. import 'dart:js_interop'; -import 'dart:js_interop_unsafe'; + import 'package:web/web.dart' as web; +import 'pkg_web_tweaks.dart'; // Returns the URL to load an asset from this example app as a network source. // @@ -19,40 +20,29 @@ String getUrlForAssetAsNetworkSource(String assetKey) { '?raw=true'; } -extension type Descriptor._(JSObject _) implements JSObject { - // May also contain "configurable" and "enumerable" bools. - // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description - external factory Descriptor({ - // bool configurable, - // bool enumerable, - JSBoolean writable, - JSAny value, - }); -} - -void _defineProperty( - Object object, - String property, - Descriptor description, -) { - (globalContext['Object'] as JSObject?)?.callMethod( - 'defineProperty'.toJS, - object as JSObject, - property.toJS, - description, - ); -} - /// Forces a VideoElement to report "Infinity" duration. /// /// Uses JS Object.defineProperty to set the value of a readonly property. -void setInfinityDuration(Object videoElement) { - assert(videoElement is web.HTMLVideoElement); - _defineProperty( - videoElement, - 'duration', - Descriptor( - writable: true.toJS, - value: double.infinity.toJS, +void setInfinityDuration(web.HTMLVideoElement element) { + DomObject.defineProperty( + element, + 'duration', + Descriptor.data( + writable: true, + value: double.infinity.toJS, + ), + ); +} + +/// Makes the `currentTime` setter throw an exception if used. +void makeSetCurrentTimeThrow(web.HTMLVideoElement element) { + DomObject.defineProperty( + element, + 'currentTime', + Descriptor.accessor( + set: (JSAny? value) { + throw Exception('Unexpected call to currentTime with value: $value'); + }, + get: () => 100.toJS, )); } diff --git a/packages/video_player/video_player_web/example/integration_test/video_player_test.dart b/packages/video_player/video_player_web/example/integration_test/video_player_test.dart index 01f8e2f3435..51199ba79d2 100644 --- a/packages/video_player/video_player_web/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player_web/example/integration_test/video_player_test.dart @@ -3,27 +3,28 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:html' as html; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import 'package:video_player_web/src/duration_utils.dart'; import 'package:video_player_web/src/video_player.dart'; +import 'package:web/web.dart' as web; +import 'pkg_web_tweaks.dart'; import 'utils.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('VideoPlayer', () { - late html.VideoElement video; + late web.HTMLVideoElement video; setUp(() { // Never set "src" on the video, so this test doesn't hit the network! - video = html.VideoElement() + video = web.HTMLVideoElement() ..controls = true - ..setAttribute('playsinline', 'false'); + ..playsInline = false; }); testWidgets('fixes critical video element config', (WidgetTester _) async { @@ -36,8 +37,7 @@ void main() { // see: https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML expect(video.getAttribute('autoplay'), isNull, reason: 'autoplay attribute on video tag must NOT be set'); - expect(video.getAttribute('playsinline'), 'true', - reason: 'Needed by safari iOS'); + expect(video.playsInline, true, reason: 'Needed by safari iOS'); }); testWidgets('setVolume', (WidgetTester tester) async { @@ -69,12 +69,32 @@ void main() { }, throwsAssertionError, reason: 'Playback speed cannot be == 0'); }); - testWidgets('seekTo', (WidgetTester tester) async { - final VideoPlayer player = VideoPlayer(videoElement: video)..initialize(); + group('seekTo', () { + testWidgets('negative time - throws assert', (WidgetTester tester) async { + final VideoPlayer player = VideoPlayer(videoElement: video) + ..initialize(); - expect(() { - player.seekTo(const Duration(seconds: -1)); - }, throwsAssertionError, reason: 'Cannot seek into negative numbers'); + expect(() { + player.seekTo(const Duration(seconds: -1)); + }, throwsAssertionError, reason: 'Cannot seek into negative numbers'); + }); + + testWidgets('setting currentTime to its current value - noop', + (WidgetTester tester) async { + makeSetCurrentTimeThrow(video); + final VideoPlayer player = VideoPlayer(videoElement: video) + ..initialize(); + + expect(() { + // Self-test... + video.currentTime = 123; + }, throwsException, reason: 'Setting currentTime must throw!'); + + expect(() { + // Should not set currentTime (and throw) when seekTo current time. + player.seekTo(Duration(seconds: video.currentTime.toInt())); + }, returnsNormally); + }); }); // The events tested in this group do *not* represent the actual sequence @@ -145,7 +165,7 @@ void main() { player.setBuffering(true); // Simulate "canplay" event... - video.dispatchEvent(html.Event('canplay')); + video.dispatchEvent(web.Event('canplay')); final List events = await stream; @@ -166,7 +186,7 @@ void main() { player.setBuffering(true); // Simulate "canplaythrough" event... - video.dispatchEvent(html.Event('canplaythrough')); + video.dispatchEvent(web.Event('canplaythrough')); final List events = await stream; @@ -177,9 +197,9 @@ void main() { testWidgets('initialized dispatches only once', (WidgetTester tester) async { // Dispatch some bogus "canplay" events from the video object - video.dispatchEvent(html.Event('canplay')); - video.dispatchEvent(html.Event('canplay')); - video.dispatchEvent(html.Event('canplay')); + video.dispatchEvent(web.Event('canplay')); + video.dispatchEvent(web.Event('canplay')); + video.dispatchEvent(web.Event('canplay')); // Take all the "initialized" events that we see during the next few seconds final Future> stream = timedStream @@ -187,9 +207,9 @@ void main() { event.eventType == VideoEventType.initialized) .toList(); - video.dispatchEvent(html.Event('canplay')); - video.dispatchEvent(html.Event('canplay')); - video.dispatchEvent(html.Event('canplay')); + video.dispatchEvent(web.Event('canplay')); + video.dispatchEvent(web.Event('canplay')); + video.dispatchEvent(web.Event('canplay')); final List events = await stream; @@ -200,8 +220,8 @@ void main() { // Issue: https://github.com/flutter/flutter/issues/137023 testWidgets('loadedmetadata dispatches initialized', (WidgetTester tester) async { - video.dispatchEvent(html.Event('loadedmetadata')); - video.dispatchEvent(html.Event('loadedmetadata')); + video.dispatchEvent(web.Event('loadedmetadata')); + video.dispatchEvent(web.Event('loadedmetadata')); final Future> stream = timedStream .where((VideoEvent event) => @@ -224,7 +244,7 @@ void main() { event.eventType == VideoEventType.initialized) .toList(); - video.dispatchEvent(html.Event('canplay')); + video.dispatchEvent(web.Event('canplay')); final List events = await stream; @@ -238,7 +258,7 @@ void main() { late VideoPlayer player; setUp(() { - video = html.VideoElement(); + video = web.HTMLVideoElement(); player = VideoPlayer(videoElement: video)..initialize(); }); @@ -271,7 +291,7 @@ void main() { expect(video.controlsList?.contains('nodownload'), isFalse); expect(video.controlsList?.contains('nofullscreen'), isFalse); expect(video.controlsList?.contains('noplaybackrate'), isFalse); - expect(video.getAttribute('disablePictureInPicture'), isNull); + expect(video.disablePictureInPicture, isFalse); }); testWidgets('and no download expect correct controls', @@ -290,7 +310,7 @@ void main() { expect(video.controlsList?.contains('nodownload'), isTrue); expect(video.controlsList?.contains('nofullscreen'), isFalse); expect(video.controlsList?.contains('noplaybackrate'), isFalse); - expect(video.getAttribute('disablePictureInPicture'), isNull); + expect(video.disablePictureInPicture, isFalse); }); testWidgets('and no fullscreen expect correct controls', @@ -309,7 +329,7 @@ void main() { expect(video.controlsList?.contains('nodownload'), isFalse); expect(video.controlsList?.contains('nofullscreen'), isTrue); expect(video.controlsList?.contains('noplaybackrate'), isFalse); - expect(video.getAttribute('disablePictureInPicture'), isNull); + expect(video.disablePictureInPicture, isFalse); }); testWidgets('and no playback rate expect correct controls', @@ -328,7 +348,7 @@ void main() { expect(video.controlsList?.contains('nodownload'), isFalse); expect(video.controlsList?.contains('nofullscreen'), isFalse); expect(video.controlsList?.contains('noplaybackrate'), isTrue); - expect(video.getAttribute('disablePictureInPicture'), isNull); + expect(video.disablePictureInPicture, isFalse); }); testWidgets('and no picture in picture expect correct controls', @@ -347,7 +367,7 @@ void main() { expect(video.controlsList?.contains('nodownload'), isFalse); expect(video.controlsList?.contains('nofullscreen'), isFalse); expect(video.controlsList?.contains('noplaybackrate'), isFalse); - expect(video.getAttribute('disablePictureInPicture'), 'true'); + expect(video.disablePictureInPicture, isTrue); }); }); }); @@ -362,7 +382,7 @@ void main() { ), ); - expect(video.getAttribute('disableRemotePlayback'), isNull); + expect(video.disableRemotePlayback, isFalse); }); testWidgets('when disabled expect attribute', @@ -373,7 +393,7 @@ void main() { ), ); - expect(video.getAttribute('disableRemotePlayback'), 'true'); + expect(video.disableRemotePlayback, isTrue); }); }); @@ -398,8 +418,8 @@ void main() { expect(video.controlsList?.contains('nodownload'), isTrue); expect(video.controlsList?.contains('nofullscreen'), isTrue); expect(video.controlsList?.contains('noplaybackrate'), isTrue); - expect(video.getAttribute('disablePictureInPicture'), 'true'); - expect(video.getAttribute('disableRemotePlayback'), 'true'); + expect(video.disablePictureInPicture, isTrue); + expect(video.disableRemotePlayback, isTrue); }); group('when called once more', () { @@ -421,8 +441,8 @@ void main() { expect(video.controlsList?.contains('nodownload'), isFalse); expect(video.controlsList?.contains('nofullscreen'), isFalse); expect(video.controlsList?.contains('noplaybackrate'), isFalse); - expect(video.getAttribute('disablePictureInPicture'), isNull); - expect(video.getAttribute('disableRemotePlayback'), isNull); + expect(video.disablePictureInPicture, isFalse); + expect(video.disableRemotePlayback, isFalse); }); }); }); diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml index bd2f3ba4878..27aba7660bb 100644 --- a/packages/video_player/video_player_web/example/pubspec.yaml +++ b/packages/video_player/video_player_web/example/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: video_player_platform_interface: ^6.1.0 video_player_web: path: ../ - web: ^0.5.0 + web: ^0.5.1 dev_dependencies: flutter_test: diff --git a/packages/video_player/video_player_web/lib/src/pkg_web_tweaks.dart b/packages/video_player/video_player_web/lib/src/pkg_web_tweaks.dart new file mode 100644 index 00000000000..c0ae661c96b --- /dev/null +++ b/packages/video_player/video_player_web/lib/src/pkg_web_tweaks.dart @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:js_interop'; +import 'package:web/web.dart' as web; + +/// Adds a "disablePictureInPicture" setter to [web.HTMLVideoElement]s. +extension NonStandardSettersOnVideoElement on web.HTMLVideoElement { + external set disablePictureInPicture(JSBoolean disabled); +} + +/// Adds a "disableRemotePlayback" and "controlsList" setters to [web.HTMLMediaElement]s. +extension NonStandardSettersOnMediaElement on web.HTMLMediaElement { + external set disableRemotePlayback(JSBoolean disabled); + external set controlsList(JSString? controlsList); +} 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 4adb2e1e866..012463fc780 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 @@ -3,13 +3,16 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:html' as html; +import 'dart:js_interop'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; +import 'package:web/helpers.dart'; +import 'package:web/web.dart' as web; import 'duration_utils.dart'; +import 'pkg_web_tweaks.dart'; // An error code value to error name Map. // See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code @@ -34,29 +37,29 @@ const Map _kErrorValueToErrorDescription = { const String _kDefaultErrorMessage = 'No further diagnostic information can be determined or provided.'; -/// Wraps a [html.VideoElement] so its API complies with what is expected by the plugin. +/// Wraps a [web.HTMLVideoElement] so its API complies with what is expected by the plugin. class VideoPlayer { - /// Create a [VideoPlayer] from a [html.VideoElement] instance. + /// Create a [VideoPlayer] from a [web.HTMLVideoElement] instance. VideoPlayer({ - required html.VideoElement videoElement, + required web.HTMLVideoElement videoElement, @visibleForTesting StreamController? eventController, }) : _videoElement = videoElement, _eventController = eventController ?? StreamController(); final StreamController _eventController; - final html.VideoElement _videoElement; - void Function(html.Event)? _onContextMenu; + final web.HTMLVideoElement _videoElement; + web.EventHandler? _onContextMenu; bool _isInitialized = false; bool _isBuffering = false; - /// Returns the [Stream] of [VideoEvent]s from the inner [html.VideoElement]. + /// Returns the [Stream] of [VideoEvent]s from the inner [web.HTMLVideoElement]. Stream get events => _eventController.stream; - /// Initializes the wrapped [html.VideoElement]. + /// Initializes the wrapped [web.HTMLVideoElement]. /// /// This method sets the required DOM attributes so videos can [play] programmatically, - /// and attaches listeners to the internal events from the [html.VideoElement] + /// and attaches listeners to the internal events from the [web.HTMLVideoElement] /// to react to them / expose them through the [VideoPlayer.events] stream. /// /// The [src] parameter is the URL of the video. It is passed in from the plugin @@ -71,14 +74,8 @@ class VideoPlayer { }) { _videoElement ..autoplay = false - ..controls = false; - - // Allows Safari iOS to play the video inline. - // - // This property is not exposed through dart:html so we use the - // HTML Boolean attribute form (when present with any value => true) - // See: https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML - _videoElement.setAttribute('playsinline', true); + ..controls = false + ..playsInline = true; _videoElement.onCanPlay.listen(_onVideoElementInitialization); // Needed for Safari iOS 17, which may not send `canplay`. @@ -98,12 +95,12 @@ class VideoPlayer { }); // The error event fires when some form of error occurs while attempting to load or perform the media. - _videoElement.onError.listen((html.Event _) { + _videoElement.onError.listen((web.Event _) { setBuffering(false); // The Event itself (_) doesn't contain info about the actual error. // We need to look at the HTMLMediaElement.error. // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error - final html.MediaError error = _videoElement.error!; + final web.MediaError error = _videoElement.error!; _eventController.addError(PlatformException( code: _kErrorValueToErrorName[error.code]!, message: error.message != '' ? error.message : _kDefaultErrorMessage, @@ -145,18 +142,19 @@ class VideoPlayer { /// When called from some user interaction (a tap on a button), the above /// limitation should disappear. Future play() { - return _videoElement.play().catchError((Object e) { + return _videoElement.play().toDart.catchError((Object e) { // play() attempts to begin playback of the media. It returns // a Promise which can get rejected in case of failure to begin // playback for any reason, such as permission issues. - // The rejection handler is called with a DomException. + // The rejection handler is called with a DOMException. // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play - final html.DomException exception = e as html.DomException; + final web.DOMException exception = e as web.DOMException; _eventController.addError(PlatformException( code: exception.name, message: exception.message, )); - }, test: (Object e) => e is html.DomException); + return null; + }, test: (Object e) => e is web.DOMException); } /// Pauses the video in the current position. @@ -175,7 +173,7 @@ class VideoPlayer { /// Values must fall between 0 and 1, where 0 is muted and 1 is the loudest. /// /// When volume is set to 0, the `muted` property is also applied to the - /// [html.VideoElement]. This is required for auto-play on the web. + /// [web.HTMLVideoElement]. This is required for auto-play on the web. void setVolume(double volume) { assert(volume >= 0 && volume <= 1); @@ -208,12 +206,28 @@ class VideoPlayer { void seekTo(Duration position) { assert(!position.isNegative); + // Don't seek if video is already at target position. + // + // This is needed because the core plugin will pause and seek to the end of + // the video when it finishes, and that causes an infinite loop of `ended` + // events on the web. + // + // See: https://github.com/flutter/flutter/issues/77674 + if (position == _videoElementCurrentTime) { + return; + } + _videoElement.currentTime = position.inMilliseconds.toDouble() / 1000; } /// Returns the current playback head position as a [Duration]. Duration getPosition() { _sendBufferingRangesUpdate(); + return _videoElementCurrentTime; + } + + /// Returns the currentTime of the underlying video element. + Duration get _videoElementCurrentTime { return Duration(milliseconds: (_videoElement.currentTime * 1000).round()); } @@ -226,21 +240,21 @@ class VideoPlayer { _videoElement.controls = true; final String controlsList = options.controls.controlsList; if (controlsList.isNotEmpty) { - _videoElement.setAttribute('controlsList', controlsList); + _videoElement.controlsList = controlsList.toJS; } if (!options.controls.allowPictureInPicture) { - _videoElement.setAttribute('disablePictureInPicture', true); + _videoElement.disablePictureInPicture = true.toJS; } } if (!options.allowContextMenu) { - _onContextMenu = (html.Event event) => event.preventDefault(); + _onContextMenu = ((web.Event event) => event.preventDefault()).toJS; _videoElement.addEventListener('contextmenu', _onContextMenu); } if (!options.allowRemotePlayback) { - _videoElement.setAttribute('disableRemotePlayback', true); + _videoElement.disableRemotePlayback = true.toJS; } } @@ -255,7 +269,7 @@ class VideoPlayer { _videoElement.removeAttribute('disableRemotePlayback'); } - /// Disposes of the current [html.VideoElement]. + /// Disposes of the current [web.HTMLVideoElement]. void dispose() { _videoElement.removeAttribute('src'); if (_onContextMenu != null) { @@ -316,7 +330,7 @@ class VideoPlayer { } } - // Broadcasts the [html.VideoElement.buffered] status through the [events] stream. + // Broadcasts the [web.HTMLVideoElement.buffered] status through the [events] stream. void _sendBufferingRangesUpdate() { _eventController.add(VideoEvent( buffered: _toDurationRange(_videoElement.buffered), @@ -325,7 +339,7 @@ class VideoPlayer { } // Converts from [html.TimeRanges] to our own List. - List _toDurationRange(html.TimeRanges buffered) { + List _toDurationRange(web.TimeRanges buffered) { final List durationRange = []; for (int i = 0; i < buffered.length; i++) { durationRange.add(DurationRange( diff --git a/packages/video_player/video_player_web/lib/video_player_web.dart b/packages/video_player/video_player_web/lib/video_player_web.dart index 67cf2746977..8f5c0265e96 100644 --- a/packages/video_player/video_player_web/lib/video_player_web.dart +++ b/packages/video_player/video_player_web/lib/video_player_web.dart @@ -3,12 +3,12 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:html'; import 'dart:ui_web' as ui_web; import 'package:flutter/material.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; +import 'package:web/web.dart' as web; import 'src/video_player.dart'; @@ -71,7 +71,7 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { 'web implementation of video_player cannot play content uri')); } - final VideoElement videoElement = VideoElement() + final web.HTMLVideoElement videoElement = web.HTMLVideoElement() ..id = 'videoElement-$textureId' ..style.border = 'none' ..style.height = '100%' diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index 0cb69f88b74..dd876328b01 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.2.0 +version: 2.3.0 environment: sdk: ^3.3.0 @@ -22,6 +22,7 @@ dependencies: flutter_web_plugins: sdk: flutter video_player_platform_interface: ^6.2.0 + web: ^0.5.1 dev_dependencies: flutter_test: