[video_player_web] Improve handling of "Infinite" videos. (#6101)
diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md
index e36d044..603f296 100644
--- a/packages/video_player/video_player_web/CHANGELOG.md
+++ b/packages/video_player/video_player_web/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.0.11
+
+* Improves handling of videos with `Infinity` duration.
+
## 2.0.10
* Minor fixes for new analysis options.
diff --git a/packages/video_player/video_player_web/example/integration_test/duration_utils_test.dart b/packages/video_player/video_player_web/example/integration_test/duration_utils_test.dart
new file mode 100644
index 0000000..c0d6398
--- /dev/null
+++ b/packages/video_player/video_player_web/example/integration_test/duration_utils_test.dart
@@ -0,0 +1,52 @@
+// 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 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:video_player_web/src/duration_utils.dart';
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ group('convertNumVideoDurationToPluginDuration', () {
+ testWidgets('Finite value converts to milliseconds',
+ (WidgetTester _) async {
+ final Duration? result = convertNumVideoDurationToPluginDuration(1.5);
+ final Duration? zero = convertNumVideoDurationToPluginDuration(0.0001);
+
+ expect(result, isNotNull);
+ expect(result!.inMilliseconds, equals(1500));
+ expect(zero, equals(Duration.zero));
+ });
+
+ testWidgets('Finite value rounds 3rd decimal value',
+ (WidgetTester _) async {
+ final Duration? result =
+ convertNumVideoDurationToPluginDuration(1.567899089087);
+ final Duration? another =
+ convertNumVideoDurationToPluginDuration(1.567199089087);
+
+ expect(result, isNotNull);
+ expect(result!.inMilliseconds, equals(1568));
+ expect(another!.inMilliseconds, equals(1567));
+ });
+
+ testWidgets('Infinite value returns magic constant',
+ (WidgetTester _) async {
+ final Duration? result =
+ convertNumVideoDurationToPluginDuration(double.infinity);
+
+ expect(result, isNotNull);
+ expect(result, equals(jsCompatibleTimeUnset));
+ expect(result!.inMilliseconds, equals(-9007199254740990));
+ });
+
+ testWidgets('NaN value returns null', (WidgetTester _) async {
+ final Duration? result =
+ convertNumVideoDurationToPluginDuration(double.nan);
+
+ expect(result, isNull);
+ });
+ });
+}
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 b011851..2bb234e 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,13 @@
// 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:html';
+
+import 'package:js/js.dart';
+
// Returns the URL to load an asset from this example app as a network source.
//
// TODO(stuartmorgan): Convert this to a local `HttpServer` that vends the
@@ -14,3 +21,36 @@
'$assetKey'
'?raw=true';
}
+
+@JS()
+@anonymous
+class _Descriptor {
+ // 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,
+ bool writable,
+ Object value,
+ });
+}
+
+@JS('Object.defineProperty')
+external void _defineProperty(
+ Object object,
+ String property,
+ _Descriptor description,
+);
+
+/// Forces a VideoElement to report "Infinity" duration.
+///
+/// Uses JS Object.defineProperty to set the value of a readonly property.
+void setInfinityDuration(VideoElement element) {
+ _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 41aba97..28046f4 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
@@ -8,8 +8,11 @@
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 'utils.dart';
+
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -190,6 +193,25 @@
expect(events, hasLength(1));
expect(events[0].eventType, VideoEventType.initialized);
});
+
+ // Issue: https://github.com/flutter/flutter/issues/105649
+ testWidgets('supports `Infinity` duration', (WidgetTester _) async {
+ setInfinityDuration(video);
+ expect(video.duration.isInfinite, isTrue);
+
+ final Future<List<VideoEvent>> stream = timedStream
+ .where((VideoEvent event) =>
+ event.eventType == VideoEventType.initialized)
+ .toList();
+
+ video.dispatchEvent(html.Event('canplay'));
+
+ final List<VideoEvent> events = await stream;
+
+ expect(events, hasLength(1));
+ expect(events[0].eventType, VideoEventType.initialized);
+ expect(events[0].duration, equals(jsCompatibleTimeUnset));
+ });
});
});
}
diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml
index 6fb2cd0..abd299a 100644
--- a/packages/video_player/video_player_web/example/pubspec.yaml
+++ b/packages/video_player/video_player_web/example/pubspec.yaml
@@ -8,6 +8,7 @@
dependencies:
flutter:
sdk: flutter
+ js: ^0.6.0
video_player_web:
path: ../
diff --git a/packages/video_player/video_player_web/lib/src/duration_utils.dart b/packages/video_player/video_player_web/lib/src/duration_utils.dart
new file mode 100644
index 0000000..030d6b0
--- /dev/null
+++ b/packages/video_player/video_player_web/lib/src/duration_utils.dart
@@ -0,0 +1,33 @@
+// 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.
+
+/// The "length" of a video which doesn't have finite duration.
+// See: https://github.com/flutter/flutter/issues/107882
+const Duration jsCompatibleTimeUnset = Duration(
+ milliseconds: -9007199254740990, // Number.MIN_SAFE_INTEGER + 1. -(2^53 - 1)
+);
+
+/// Converts a `num` duration coming from a [VideoElement] into a [Duration] that
+/// the plugin can use.
+///
+/// From the documentation, `videoDuration` is "a double-precision floating-point
+/// value indicating the duration of the media in seconds.
+/// If no media data is available, the value `NaN` is returned.
+/// If the element's media doesn't have a known duration —such as for live media
+/// streams— the value of duration is `+Infinity`."
+///
+/// If the `videoDuration` is finite, this method returns it as a `Duration`.
+/// If the `videoDuration` is `Infinity`, the duration will be
+/// `-9007199254740990` milliseconds. (See https://github.com/flutter/flutter/issues/107882)
+/// If the `videoDuration` is `NaN`, this will return null.
+Duration? convertNumVideoDurationToPluginDuration(num duration) {
+ if (duration.isFinite) {
+ return Duration(
+ milliseconds: (duration * 1000).round(),
+ );
+ } else if (duration.isInfinite) {
+ return jsCompatibleTimeUnset;
+ }
+ return null;
+}
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 0761673..02ead1f 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
@@ -9,6 +9,8 @@
import 'package:flutter/services.dart';
import 'package:video_player_platform_interface/video_player_platform_interface.dart';
+import 'duration_utils.dart';
+
// An error code value to error name Map.
// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code
const Map<int, String> _kErrorValueToErrorName = <int, String>{
@@ -194,13 +196,10 @@
// Sends an [VideoEventType.initialized] [VideoEvent] with info about the wrapped video.
void _sendInitialized() {
- final Duration? duration = !_videoElement.duration.isNaN
- ? Duration(
- milliseconds: (_videoElement.duration * 1000).round(),
- )
- : null;
+ final Duration? duration =
+ convertNumVideoDurationToPluginDuration(_videoElement.duration);
- final Size? size = !_videoElement.videoHeight.isNaN
+ final Size? size = _videoElement.videoHeight.isFinite
? Size(
_videoElement.videoWidth.toDouble(),
_videoElement.videoHeight.toDouble(),
diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml
index 36b89ab..4083341 100644
--- a/packages/video_player/video_player_web/pubspec.yaml
+++ b/packages/video_player/video_player_web/pubspec.yaml
@@ -2,7 +2,7 @@
description: Web platform implementation of video_player.
repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player_web
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
-version: 2.0.10
+version: 2.0.11
environment:
sdk: ">=2.12.0 <3.0.0"