[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"