From 76c91781275489d6c5e482042a0e65e40580189c Mon Sep 17 00:00:00 2001 From: balvinderz Date: Thu, 4 Jan 2024 21:42:48 +0530 Subject: [PATCH 01/13] migrate to package:web' --- .../video_player_web/CHANGELOG.md | 5 ++ .../example/integration_test/utils.dart | 5 +- .../integration_test/video_player_test.dart | 34 +++++----- .../video_player_web/example/pubspec.yaml | 6 +- .../lib/src/video_player.dart | 63 +++++++++---------- .../lib/video_player_web.dart | 13 ++-- .../video_player_web/pubspec.yaml | 7 ++- 7 files changed, 71 insertions(+), 62 deletions(-) diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 5c9deb860d1..6edb81c93d3 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.1.4 + +* Migrates package and tests to 'platform:web'. +* Updates minimum supported SDK version to Flutter 3.16.0/Dart 3.2.0. + ## 2.1.3 * Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. 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 6be5b5abcc7..227abc7caee 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 @@ -5,9 +5,8 @@ @JS() library integration_test_utils; -import 'dart:html'; - import 'package:js/js.dart'; +import 'package:web/web.dart' as web; // Returns the URL to load an asset from this example app as a network source. // @@ -45,7 +44,7 @@ external void _defineProperty( /// Forces a VideoElement to report "Infinity" duration. /// /// Uses JS Object.defineProperty to set the value of a readonly property. -void setInfinityDuration(VideoElement element) { +void setInfinityDuration(web.HTMLVideoElement element) { _defineProperty( element, 'duration', 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..d10340a314f 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,13 +3,13 @@ // 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 'utils.dart'; @@ -17,11 +17,11 @@ 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.document.createElement('video') as web.HTMLVideoElement) ..controls = true ..setAttribute('playsinline', 'false'); }); @@ -145,7 +145,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 +166,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 +177,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 +187,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 +200,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 +224,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 +238,7 @@ void main() { late VideoPlayer player; setUp(() { - video = html.VideoElement(); + video = web.document.createElement('video') as web.HTMLVideoElement; player = VideoPlayer(videoElement: video)..initialize(); }); @@ -429,3 +429,7 @@ void main() { }); }); } + +extension ControlsListInVideoElement on web.HTMLVideoElement { + external web.DOMTokenList? get controlsList; +} diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml index 26bc4031ae4..eed20563368 100644 --- a/packages/video_player/video_player_web/example/pubspec.yaml +++ b/packages/video_player/video_player_web/example/pubspec.yaml @@ -2,8 +2,8 @@ name: video_player_for_web_integration_tests publish_to: none environment: - sdk: ">=3.0.0 <4.0.0" - flutter: ">=3.10.0" + sdk: ^3.2.0 + flutter: ">=3.16.0" dependencies: flutter: @@ -12,6 +12,8 @@ dependencies: video_player_platform_interface: ">=6.1.0 <7.0.0" video_player_web: path: ../ + web: '>=0.3.0 <0.5.0' + dev_dependencies: flutter_test: 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..e6a650ee968 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,11 +3,13 @@ // 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'; @@ -34,29 +36,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; + void Function(web.Event)? _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 +73,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 +94,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 +141,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 exception.jsify(); + }, test: (Object e) => e is web.DOMException); } /// Pauses the video in the current position. @@ -175,7 +172,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); @@ -230,17 +227,17 @@ class VideoPlayer { } if (!options.controls.allowPictureInPicture) { - _videoElement.setAttribute('disablePictureInPicture', true); + _videoElement.disablePictureInPicture = true; } } if (!options.allowContextMenu) { - _onContextMenu = (html.Event event) => event.preventDefault(); - _videoElement.addEventListener('contextmenu', _onContextMenu); + _onContextMenu = (web.Event event) => event.preventDefault(); + _videoElement.addEventListener('contextmenu', _onContextMenu!.toJS); } if (!options.allowRemotePlayback) { - _videoElement.setAttribute('disableRemotePlayback', true); + _videoElement.disableRemotePlayback = true; } } @@ -249,17 +246,17 @@ class VideoPlayer { _videoElement.removeAttribute('controlsList'); _videoElement.removeAttribute('disablePictureInPicture'); if (_onContextMenu != null) { - _videoElement.removeEventListener('contextmenu', _onContextMenu); + _videoElement.removeEventListener('contextmenu', _onContextMenu!.toJS); _onContextMenu = null; } _videoElement.removeAttribute('disableRemotePlayback'); } - /// Disposes of the current [html.VideoElement]. + /// Disposes of the current [web.HTMLVideoElement]. void dispose() { _videoElement.removeAttribute('src'); if (_onContextMenu != null) { - _videoElement.removeEventListener('contextmenu', _onContextMenu); + _videoElement.removeEventListener('contextmenu', _onContextMenu!.toJS); _onContextMenu = null; } _videoElement.load(); @@ -316,7 +313,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 +322,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..d1903167298 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,11 +71,12 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { 'web implementation of video_player cannot play content uri')); } - final VideoElement videoElement = VideoElement() - ..id = 'videoElement-$textureId' - ..style.border = 'none' - ..style.height = '100%' - ..style.width = '100%'; + final web.HTMLVideoElement videoElement = + (web.document.createElement('video') as web.HTMLVideoElement) + ..id = 'videoElement-$textureId' + ..style.border = 'none' + ..style.height = '100%' + ..style.width = '100%'; // TODO(hterkelsen): Use initialization parameters once they are available ui_web.platformViewRegistry.registerViewFactory( diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index 81ad0e6d1bf..b25758b81bd 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -2,11 +2,11 @@ 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.1.3 +version: 2.1.4 environment: - sdk: ">=3.1.0 <4.0.0" - flutter: ">=3.13.0" + sdk: ^3.2.0 + flutter: ">=3.16.0" flutter: plugin: @@ -22,6 +22,7 @@ dependencies: flutter_web_plugins: sdk: flutter video_player_platform_interface: ^6.2.0 + web: '>=0.3.0 <0.5.0' dev_dependencies: flutter_test: From ee17974be4d09679e5f3713e9befa348a1d546a5 Mon Sep 17 00:00:00 2001 From: balvinderz Date: Thu, 4 Jan 2024 21:57:58 +0530 Subject: [PATCH 02/13] fix tests --- .../integration_test/video_player_test.dart | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) 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 d10340a314f..d7dacabcff8 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 @@ -23,7 +23,7 @@ void main() { // Never set "src" on the video, so this test doesn't hit the network! video = (web.document.createElement('video') as web.HTMLVideoElement) ..controls = true - ..setAttribute('playsinline', 'false'); + ..playsInline = false; }); testWidgets('fixes critical video element config', (WidgetTester _) async { @@ -36,8 +36,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 { @@ -271,7 +270,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 +289,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 +308,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 +327,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 +346,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 +361,7 @@ void main() { ), ); - expect(video.getAttribute('disableRemotePlayback'), isNull); + expect(video.disableRemotePlayback, isFalse); }); testWidgets('when disabled expect attribute', @@ -373,7 +372,7 @@ void main() { ), ); - expect(video.getAttribute('disableRemotePlayback'), 'true'); + expect(video.disableRemotePlayback, isTrue); }); }); @@ -398,8 +397,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 +420,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); }); }); }); From 2a6f7bfa4b59443e7dbd29a7cd2b6acf96a57b4e Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 29 Feb 2024 18:18:21 -0800 Subject: [PATCH 03/13] Ensure web 0.5.0 everywhere. Update some syntax. --- .../integration_test/pkg_web_tweaks.dart | 51 +++++++++++++++++++ .../example/integration_test/utils.dart | 45 ++++------------ .../integration_test/video_player_test.dart | 5 +- .../lib/src/pkg_web_tweaks.dart | 21 ++++++++ .../lib/src/video_player.dart | 7 +-- .../video_player_web/pubspec.yaml | 2 +- 6 files changed, 87 insertions(+), 44 deletions(-) create mode 100644 packages/video_player/video_player_web/example/integration_test/pkg_web_tweaks.dart create mode 100644 packages/video_player/video_player_web/lib/src/pkg_web_tweaks.dart 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..8219fbe0d4f --- /dev/null +++ b/packages/video_player/video_player_web/example/integration_test/pkg_web_tweaks.dart @@ -0,0 +1,51 @@ +// 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; + +extension ControlsListInVideoElement on web.HTMLVideoElement { + external web.DOMTokenList? get controlsList; +} + +extension DisablePictureInPictureInVideoElement on web.HTMLVideoElement { + external JSBoolean get disablePictureInPicture; +} + +extension DisableRemotePlaybackInMediaElement on web.HTMLMediaElement { + external JSBoolean get disableRemotePlayback; +} + +@JS('Object') +external DomObjectConstructor get jsObjectConstructor; + +extension type DomObjectConstructor._(JSAny _) { + @JS('defineProperty') + external void _defineProperty(JSAny? object, JSString property, JSAny? value); + void defineProperty(JSObject object, String property, Object? value) { + return _defineProperty(object, property.toJS, value.jsify()); + } +} + +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 + factory Descriptor({ + bool? writable, + Object? value, + }) => + Descriptor._js( + writable: writable!.toJS, + value: value.jsify()!, + ); + external factory Descriptor._js({ + // bool configurable, + // bool 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 bdd663d54fc..963f7c5a20f 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 @@ -2,12 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@JS() -library integration_test_utils; - -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. // @@ -22,39 +18,16 @@ 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(web.HTMLVideoElement element) { - _defineProperty( - element, - 'duration', - Descriptor( - writable: true.toJS, - value: double.infinity.toJS, - )); + jsObjectConstructor.defineProperty( + element, + 'duration', + Descriptor( + writable: true, + value: double.infinity, + ), + ); } 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 d7dacabcff8..d865a6140f7 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 @@ -11,6 +11,7 @@ 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() { @@ -428,7 +429,3 @@ void main() { }); }); } - -extension ControlsListInVideoElement on web.HTMLVideoElement { - external web.DOMTokenList? get controlsList; -} 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..6999b23c550 --- /dev/null +++ b/packages/video_player/video_player_web/lib/src/pkg_web_tweaks.dart @@ -0,0 +1,21 @@ +// 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 DisablePictureSetterInPictureInVideoElement on web.HTMLVideoElement { + external set disablePictureInPicture(JSBoolean disabled); +} + +/// Adds a "disableRemotePlayback" setter to [web.HTMLMediaElement]s. +extension DisableRemotePlaybackSetterInMediaElement on web.HTMLMediaElement { + external set disableRemotePlayback(JSBoolean disabled); +} + +/// Adds a "controlsList" setter to [web.HTMLMediaElement]s. +extension ControlsListSetterInMediaElement on web.HTMLMediaElement { + 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 e6a650ee968..733195797ca 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 @@ -12,6 +12,7 @@ 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 @@ -223,11 +224,11 @@ 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.disablePictureInPicture = true; + _videoElement.disablePictureInPicture = true.toJS; } } @@ -237,7 +238,7 @@ class VideoPlayer { } if (!options.allowRemotePlayback) { - _videoElement.disableRemotePlayback = true; + _videoElement.disableRemotePlayback = true.toJS; } } diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index b4d1eaedf8c..4365a3d2fe8 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: flutter_web_plugins: sdk: flutter video_player_platform_interface: ^6.2.0 - web: '>=0.3.0 <0.5.0' + web: ^0.5.0 dev_dependencies: flutter_test: From 54fa27e9fb112a48b0974e33f74e83b8ec1ce144 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 29 Feb 2024 18:54:32 -0800 Subject: [PATCH 04/13] Address some additional PR comments. --- .../video_player_web/lib/src/video_player.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 733195797ca..d97e581455a 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 @@ -48,7 +48,7 @@ class VideoPlayer { final StreamController _eventController; final web.HTMLVideoElement _videoElement; - void Function(web.Event)? _onContextMenu; + web.EventHandler? _onContextMenu; bool _isInitialized = false; bool _isBuffering = false; @@ -153,7 +153,7 @@ class VideoPlayer { code: exception.name, message: exception.message, )); - return exception.jsify(); + return null; }, test: (Object e) => e is web.DOMException); } @@ -233,8 +233,8 @@ class VideoPlayer { } if (!options.allowContextMenu) { - _onContextMenu = (web.Event event) => event.preventDefault(); - _videoElement.addEventListener('contextmenu', _onContextMenu!.toJS); + _onContextMenu = ((web.Event event) => event.preventDefault()).toJS; + _videoElement.addEventListener('contextmenu', _onContextMenu); } if (!options.allowRemotePlayback) { @@ -247,7 +247,7 @@ class VideoPlayer { _videoElement.removeAttribute('controlsList'); _videoElement.removeAttribute('disablePictureInPicture'); if (_onContextMenu != null) { - _videoElement.removeEventListener('contextmenu', _onContextMenu!.toJS); + _videoElement.removeEventListener('contextmenu', _onContextMenu); _onContextMenu = null; } _videoElement.removeAttribute('disableRemotePlayback'); @@ -257,7 +257,7 @@ class VideoPlayer { void dispose() { _videoElement.removeAttribute('src'); if (_onContextMenu != null) { - _videoElement.removeEventListener('contextmenu', _onContextMenu!.toJS); + _videoElement.removeEventListener('contextmenu', _onContextMenu); _onContextMenu = null; } _videoElement.load(); From dcfdd7f993e30b014aea7cb1be1aa89f2beb292d Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 4 Mar 2024 16:16:13 -0800 Subject: [PATCH 05/13] Comment some public integration_test utils. --- .../example/integration_test/pkg_web_tweaks.dart | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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 index 8219fbe0d4f..e0f823e3a8d 100644 --- 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 @@ -8,32 +8,41 @@ library video_player_web_integration_test_pkg_web_tweaks; import 'dart:js_interop'; import 'package:web/web.dart' as web; +/// Adds a `controlsList` getter to `HTMLVideoElement`s. extension ControlsListInVideoElement on web.HTMLVideoElement { external web.DOMTokenList? get controlsList; } +/// Adds a `disablePictureInPicture` getter to `HTMLVideoElement`s. extension DisablePictureInPictureInVideoElement on web.HTMLVideoElement { external JSBoolean get disablePictureInPicture; } +/// Adds a `disableRemotePlayback` getter to `HTMLMediaElement`s. extension DisableRemotePlaybackInMediaElement on web.HTMLMediaElement { external JSBoolean get disableRemotePlayback; } +/// Retrieves the `Object` constructor from the DOM. @JS('Object') external DomObjectConstructor get jsObjectConstructor; +/// Defines the JS interop we need from the `Object` constructor. extension type DomObjectConstructor._(JSAny _) { @JS('defineProperty') external void _defineProperty(JSAny? object, JSString property, JSAny? value); + + /// `Object.defineProperty`. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty void defineProperty(JSObject object, String property, Object? value) { return _defineProperty(object, property.toJS, value.jsify()); } } +/// The bag of properties that can be set by `defineProperty`. 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 + /// Constructs a bag of properties to be defined on a target object with `defineProperty`. factory Descriptor({ bool? writable, Object? value, @@ -42,6 +51,8 @@ extension type Descriptor._(JSObject _) implements JSObject { writable: writable!.toJS, value: value.jsify()!, ); + // 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._js({ // bool configurable, // bool enumerable, From 66fac4b7ed2cc4980268c56c06fb7d383f271269 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 4 Mar 2024 16:22:49 -0800 Subject: [PATCH 06/13] Incorporate #5920 --- .../video_player_web/CHANGELOG.md | 1 + .../example/integration_test/utils.dart | 10 +++++++ .../integration_test/video_player_test.dart | 29 +++++++++++++++---- .../lib/src/video_player.dart | 16 ++++++++++ 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 0d628c63b21..69c91ca0bf5 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,6 +1,7 @@ ## 2.3.0 * Migrates package and tests to 'platform:web'. +* Fixes infinite event loop caused by `seekTo` when the video ends. ## 2.2.0 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 963f7c5a20f..319def8a2a8 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 @@ -31,3 +31,13 @@ void setInfinityDuration(web.HTMLVideoElement element) { ), ); } + +/// Modifies a HTMLVideoElement to throw an exception when setting `currentTime`. +extension type ThrowyVideoElement(web.HTMLVideoElement _) + implements web.HTMLVideoElement { + set currentTime(num value) { + throw Exception('Unexpected call to currentTime with value: $value'); + } + + int get currentTime => 100; +} 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 d865a6140f7..56a463e2537 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 @@ -69,12 +69,31 @@ 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 { + final ThrowyVideoElement throwy = ThrowyVideoElement(video); + final VideoPlayer player = VideoPlayer(videoElement: throwy) + ..initialize(); + + expect(() { + // Self-test... + throwy.currentTime = 123; + }, throwsException, reason: 'The throwy implementation must throw!'); + + expect(() { + player.seekTo(Duration(milliseconds: throwy.currentTime)); + }, returnsNormally); + }); }); // The events tested in this group do *not* represent the actual sequence 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 d97e581455a..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 @@ -206,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()); } From 0df1bc792abb481a150fd4785df5b84359f4e37e Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 4 Mar 2024 16:24:58 -0800 Subject: [PATCH 07/13] Move all js-interop code together. --- .../example/integration_test/pkg_web_tweaks.dart | 12 ++++++++++++ .../example/integration_test/utils.dart | 10 ---------- 2 files changed, 12 insertions(+), 10 deletions(-) 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 index e0f823e3a8d..a958209a743 100644 --- 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 @@ -60,3 +60,15 @@ extension type Descriptor._(JSObject _) implements JSObject { JSAny value, }); } + +/// Modifies a HTMLVideoElement to throw an exception when setting `currentTime`. +/// +/// Test-only! +extension type ThrowyVideoElement(web.HTMLVideoElement _) + implements web.HTMLVideoElement { + set currentTime(num value) { + throw Exception('Unexpected call to currentTime with value: $value'); + } + + int get currentTime => 100; +} 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 319def8a2a8..963f7c5a20f 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 @@ -31,13 +31,3 @@ void setInfinityDuration(web.HTMLVideoElement element) { ), ); } - -/// Modifies a HTMLVideoElement to throw an exception when setting `currentTime`. -extension type ThrowyVideoElement(web.HTMLVideoElement _) - implements web.HTMLVideoElement { - set currentTime(num value) { - throw Exception('Unexpected call to currentTime with value: $value'); - } - - int get currentTime => 100; -} From 46de6a4179b712e1c53308469efe4a67d875a13f Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 5 Mar 2024 12:29:45 -0800 Subject: [PATCH 08/13] Fix version in pubspec --- packages/video_player/video_player_web/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index 4365a3d2fe8..781a69c4b30 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 From c898c83c6e5aa6bde3079dc512e75bccf9b31c4b Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 5 Mar 2024 14:58:18 -0800 Subject: [PATCH 09/13] Use HTMLElement constructors from web 0.5.1 --- packages/video_player/video_player_web/CHANGELOG.md | 2 +- .../example/integration_test/video_player_test.dart | 4 ++-- .../video_player/video_player_web/lib/video_player_web.dart | 3 +-- packages/video_player/video_player_web/pubspec.yaml | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 69c91ca0bf5..cac6a397996 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,6 +1,6 @@ ## 2.3.0 -* Migrates package and tests to 'platform:web'. +* Migrates package and tests to `package:web``. * Fixes infinite event loop caused by `seekTo` when the video ends. ## 2.2.0 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 56a463e2537..c26d7416d60 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 @@ -22,7 +22,7 @@ void main() { setUp(() { // Never set "src" on the video, so this test doesn't hit the network! - video = (web.document.createElement('video') as web.HTMLVideoElement) + video = web.HTMLVideoElement() ..controls = true ..playsInline = false; }); @@ -257,7 +257,7 @@ void main() { late VideoPlayer player; setUp(() { - video = web.document.createElement('video') as web.HTMLVideoElement; + video = web.HTMLVideoElement(); player = VideoPlayer(videoElement: video)..initialize(); }); 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 d1903167298..7d896a417ae 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 @@ -71,8 +71,7 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { 'web implementation of video_player cannot play content uri')); } - final web.HTMLVideoElement videoElement = - (web.document.createElement('video') as web.HTMLVideoElement) + 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 781a69c4b30..dd876328b01 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: flutter_web_plugins: sdk: flutter video_player_platform_interface: ^6.2.0 - web: ^0.5.0 + web: ^0.5.1 dev_dependencies: flutter_test: From 66a9db2e5c2f4abcfa08bc0059943bcedf40bbe9 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 5 Mar 2024 17:11:30 -0800 Subject: [PATCH 10/13] Format --- .../video_player_web/lib/video_player_web.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 7d896a417ae..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 @@ -72,10 +72,10 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { } final web.HTMLVideoElement videoElement = web.HTMLVideoElement() - ..id = 'videoElement-$textureId' - ..style.border = 'none' - ..style.height = '100%' - ..style.width = '100%'; + ..id = 'videoElement-$textureId' + ..style.border = 'none' + ..style.height = '100%' + ..style.width = '100%'; // TODO(hterkelsen): Use initialization parameters once they are available ui_web.platformViewRegistry.registerViewFactory( From 9b022580c9c726e92b1437e98ea1abd005c697b1 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 5 Mar 2024 17:12:42 -0800 Subject: [PATCH 11/13] Improves JS-interop for defineProperty. * Adds factories for separate data/accessor Descriptors * Makes defineProperty a static method of 'Object', so it mirrors the JS API * Improves the `seekTo` test so it really throws from within the video object (throwy was not enough). --- .../integration_test/pkg_web_tweaks.dart | 71 ++++++++++--------- .../example/integration_test/utils.dart | 21 +++++- .../integration_test/video_player_test.dart | 11 +-- 3 files changed, 62 insertions(+), 41 deletions(-) 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 index a958209a743..a3fe5202f63 100644 --- 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 @@ -23,52 +23,57 @@ extension DisableRemotePlaybackInMediaElement on web.HTMLMediaElement { external JSBoolean get disableRemotePlayback; } -/// Retrieves the `Object` constructor from the DOM. +/// Defines JS interop to access static methods from `Object`. @JS('Object') -external DomObjectConstructor get jsObjectConstructor; - -/// Defines the JS interop we need from the `Object` constructor. -extension type DomObjectConstructor._(JSAny _) { +extension type DomObject._(JSAny _) { @JS('defineProperty') - external void _defineProperty(JSAny? object, JSString property, JSAny? value); + 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 - void defineProperty(JSObject object, String property, Object? value) { - return _defineProperty(object, property.toJS, value.jsify()); + static void defineProperty( + JSObject object, String property, Descriptor descriptor) { + return _defineProperty(object, property.toJS, descriptor); } } -/// The bag of properties that can be set by `defineProperty`. +/// 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 { - /// Constructs a bag of properties to be defined on a target object with `defineProperty`. - factory Descriptor({ + /// Builds a "data descriptor". + factory Descriptor.data({ bool? writable, - Object? value, + JSAny? value, }) => - Descriptor._js( - writable: writable!.toJS, - value: value.jsify()!, + Descriptor._data( + writable: writable?.toJS, + value: value.jsify(), ); - // 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._js({ - // bool configurable, - // bool enumerable, - JSBoolean? writable, - JSAny value, - }); -} -/// Modifies a HTMLVideoElement to throw an exception when setting `currentTime`. -/// -/// Test-only! -extension type ThrowyVideoElement(web.HTMLVideoElement _) - implements web.HTMLVideoElement { - set currentTime(num value) { - throw Exception('Unexpected call to currentTime with value: $value'); - } + /// 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, + }); - int get currentTime => 100; + 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 963f7c5a20f..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 @@ -2,6 +2,8 @@ // 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; import 'pkg_web_tweaks.dart'; @@ -22,12 +24,25 @@ String getUrlForAssetAsNetworkSource(String assetKey) { /// /// Uses JS Object.defineProperty to set the value of a readonly property. void setInfinityDuration(web.HTMLVideoElement element) { - jsObjectConstructor.defineProperty( + DomObject.defineProperty( element, 'duration', - Descriptor( + Descriptor.data( writable: true, - value: double.infinity, + 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 c26d7416d60..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 @@ -81,17 +81,18 @@ void main() { testWidgets('setting currentTime to its current value - noop', (WidgetTester tester) async { - final ThrowyVideoElement throwy = ThrowyVideoElement(video); - final VideoPlayer player = VideoPlayer(videoElement: throwy) + makeSetCurrentTimeThrow(video); + final VideoPlayer player = VideoPlayer(videoElement: video) ..initialize(); expect(() { // Self-test... - throwy.currentTime = 123; - }, throwsException, reason: 'The throwy implementation must throw!'); + video.currentTime = 123; + }, throwsException, reason: 'Setting currentTime must throw!'); expect(() { - player.seekTo(Duration(milliseconds: throwy.currentTime)); + // Should not set currentTime (and throw) when seekTo current time. + player.seekTo(Duration(seconds: video.currentTime.toInt())); }, returnsNormally); }); }); From 06fab2e20ea24ecde045ed401aea177bdcbce18b Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 5 Mar 2024 17:24:47 -0800 Subject: [PATCH 12/13] Consolidated extension types. --- .../example/integration_test/pkg_web_tweaks.dart | 12 ++++-------- .../video_player_web/lib/src/pkg_web_tweaks.dart | 10 +++------- 2 files changed, 7 insertions(+), 15 deletions(-) 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 index a3fe5202f63..ccf5b32e5c6 100644 --- 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 @@ -8,18 +8,14 @@ library video_player_web_integration_test_pkg_web_tweaks; import 'dart:js_interop'; import 'package:web/web.dart' as web; -/// Adds a `controlsList` getter to `HTMLVideoElement`s. -extension ControlsListInVideoElement on web.HTMLVideoElement { +/// Adds a `controlsList` and `disablePictureInPicture` getters. +extension NonStandardGettersOnVideoElement on web.HTMLVideoElement { external web.DOMTokenList? get controlsList; -} - -/// Adds a `disablePictureInPicture` getter to `HTMLVideoElement`s. -extension DisablePictureInPictureInVideoElement on web.HTMLVideoElement { external JSBoolean get disablePictureInPicture; } -/// Adds a `disableRemotePlayback` getter to `HTMLMediaElement`s. -extension DisableRemotePlaybackInMediaElement on web.HTMLMediaElement { +/// Adds a `disableRemotePlayback` getter. +extension NonStandardGettersOnMediaElement on web.HTMLMediaElement { external JSBoolean get disableRemotePlayback; } 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 index 6999b23c550..c0ae661c96b 100644 --- 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 @@ -6,16 +6,12 @@ import 'dart:js_interop'; import 'package:web/web.dart' as web; /// Adds a "disablePictureInPicture" setter to [web.HTMLVideoElement]s. -extension DisablePictureSetterInPictureInVideoElement on web.HTMLVideoElement { +extension NonStandardSettersOnVideoElement on web.HTMLVideoElement { external set disablePictureInPicture(JSBoolean disabled); } -/// Adds a "disableRemotePlayback" setter to [web.HTMLMediaElement]s. -extension DisableRemotePlaybackSetterInMediaElement on web.HTMLMediaElement { +/// Adds a "disableRemotePlayback" and "controlsList" setters to [web.HTMLMediaElement]s. +extension NonStandardSettersOnMediaElement on web.HTMLMediaElement { external set disableRemotePlayback(JSBoolean disabled); -} - -/// Adds a "controlsList" setter to [web.HTMLMediaElement]s. -extension ControlsListSetterInMediaElement on web.HTMLMediaElement { external set controlsList(JSString? controlsList); } From 2ba0a9c03540dd76117dba41edaf2e2eb3a12d9d Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 5 Mar 2024 17:25:56 -0800 Subject: [PATCH 13/13] Use web 0.5.1 for test app too. --- packages/video_player/video_player_web/example/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: