Skip to content

Commit

Permalink
[video_player_web] migrates to package:web (flutter#5800)
Browse files Browse the repository at this point in the history
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 <120418414+ToddZeil@users.noreply.github.com>
  • Loading branch information
balvinderz and ToddZeil committed Mar 6, 2024
1 parent 79faa24 commit b097d99
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 102 deletions.
5 changes: 5 additions & 0 deletions packages/video_player/video_player_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand All @@ -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,
));
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -145,7 +165,7 @@ void main() {
player.setBuffering(true);

// Simulate "canplay" event...
video.dispatchEvent(html.Event('canplay'));
video.dispatchEvent(web.Event('canplay'));

final List<bool> events = await stream;

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

// Simulate "canplaythrough" event...
video.dispatchEvent(html.Event('canplaythrough'));
video.dispatchEvent(web.Event('canplaythrough'));

final List<bool> events = await stream;

Expand All @@ -177,19 +197,19 @@ 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<List<VideoEvent>> stream = timedStream
.where((VideoEvent event) =>
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<VideoEvent> events = await stream;

Expand All @@ -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<List<VideoEvent>> stream = timedStream
.where((VideoEvent event) =>
Expand All @@ -224,7 +244,7 @@ void main() {
event.eventType == VideoEventType.initialized)
.toList();

video.dispatchEvent(html.Event('canplay'));
video.dispatchEvent(web.Event('canplay'));

final List<VideoEvent> events = await stream;

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

setUp(() {
video = html.VideoElement();
video = web.HTMLVideoElement();
player = VideoPlayer(videoElement: video)..initialize();
});

Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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);
});
});
});
Expand All @@ -362,7 +382,7 @@ void main() {
),
);

expect(video.getAttribute('disableRemotePlayback'), isNull);
expect(video.disableRemotePlayback, isFalse);
});

testWidgets('when disabled expect attribute',
Expand All @@ -373,7 +393,7 @@ void main() {
),
);

expect(video.getAttribute('disableRemotePlayback'), 'true');
expect(video.disableRemotePlayback, isTrue);
});
});

Expand All @@ -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', () {
Expand All @@ -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);
});
});
});
Expand Down
Loading

0 comments on commit b097d99

Please sign in to comment.