Skip to content

Commit b097d99

Browse files
balvinderzToddZeil
andauthored
[video_player_web] migrates to package:web (flutter#5800)
Updates the web implementation of `video_player_web` to `package:web`. Also: prevents an infinite event loop when seeking to the end of a video after it's done. ### Issues * Fixes: flutter#139752 Co-authored-by: ToddZeil <[email protected]>
1 parent 79faa24 commit b097d99

File tree

9 files changed

+224
-102
lines changed

9 files changed

+224
-102
lines changed

packages/video_player/video_player_web/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.3.0
2+
3+
* Migrates package and tests to `package:web``.
4+
* Fixes infinite event loop caused by `seekTo` when the video ends.
5+
16
## 2.2.0
27

38
* Updates SDK version to Dart `^3.3.0`. Flutter `^3.19.0`.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
@JS()
6+
library video_player_web_integration_test_pkg_web_tweaks;
7+
8+
import 'dart:js_interop';
9+
import 'package:web/web.dart' as web;
10+
11+
/// Adds a `controlsList` and `disablePictureInPicture` getters.
12+
extension NonStandardGettersOnVideoElement on web.HTMLVideoElement {
13+
external web.DOMTokenList? get controlsList;
14+
external JSBoolean get disablePictureInPicture;
15+
}
16+
17+
/// Adds a `disableRemotePlayback` getter.
18+
extension NonStandardGettersOnMediaElement on web.HTMLMediaElement {
19+
external JSBoolean get disableRemotePlayback;
20+
}
21+
22+
/// Defines JS interop to access static methods from `Object`.
23+
@JS('Object')
24+
extension type DomObject._(JSAny _) {
25+
@JS('defineProperty')
26+
external static void _defineProperty(
27+
JSAny? object, JSString property, Descriptor value);
28+
29+
/// `Object.defineProperty`.
30+
///
31+
/// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
32+
static void defineProperty(
33+
JSObject object, String property, Descriptor descriptor) {
34+
return _defineProperty(object, property.toJS, descriptor);
35+
}
36+
}
37+
38+
/// The descriptor for the property being defined or modified with `defineProperty`.
39+
///
40+
/// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description
41+
extension type Descriptor._(JSObject _) implements JSObject {
42+
/// Builds a "data descriptor".
43+
factory Descriptor.data({
44+
bool? writable,
45+
JSAny? value,
46+
}) =>
47+
Descriptor._data(
48+
writable: writable?.toJS,
49+
value: value.jsify(),
50+
);
51+
52+
/// Builds an "accessor descriptor".
53+
factory Descriptor.accessor({
54+
void Function(JSAny? value)? set,
55+
JSAny? Function()? get,
56+
}) =>
57+
Descriptor._accessor(
58+
set: set?.toJS,
59+
get: get?.toJS,
60+
);
61+
62+
external factory Descriptor._accessor({
63+
// JSBoolean configurable,
64+
// JSBoolean enumerable,
65+
JSFunction? set,
66+
JSFunction? get,
67+
});
68+
69+
external factory Descriptor._data({
70+
// JSBoolean configurable,
71+
// JSBoolean enumerable,
72+
JSBoolean? writable,
73+
JSAny? value,
74+
});
75+
}

packages/video_player/video_player_web/example/integration_test/utils.dart

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
// found in the LICENSE file.
44

55
import 'dart:js_interop';
6-
import 'dart:js_interop_unsafe';
6+
77
import 'package:web/web.dart' as web;
8+
import 'pkg_web_tweaks.dart';
89

910
// Returns the URL to load an asset from this example app as a network source.
1011
//
@@ -19,40 +20,29 @@ String getUrlForAssetAsNetworkSource(String assetKey) {
1920
'?raw=true';
2021
}
2122

22-
extension type Descriptor._(JSObject _) implements JSObject {
23-
// May also contain "configurable" and "enumerable" bools.
24-
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description
25-
external factory Descriptor({
26-
// bool configurable,
27-
// bool enumerable,
28-
JSBoolean writable,
29-
JSAny value,
30-
});
31-
}
32-
33-
void _defineProperty(
34-
Object object,
35-
String property,
36-
Descriptor description,
37-
) {
38-
(globalContext['Object'] as JSObject?)?.callMethod(
39-
'defineProperty'.toJS,
40-
object as JSObject,
41-
property.toJS,
42-
description,
43-
);
44-
}
45-
4623
/// Forces a VideoElement to report "Infinity" duration.
4724
///
4825
/// Uses JS Object.defineProperty to set the value of a readonly property.
49-
void setInfinityDuration(Object videoElement) {
50-
assert(videoElement is web.HTMLVideoElement);
51-
_defineProperty(
52-
videoElement,
53-
'duration',
54-
Descriptor(
55-
writable: true.toJS,
56-
value: double.infinity.toJS,
26+
void setInfinityDuration(web.HTMLVideoElement element) {
27+
DomObject.defineProperty(
28+
element,
29+
'duration',
30+
Descriptor.data(
31+
writable: true,
32+
value: double.infinity.toJS,
33+
),
34+
);
35+
}
36+
37+
/// Makes the `currentTime` setter throw an exception if used.
38+
void makeSetCurrentTimeThrow(web.HTMLVideoElement element) {
39+
DomObject.defineProperty(
40+
element,
41+
'currentTime',
42+
Descriptor.accessor(
43+
set: (JSAny? value) {
44+
throw Exception('Unexpected call to currentTime with value: $value');
45+
},
46+
get: () => 100.toJS,
5747
));
5848
}

packages/video_player/video_player_web/example/integration_test/video_player_test.dart

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,28 @@
33
// found in the LICENSE file.
44

55
import 'dart:async';
6-
import 'dart:html' as html;
76

87
import 'package:flutter_test/flutter_test.dart';
98
import 'package:integration_test/integration_test.dart';
109
import 'package:video_player_platform_interface/video_player_platform_interface.dart';
1110
import 'package:video_player_web/src/duration_utils.dart';
1211
import 'package:video_player_web/src/video_player.dart';
12+
import 'package:web/web.dart' as web;
1313

14+
import 'pkg_web_tweaks.dart';
1415
import 'utils.dart';
1516

1617
void main() {
1718
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
1819

1920
group('VideoPlayer', () {
20-
late html.VideoElement video;
21+
late web.HTMLVideoElement video;
2122

2223
setUp(() {
2324
// Never set "src" on the video, so this test doesn't hit the network!
24-
video = html.VideoElement()
25+
video = web.HTMLVideoElement()
2526
..controls = true
26-
..setAttribute('playsinline', 'false');
27+
..playsInline = false;
2728
});
2829

2930
testWidgets('fixes critical video element config', (WidgetTester _) async {
@@ -36,8 +37,7 @@ void main() {
3637
// see: https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML
3738
expect(video.getAttribute('autoplay'), isNull,
3839
reason: 'autoplay attribute on video tag must NOT be set');
39-
expect(video.getAttribute('playsinline'), 'true',
40-
reason: 'Needed by safari iOS');
40+
expect(video.playsInline, true, reason: 'Needed by safari iOS');
4141
});
4242

4343
testWidgets('setVolume', (WidgetTester tester) async {
@@ -69,12 +69,32 @@ void main() {
6969
}, throwsAssertionError, reason: 'Playback speed cannot be == 0');
7070
});
7171

72-
testWidgets('seekTo', (WidgetTester tester) async {
73-
final VideoPlayer player = VideoPlayer(videoElement: video)..initialize();
72+
group('seekTo', () {
73+
testWidgets('negative time - throws assert', (WidgetTester tester) async {
74+
final VideoPlayer player = VideoPlayer(videoElement: video)
75+
..initialize();
7476

75-
expect(() {
76-
player.seekTo(const Duration(seconds: -1));
77-
}, throwsAssertionError, reason: 'Cannot seek into negative numbers');
77+
expect(() {
78+
player.seekTo(const Duration(seconds: -1));
79+
}, throwsAssertionError, reason: 'Cannot seek into negative numbers');
80+
});
81+
82+
testWidgets('setting currentTime to its current value - noop',
83+
(WidgetTester tester) async {
84+
makeSetCurrentTimeThrow(video);
85+
final VideoPlayer player = VideoPlayer(videoElement: video)
86+
..initialize();
87+
88+
expect(() {
89+
// Self-test...
90+
video.currentTime = 123;
91+
}, throwsException, reason: 'Setting currentTime must throw!');
92+
93+
expect(() {
94+
// Should not set currentTime (and throw) when seekTo current time.
95+
player.seekTo(Duration(seconds: video.currentTime.toInt()));
96+
}, returnsNormally);
97+
});
7898
});
7999

80100
// The events tested in this group do *not* represent the actual sequence
@@ -145,7 +165,7 @@ void main() {
145165
player.setBuffering(true);
146166

147167
// Simulate "canplay" event...
148-
video.dispatchEvent(html.Event('canplay'));
168+
video.dispatchEvent(web.Event('canplay'));
149169

150170
final List<bool> events = await stream;
151171

@@ -166,7 +186,7 @@ void main() {
166186
player.setBuffering(true);
167187

168188
// Simulate "canplaythrough" event...
169-
video.dispatchEvent(html.Event('canplaythrough'));
189+
video.dispatchEvent(web.Event('canplaythrough'));
170190

171191
final List<bool> events = await stream;
172192

@@ -177,19 +197,19 @@ void main() {
177197
testWidgets('initialized dispatches only once',
178198
(WidgetTester tester) async {
179199
// Dispatch some bogus "canplay" events from the video object
180-
video.dispatchEvent(html.Event('canplay'));
181-
video.dispatchEvent(html.Event('canplay'));
182-
video.dispatchEvent(html.Event('canplay'));
200+
video.dispatchEvent(web.Event('canplay'));
201+
video.dispatchEvent(web.Event('canplay'));
202+
video.dispatchEvent(web.Event('canplay'));
183203

184204
// Take all the "initialized" events that we see during the next few seconds
185205
final Future<List<VideoEvent>> stream = timedStream
186206
.where((VideoEvent event) =>
187207
event.eventType == VideoEventType.initialized)
188208
.toList();
189209

190-
video.dispatchEvent(html.Event('canplay'));
191-
video.dispatchEvent(html.Event('canplay'));
192-
video.dispatchEvent(html.Event('canplay'));
210+
video.dispatchEvent(web.Event('canplay'));
211+
video.dispatchEvent(web.Event('canplay'));
212+
video.dispatchEvent(web.Event('canplay'));
193213

194214
final List<VideoEvent> events = await stream;
195215

@@ -200,8 +220,8 @@ void main() {
200220
// Issue: https://github.com/flutter/flutter/issues/137023
201221
testWidgets('loadedmetadata dispatches initialized',
202222
(WidgetTester tester) async {
203-
video.dispatchEvent(html.Event('loadedmetadata'));
204-
video.dispatchEvent(html.Event('loadedmetadata'));
223+
video.dispatchEvent(web.Event('loadedmetadata'));
224+
video.dispatchEvent(web.Event('loadedmetadata'));
205225

206226
final Future<List<VideoEvent>> stream = timedStream
207227
.where((VideoEvent event) =>
@@ -224,7 +244,7 @@ void main() {
224244
event.eventType == VideoEventType.initialized)
225245
.toList();
226246

227-
video.dispatchEvent(html.Event('canplay'));
247+
video.dispatchEvent(web.Event('canplay'));
228248

229249
final List<VideoEvent> events = await stream;
230250

@@ -238,7 +258,7 @@ void main() {
238258
late VideoPlayer player;
239259

240260
setUp(() {
241-
video = html.VideoElement();
261+
video = web.HTMLVideoElement();
242262
player = VideoPlayer(videoElement: video)..initialize();
243263
});
244264

@@ -271,7 +291,7 @@ void main() {
271291
expect(video.controlsList?.contains('nodownload'), isFalse);
272292
expect(video.controlsList?.contains('nofullscreen'), isFalse);
273293
expect(video.controlsList?.contains('noplaybackrate'), isFalse);
274-
expect(video.getAttribute('disablePictureInPicture'), isNull);
294+
expect(video.disablePictureInPicture, isFalse);
275295
});
276296

277297
testWidgets('and no download expect correct controls',
@@ -290,7 +310,7 @@ void main() {
290310
expect(video.controlsList?.contains('nodownload'), isTrue);
291311
expect(video.controlsList?.contains('nofullscreen'), isFalse);
292312
expect(video.controlsList?.contains('noplaybackrate'), isFalse);
293-
expect(video.getAttribute('disablePictureInPicture'), isNull);
313+
expect(video.disablePictureInPicture, isFalse);
294314
});
295315

296316
testWidgets('and no fullscreen expect correct controls',
@@ -309,7 +329,7 @@ void main() {
309329
expect(video.controlsList?.contains('nodownload'), isFalse);
310330
expect(video.controlsList?.contains('nofullscreen'), isTrue);
311331
expect(video.controlsList?.contains('noplaybackrate'), isFalse);
312-
expect(video.getAttribute('disablePictureInPicture'), isNull);
332+
expect(video.disablePictureInPicture, isFalse);
313333
});
314334

315335
testWidgets('and no playback rate expect correct controls',
@@ -328,7 +348,7 @@ void main() {
328348
expect(video.controlsList?.contains('nodownload'), isFalse);
329349
expect(video.controlsList?.contains('nofullscreen'), isFalse);
330350
expect(video.controlsList?.contains('noplaybackrate'), isTrue);
331-
expect(video.getAttribute('disablePictureInPicture'), isNull);
351+
expect(video.disablePictureInPicture, isFalse);
332352
});
333353

334354
testWidgets('and no picture in picture expect correct controls',
@@ -347,7 +367,7 @@ void main() {
347367
expect(video.controlsList?.contains('nodownload'), isFalse);
348368
expect(video.controlsList?.contains('nofullscreen'), isFalse);
349369
expect(video.controlsList?.contains('noplaybackrate'), isFalse);
350-
expect(video.getAttribute('disablePictureInPicture'), 'true');
370+
expect(video.disablePictureInPicture, isTrue);
351371
});
352372
});
353373
});
@@ -362,7 +382,7 @@ void main() {
362382
),
363383
);
364384

365-
expect(video.getAttribute('disableRemotePlayback'), isNull);
385+
expect(video.disableRemotePlayback, isFalse);
366386
});
367387

368388
testWidgets('when disabled expect attribute',
@@ -373,7 +393,7 @@ void main() {
373393
),
374394
);
375395

376-
expect(video.getAttribute('disableRemotePlayback'), 'true');
396+
expect(video.disableRemotePlayback, isTrue);
377397
});
378398
});
379399

@@ -398,8 +418,8 @@ void main() {
398418
expect(video.controlsList?.contains('nodownload'), isTrue);
399419
expect(video.controlsList?.contains('nofullscreen'), isTrue);
400420
expect(video.controlsList?.contains('noplaybackrate'), isTrue);
401-
expect(video.getAttribute('disablePictureInPicture'), 'true');
402-
expect(video.getAttribute('disableRemotePlayback'), 'true');
421+
expect(video.disablePictureInPicture, isTrue);
422+
expect(video.disableRemotePlayback, isTrue);
403423
});
404424

405425
group('when called once more', () {
@@ -421,8 +441,8 @@ void main() {
421441
expect(video.controlsList?.contains('nodownload'), isFalse);
422442
expect(video.controlsList?.contains('nofullscreen'), isFalse);
423443
expect(video.controlsList?.contains('noplaybackrate'), isFalse);
424-
expect(video.getAttribute('disablePictureInPicture'), isNull);
425-
expect(video.getAttribute('disableRemotePlayback'), isNull);
444+
expect(video.disablePictureInPicture, isFalse);
445+
expect(video.disableRemotePlayback, isFalse);
426446
});
427447
});
428448
});

0 commit comments

Comments
 (0)