From a67e5ef147308e0fdc5cb0e11406a4b5bc2c982c Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Thu, 26 Dec 2024 00:18:03 -0400 Subject: [PATCH 01/27] Optimize caption retrieval with binary search in VideoPlayerController --- .../video_player/lib/video_player.dart | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index b7ba8340fa66..d037e3b340db 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -728,20 +728,36 @@ class VideoPlayerController extends ValueNotifier { /// /// If no [closedCaptionFile] was specified, this will always return an empty /// [Caption]. + Caption _getCaptionAt(Duration position) { if (_closedCaptionFile == null) { return Caption.none; } final Duration delayedPosition = position + value.captionOffset; - // TODO(johnsonmh): This would be more efficient as a binary search. - for (final Caption caption in _closedCaptionFile!.captions) { - if (caption.start <= delayedPosition && caption.end >= delayedPosition) { - return caption; + + final List captions = _closedCaptionFile!.captions; + + int left = 0; + int right = captions.length - 1; + + while (left <= right) { + final int mid = left + ((right - left) ~/ 2); + final Caption midCaption = captions[mid]; + + if (midCaption.start <= delayedPosition && + midCaption.end >= delayedPosition) { + return midCaption; // Found the matching caption + } else if (midCaption.end < delayedPosition) { + // Move to the right half + left = mid + 1; + } else { + // Move to the left half + right = mid - 1; } } - return Caption.none; + return Caption.none; // No matching caption found } /// Returns the file containing closed captions for the video, if any. From 106f8384ccd485cd3d65361dcb9c850eab8a8e86 Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Thu, 26 Dec 2024 00:19:48 -0400 Subject: [PATCH 02/27] Update Change Log --- packages/video_player/video_player/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 22a4bc0ed80a..38722c2e9964 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. +* Use Binary search for finding the correct caption ## 2.9.2 From 44aefb8402ce1b37796c96a9fd06eb0a8038bd0b Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Thu, 26 Dec 2024 15:54:51 -0400 Subject: [PATCH 03/27] Refactor: Optimize caption lookup using binary search with `package:collection`, sorting captions and caching start times for improved performance. --- .../video_player/lib/video_player.dart | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index d037e3b340db..695560d0ed1f 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -378,6 +378,10 @@ class VideoPlayerController extends ValueNotifier { Future? _closedCaptionFileFuture; ClosedCaptionFile? _closedCaptionFile; + + /// The starting durations of each closed caption. Used for binary search. + /// to avoid mapping the list of captions each time. + List _closedCaptionStartingDurations = []; Timer? _timer; bool _isDisposed = false; Completer? _creatingCompleter; @@ -736,25 +740,18 @@ class VideoPlayerController extends ValueNotifier { final Duration delayedPosition = position + value.captionOffset; - final List captions = _closedCaptionFile!.captions; - - int left = 0; - int right = captions.length - 1; + final int captionIndex = binarySearch( + _closedCaptionStartingDurations, delayedPosition); - while (left <= right) { - final int mid = left + ((right - left) ~/ 2); - final Caption midCaption = captions[mid]; + /// if the captionIndex is -1, then the position is before the first caption. + if (captionIndex == -1) { + return Caption.none; + } + final Caption caption = _closedCaptionFile!.captions[captionIndex]; - if (midCaption.start <= delayedPosition && - midCaption.end >= delayedPosition) { - return midCaption; // Found the matching caption - } else if (midCaption.end < delayedPosition) { - // Move to the right half - left = mid + 1; - } else { - // Move to the left half - right = mid - 1; - } + /// Check if the current position is within the caption's start and end time. + if (caption.start <= delayedPosition && caption.end >= delayedPosition) { + return caption; } return Caption.none; // No matching caption found @@ -779,6 +776,16 @@ class VideoPlayerController extends ValueNotifier { Future? closedCaptionFile, ) async { _closedCaptionFile = await closedCaptionFile; + if (_closedCaptionFile != null) { + /// Sort the captions by start time so that we can do a binary search. + _closedCaptionFile!.captions.sort((Caption a, Caption b) { + return a.start.compareTo(b.start); + }); + + /// Store the starting durations of each caption to avoid mapping the list on each check + _closedCaptionStartingDurations = + _closedCaptionFile!.captions.map((Caption e) => e.start).toList(); + } value = value.copyWith(caption: _getCaptionAt(value.position)); } From 08f21d94746c60adc51ec07d683283ae3fea4a36 Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Thu, 26 Dec 2024 17:07:43 -0400 Subject: [PATCH 04/27] Refactor: Enhance closed caption retrieval using binary search from `package:collection` and streamline caption sorting --- .../video_player/lib/video_player.dart | 33 ++++++++++--------- .../video_player/video_player/pubspec.yaml | 2 ++ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 695560d0ed1f..06478f0d536c 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math' as math; +import 'package:collection/collection.dart' as collection; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -379,9 +380,6 @@ class VideoPlayerController extends ValueNotifier { Future? _closedCaptionFileFuture; ClosedCaptionFile? _closedCaptionFile; - /// The starting durations of each closed caption. Used for binary search. - /// to avoid mapping the list of captions each time. - List _closedCaptionStartingDurations = []; Timer? _timer; bool _isDisposed = false; Completer? _creatingCompleter; @@ -740,8 +738,17 @@ class VideoPlayerController extends ValueNotifier { final Duration delayedPosition = position + value.captionOffset; - final int captionIndex = binarySearch( - _closedCaptionStartingDurations, delayedPosition); + final int captionIndex = collection.binarySearch( + _closedCaptionFile!.captions, + Caption(number: -1, start: delayedPosition, end: Duration.zero, text: ''), + compare: (Caption p0, Caption p1) { + if (p0.start <= delayedPosition && p0.end >= delayedPosition) { + return 0; + } else { + return p0.start.compareTo(p1.start); + } + }, + ); /// if the captionIndex is -1, then the position is before the first caption. if (captionIndex == -1) { @@ -776,16 +783,12 @@ class VideoPlayerController extends ValueNotifier { Future? closedCaptionFile, ) async { _closedCaptionFile = await closedCaptionFile; - if (_closedCaptionFile != null) { - /// Sort the captions by start time so that we can do a binary search. - _closedCaptionFile!.captions.sort((Caption a, Caption b) { - return a.start.compareTo(b.start); - }); - - /// Store the starting durations of each caption to avoid mapping the list on each check - _closedCaptionStartingDurations = - _closedCaptionFile!.captions.map((Caption e) => e.start).toList(); - } + + /// Sort the captions by start time so that we can do a binary search. + _closedCaptionFile?.captions.sort((Caption a, Caption b) { + return a.start.compareTo(b.start); + }); + value = value.copyWith(caption: _getCaptionAt(value.position)); } diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index c19f6c6f77ca..ed69f80174d8 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -25,11 +25,13 @@ dependencies: flutter: sdk: flutter html: ^0.15.0 + collection: ^1.19.0 video_player_android: ^2.3.5 video_player_avfoundation: ^2.5.6 video_player_platform_interface: ^6.2.0 video_player_web: ^2.1.0 + dev_dependencies: flutter_test: sdk: flutter From 246ee3fff3b4356028851d721b18e8864496cc7b Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Fri, 27 Dec 2024 12:49:19 -0400 Subject: [PATCH 05/27] Bump version to 2.9.3 --- packages/video_player/video_player/CHANGELOG.md | 2 +- packages/video_player/video_player/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 38722c2e9964..b3d68431533e 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,4 +1,4 @@ -## NEXT +## 2.9.3 * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. * Use Binary search for finding the correct caption diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index ed69f80174d8..0ed822f585f0 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.9.2 +version: 2.9.3 environment: sdk: ^3.4.0 From 21426597d7d2f312c12f747417cbf3a7d5ec8a53 Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Fri, 27 Dec 2024 13:24:29 -0400 Subject: [PATCH 06/27] test: Enhance closed caption tests for sorting and seeking behavior --- .../video_player/test/video_player_test.dart | 99 +++++++++++++------ 1 file changed, 69 insertions(+), 30 deletions(-) diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index f6eef2448119..613482a2ce0a 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -98,12 +98,34 @@ class _FakeClosedCaptionFile extends ClosedCaptionFile { start: Duration(milliseconds: 100), end: Duration(milliseconds: 200), ), + const Caption( text: 'two', number: 1, start: Duration(milliseconds: 300), end: Duration(milliseconds: 400), ), + + /// out of order subs to test sorting + const Caption( + text: 'three', + number: 1, + start: Duration(milliseconds: 500), + end: Duration(milliseconds: 600), + ), + + const Caption( + text: 'five', + number: 0, + start: Duration(milliseconds: 700), + end: Duration(milliseconds: 800), + ), + const Caption( + text: 'four', + number: 0, + start: Duration(milliseconds: 600), + end: Duration(milliseconds: 700), + ), ]; } } @@ -727,7 +749,7 @@ void main() { }); group('caption', () { - test('works when seeking', () async { + test('works when seeking, includes all captions', () async { final VideoPlayerController controller = VideoPlayerController.networkUrl( _localhostUri, @@ -747,20 +769,44 @@ void main() { await controller.seekTo(const Duration(milliseconds: 300)); expect(controller.value.caption.text, 'two'); - await controller.seekTo(const Duration(milliseconds: 301)); - expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 400)); + expect(controller.value.caption.text, ''); await controller.seekTo(const Duration(milliseconds: 500)); + expect(controller.value.caption.text, 'three'); + + await controller.seekTo(const Duration(milliseconds: 600)); + expect(controller.value.caption.text, 'four'); + + await controller.seekTo(const Duration(milliseconds: 700)); + expect(controller.value.caption.text, 'five'); + + await controller.seekTo(const Duration(milliseconds: 800)); expect(controller.value.caption.text, ''); + // Test going back await controller.seekTo(const Duration(milliseconds: 300)); expect(controller.value.caption.text, 'two'); + }); - await controller.seekTo(const Duration(milliseconds: 301)); - expect(controller.value.caption.text, 'two'); + test('captions are sorted correctly on initialization', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); + + await controller.initialize(); + final sortedCaptions = (await controller.closedCaptionFile!).captions; + for (int i = 1; i < sortedCaptions.length; i++) { + expect( + sortedCaptions[i - 1].start <= sortedCaptions[i].start, isTrue); + } }); - test('works when seeking with captionOffset positive', () async { + test( + 'works when seeking with captionOffset positive, includes all captions', + () async { final VideoPlayerController controller = VideoPlayerController.networkUrl( _localhostUri, @@ -773,31 +819,30 @@ void main() { expect(controller.value.caption.text, ''); await controller.seekTo(const Duration(milliseconds: 100)); - expect(controller.value.caption.text, 'one'); - - await controller.seekTo(const Duration(milliseconds: 101)); expect(controller.value.caption.text, ''); - await controller.seekTo(const Duration(milliseconds: 250)); - expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 200)); + expect(controller.value.caption.text, 'one'); - await controller.seekTo(const Duration(milliseconds: 300)); + await controller.seekTo(const Duration(milliseconds: 400)); expect(controller.value.caption.text, 'two'); - await controller.seekTo(const Duration(milliseconds: 301)); - expect(controller.value.caption.text, ''); - await controller.seekTo(const Duration(milliseconds: 500)); - expect(controller.value.caption.text, ''); + expect(controller.value.caption.text, 'three'); - await controller.seekTo(const Duration(milliseconds: 300)); - expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 600)); + expect(controller.value.caption.text, 'four'); - await controller.seekTo(const Duration(milliseconds: 301)); + await controller.seekTo(const Duration(milliseconds: 700)); + expect(controller.value.caption.text, 'five'); + + await controller.seekTo(const Duration(milliseconds: 800)); expect(controller.value.caption.text, ''); }); - test('works when seeking with captionOffset negative', () async { + test( + 'works when seeking with captionOffset negative, includes all captions', + () async { final VideoPlayerController controller = VideoPlayerController.networkUrl( _localhostUri, @@ -815,26 +860,20 @@ void main() { await controller.seekTo(const Duration(milliseconds: 200)); expect(controller.value.caption.text, 'one'); - await controller.seekTo(const Duration(milliseconds: 250)); - expect(controller.value.caption.text, 'one'); - await controller.seekTo(const Duration(milliseconds: 300)); expect(controller.value.caption.text, 'one'); - await controller.seekTo(const Duration(milliseconds: 301)); - expect(controller.value.caption.text, ''); - await controller.seekTo(const Duration(milliseconds: 400)); expect(controller.value.caption.text, 'two'); await controller.seekTo(const Duration(milliseconds: 500)); - expect(controller.value.caption.text, 'two'); + expect(controller.value.caption.text, 'three'); await controller.seekTo(const Duration(milliseconds: 600)); - expect(controller.value.caption.text, ''); + expect(controller.value.caption.text, 'four'); - await controller.seekTo(const Duration(milliseconds: 300)); - expect(controller.value.caption.text, 'one'); + await controller.seekTo(const Duration(milliseconds: 700)); + expect(controller.value.caption.text, 'five'); }); test('setClosedCaptionFile loads caption file', () async { From 5e9ace9dc77cea9b8b6df1d3d97bdd03830a3c55 Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Fri, 27 Dec 2024 13:31:33 -0400 Subject: [PATCH 07/27] feat: Add sortedCaptions getter to VideoPlayerController --- .../video_player/video_player/lib/video_player.dart | 11 ++++++++--- .../video_player/test/video_player_test.dart | 6 +++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 06478f0d536c..84d5da4c1093 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -379,7 +379,10 @@ class VideoPlayerController extends ValueNotifier { Future? _closedCaptionFileFuture; ClosedCaptionFile? _closedCaptionFile; + List? _sortedCaptions; + /// The sorted list of captions from the closed caption file. + List? get sortedCaptions => _sortedCaptions; Timer? _timer; bool _isDisposed = false; Completer? _creatingCompleter; @@ -732,14 +735,14 @@ class VideoPlayerController extends ValueNotifier { /// [Caption]. Caption _getCaptionAt(Duration position) { - if (_closedCaptionFile == null) { + if (_closedCaptionFile == null || _sortedCaptions == null) { return Caption.none; } final Duration delayedPosition = position + value.captionOffset; final int captionIndex = collection.binarySearch( - _closedCaptionFile!.captions, + _sortedCaptions!, Caption(number: -1, start: delayedPosition, end: Duration.zero, text: ''), compare: (Caption p0, Caption p1) { if (p0.start <= delayedPosition && p0.end >= delayedPosition) { @@ -784,8 +787,10 @@ class VideoPlayerController extends ValueNotifier { ) async { _closedCaptionFile = await closedCaptionFile; + _sortedCaptions = _closedCaptionFile?.captions; + /// Sort the captions by start time so that we can do a binary search. - _closedCaptionFile?.captions.sort((Caption a, Caption b) { + _sortedCaptions?.sort((Caption a, Caption b) { return a.start.compareTo(b.start); }); diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 613482a2ce0a..5adc0eefbf3e 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -83,6 +83,9 @@ class FakeController extends ValueNotifier Future setClosedCaptionFile( Future? closedCaptionFile, ) async {} + + @override + List? get sortedCaptions => null; } Future _loadClosedCaption() async => @@ -797,7 +800,8 @@ void main() { ); await controller.initialize(); - final sortedCaptions = (await controller.closedCaptionFile!).captions; + + final List sortedCaptions = controller.sortedCaptions!; for (int i = 1; i < sortedCaptions.length; i++) { expect( sortedCaptions[i - 1].start <= sortedCaptions[i].start, isTrue); From b74b3523010d19ce3577c6eedf25ff77659207e8 Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Fri, 27 Dec 2024 14:17:28 -0400 Subject: [PATCH 08/27] fix: Correct binary search logic for caption retrieval and update tests for accurate caption display --- .../video_player/lib/video_player.dart | 24 +++++++++----- .../video_player/test/video_player_test.dart | 32 +++++++++++++------ 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 84d5da4c1093..b54606ccf372 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -743,23 +743,31 @@ class VideoPlayerController extends ValueNotifier { final int captionIndex = collection.binarySearch( _sortedCaptions!, - Caption(number: -1, start: delayedPosition, end: Duration.zero, text: ''), - compare: (Caption p0, Caption p1) { - if (p0.start <= delayedPosition && p0.end >= delayedPosition) { - return 0; + Caption( + number: -1, + start: delayedPosition, + end: delayedPosition, + text: '', + ), + compare: (Caption candidate, Caption search) { + if (search.start < candidate.start) { + return 1; + } else if (search.start > candidate.end) { + return -1; } else { - return p0.start.compareTo(p1.start); + // delayedPosition is within [candidate.start, candidate.end] + return 0; } }, ); - /// if the captionIndex is -1, then the position is before the first caption. + // -1 means not found by the binary search. if (captionIndex == -1) { return Caption.none; } - final Caption caption = _closedCaptionFile!.captions[captionIndex]; - /// Check if the current position is within the caption's start and end time. + final Caption caption = _sortedCaptions![captionIndex]; + // check if it really fits within that caption's [start, end]. if (caption.start <= delayedPosition && caption.end >= delayedPosition) { return caption; } diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 5adc0eefbf3e..a7f2922a36da 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -83,7 +83,7 @@ class FakeController extends ValueNotifier Future setClosedCaptionFile( Future? closedCaptionFile, ) async {} - + @override List? get sortedCaptions => null; } @@ -773,18 +773,23 @@ void main() { expect(controller.value.caption.text, 'two'); await controller.seekTo(const Duration(milliseconds: 400)); + expect(controller.value.caption.text, 'two'); + + await controller.seekTo(const Duration(milliseconds: 401)); expect(controller.value.caption.text, ''); await controller.seekTo(const Duration(milliseconds: 500)); expect(controller.value.caption.text, 'three'); - await controller.seekTo(const Duration(milliseconds: 600)); + await controller.seekTo(const Duration(milliseconds: 601)); expect(controller.value.caption.text, 'four'); - await controller.seekTo(const Duration(milliseconds: 700)); + await controller.seekTo(const Duration(milliseconds: 701)); expect(controller.value.caption.text, 'five'); await controller.seekTo(const Duration(milliseconds: 800)); + expect(controller.value.caption.text, 'five'); + await controller.seekTo(const Duration(milliseconds: 801)); expect(controller.value.caption.text, ''); // Test going back @@ -822,20 +827,29 @@ void main() { expect(controller.value.position, Duration.zero); expect(controller.value.caption.text, ''); + await controller.seekTo(const Duration(milliseconds: 99)); + expect(controller.value.caption.text, 'one'); + await controller.seekTo(const Duration(milliseconds: 100)); + expect(controller.value.caption.text, 'one'); + + await controller.seekTo(const Duration(milliseconds: 150)); expect(controller.value.caption.text, ''); await controller.seekTo(const Duration(milliseconds: 200)); - expect(controller.value.caption.text, 'one'); + expect(controller.value.caption.text, 'two'); - await controller.seekTo(const Duration(milliseconds: 400)); + await controller.seekTo(const Duration(milliseconds: 201)); expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 400)); + expect(controller.value.caption.text, 'three'); + await controller.seekTo(const Duration(milliseconds: 500)); expect(controller.value.caption.text, 'three'); await controller.seekTo(const Duration(milliseconds: 600)); - expect(controller.value.caption.text, 'four'); + expect(controller.value.caption.text, 'five'); await controller.seekTo(const Duration(milliseconds: 700)); expect(controller.value.caption.text, 'five'); @@ -871,13 +885,13 @@ void main() { expect(controller.value.caption.text, 'two'); await controller.seekTo(const Duration(milliseconds: 500)); - expect(controller.value.caption.text, 'three'); + expect(controller.value.caption.text, 'two'); await controller.seekTo(const Duration(milliseconds: 600)); - expect(controller.value.caption.text, 'four'); + expect(controller.value.caption.text, 'three'); await controller.seekTo(const Duration(milliseconds: 700)); - expect(controller.value.caption.text, 'five'); + expect(controller.value.caption.text, 'three'); }); test('setClosedCaptionFile loads caption file', () async { From cedd1a4385ba644338cee90095511f72d164158c Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Fri, 27 Dec 2024 14:23:26 -0400 Subject: [PATCH 09/27] adding more captions checks --- .../video_player/test/video_player_test.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index a7f2922a36da..3558946ead4e 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -772,6 +772,9 @@ void main() { await controller.seekTo(const Duration(milliseconds: 300)); expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 301)); + expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 400)); expect(controller.value.caption.text, 'two'); @@ -833,6 +836,9 @@ void main() { await controller.seekTo(const Duration(milliseconds: 100)); expect(controller.value.caption.text, 'one'); + await controller.seekTo(const Duration(milliseconds: 101)); + expect(controller.value.caption.text, ''); + await controller.seekTo(const Duration(milliseconds: 150)); expect(controller.value.caption.text, ''); @@ -878,9 +884,15 @@ void main() { await controller.seekTo(const Duration(milliseconds: 200)); expect(controller.value.caption.text, 'one'); + await controller.seekTo(const Duration(milliseconds: 250)); + expect(controller.value.caption.text, 'one'); + await controller.seekTo(const Duration(milliseconds: 300)); expect(controller.value.caption.text, 'one'); + await controller.seekTo(const Duration(milliseconds: 301)); + expect(controller.value.caption.text, ''); + await controller.seekTo(const Duration(milliseconds: 400)); expect(controller.value.caption.text, 'two'); From 9a23e4787603fcacae64d227fcd3bbe2e4710bcd Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Fri, 27 Dec 2024 16:30:19 -0400 Subject: [PATCH 10/27] refactor: Remove sortedCaptions getter and related test. --- .../video_player/lib/video_player.dart | 2 -- .../video_player/test/video_player_test.dart | 19 ------------------- 2 files changed, 21 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index b54606ccf372..eeef6aed358b 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -381,8 +381,6 @@ class VideoPlayerController extends ValueNotifier { ClosedCaptionFile? _closedCaptionFile; List? _sortedCaptions; - /// The sorted list of captions from the closed caption file. - List? get sortedCaptions => _sortedCaptions; Timer? _timer; bool _isDisposed = false; Completer? _creatingCompleter; diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 3558946ead4e..50dc871fa471 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -83,9 +83,6 @@ class FakeController extends ValueNotifier Future setClosedCaptionFile( Future? closedCaptionFile, ) async {} - - @override - List? get sortedCaptions => null; } Future _loadClosedCaption() async => @@ -800,22 +797,6 @@ void main() { expect(controller.value.caption.text, 'two'); }); - test('captions are sorted correctly on initialization', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); - - await controller.initialize(); - - final List sortedCaptions = controller.sortedCaptions!; - for (int i = 1; i < sortedCaptions.length; i++) { - expect( - sortedCaptions[i - 1].start <= sortedCaptions[i].start, isTrue); - } - }); - test( 'works when seeking with captionOffset positive, includes all captions', () async { From 57260cb65fdb582569b9b39a7e8fcf4be648d039 Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Tue, 31 Dec 2024 14:56:35 -0400 Subject: [PATCH 11/27] test: Add test to verify input captions are unsorted --- .../video_player/test/video_player_test.dart | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 50dc871fa471..d2d5f5ae544c 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -749,6 +749,31 @@ void main() { }); group('caption', () { + test('makes sure the input captions are unsorted', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); + + await controller.initialize(); + final List captions = + (await controller.closedCaptionFile)!.captions.toList(); + + // Check that captions are not in sorted order + bool isSorted = true; + for (int i = 0; i < captions.length - 1; i++) { + if (captions[i].start.compareTo(captions[i + 1].start) > 0) { + isSorted = false; + break; + } + } + + expect(isSorted, false, reason: 'Expected captions to be unsorted'); + expect(captions.map((Caption c) => c.text).toList(), + ['one', 'two', 'three', 'five', 'four'], + reason: 'Captions should be in original unsorted order'); + }); test('works when seeking, includes all captions', () async { final VideoPlayerController controller = VideoPlayerController.networkUrl( From 5b383f025c31e918f0992576076c313c2dc9f604 Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Wed, 22 Jan 2025 20:16:13 -0400 Subject: [PATCH 12/27] chore: Update pubspec.yaml deps to be sorted alphabetically. --- packages/video_player/video_player/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 0ed822f585f0..07d8df517d0f 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -22,10 +22,10 @@ flutter: default_package: video_player_web dependencies: + collection: ^1.19.0 flutter: sdk: flutter html: ^0.15.0 - collection: ^1.19.0 video_player_android: ^2.3.5 video_player_avfoundation: ^2.5.6 video_player_platform_interface: ^6.2.0 From 465014f82e782f2397d1ca306370b18ef2c7098c Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Wed, 22 Jan 2025 20:17:47 -0400 Subject: [PATCH 13/27] chore: improve change log to follow guidelines --- packages/video_player/video_player/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index b3d68431533e..842b0637b96b 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,7 +1,7 @@ ## 2.9.3 * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. -* Use Binary search for finding the correct caption +* Optimizes caption retrieval with Binary search. ## 2.9.2 From d41f6a470e4a1a5c52c85713c129d335b95a87b1 Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Wed, 22 Jan 2025 20:29:25 -0400 Subject: [PATCH 14/27] chore: remove unnecessary blank line in pubspec.yaml --- packages/video_player/video_player/pubspec.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 07d8df517d0f..59049e7ca8b1 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -31,7 +31,6 @@ dependencies: video_player_platform_interface: ^6.2.0 video_player_web: ^2.1.0 - dev_dependencies: flutter_test: sdk: flutter From edb882c5ce28a952a0f899cb24c231888924ea74 Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Thu, 23 Jan 2025 22:05:34 -0400 Subject: [PATCH 15/27] chore: downgrade collection dependency from 1.19.0 to 1.18.0 --- packages/video_player/video_player/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 59049e7ca8b1..6d7fb70585ec 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -22,7 +22,7 @@ flutter: default_package: video_player_web dependencies: - collection: ^1.19.0 + collection: ^1.18.0 flutter: sdk: flutter html: ^0.15.0 From 1730ea3fd60aac46308dad73e7dce85f651f9f8f Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Fri, 21 Feb 2025 16:21:27 -0400 Subject: [PATCH 16/27] chore: follow guide lines/review --- packages/video_player/video_player/CHANGELOG.md | 2 +- packages/video_player/video_player/lib/video_player.dart | 2 +- packages/video_player/video_player/test/video_player_test.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 842b0637b96b..0aad1a887865 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,7 +1,7 @@ ## 2.9.3 * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. -* Optimizes caption retrieval with Binary search. +* Optimizes caption retrieval with binary search. ## 2.9.2 diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index eeef6aed358b..1797dd45b869 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -795,7 +795,7 @@ class VideoPlayerController extends ValueNotifier { _sortedCaptions = _closedCaptionFile?.captions; - /// Sort the captions by start time so that we can do a binary search. + /// Sort the captions by start time to allow a binary search. _sortedCaptions?.sort((Caption a, Caption b) { return a.start.compareTo(b.start); }); diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index d2d5f5ae544c..a3fa5adaf90b 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -760,7 +760,7 @@ void main() { final List captions = (await controller.closedCaptionFile)!.captions.toList(); - // Check that captions are not in sorted order + // Check that captions are not in sorted order. bool isSorted = true; for (int i = 0; i < captions.length - 1; i++) { if (captions[i].start.compareTo(captions[i + 1].start) > 0) { From 0dc3aef7470120cac94766d06bf7039143970fe8 Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Fri, 21 Feb 2025 16:37:02 -0400 Subject: [PATCH 17/27] chore: only sort ClosedCaptions when file changes --- .../video_player/lib/video_player.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 1797dd45b869..a0b9530308a0 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -509,6 +509,7 @@ class VideoPlayerController extends ValueNotifier { if (_closedCaptionFileFuture != null) { await _updateClosedCaptionWithFuture(_closedCaptionFileFuture); + _sortClosedCaption(); } void errorListener(Object obj) { @@ -786,19 +787,22 @@ class VideoPlayerController extends ValueNotifier { ) async { await _updateClosedCaptionWithFuture(closedCaptionFile); _closedCaptionFileFuture = closedCaptionFile; + _sortClosedCaption(); } - Future _updateClosedCaptionWithFuture( - Future? closedCaptionFile, - ) async { - _closedCaptionFile = await closedCaptionFile; - + void _sortClosedCaption() { _sortedCaptions = _closedCaptionFile?.captions; /// Sort the captions by start time to allow a binary search. _sortedCaptions?.sort((Caption a, Caption b) { return a.start.compareTo(b.start); }); + } + + Future _updateClosedCaptionWithFuture( + Future? closedCaptionFile, + ) async { + _closedCaptionFile = await closedCaptionFile; value = value.copyWith(caption: _getCaptionAt(value.position)); } From aafadf856dbf9f55c36571152d145482c7945491 Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Fri, 21 Feb 2025 16:41:09 -0400 Subject: [PATCH 18/27] chore: fix spacing in change log --- packages/video_player/video_player/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 23a3a433403d..1e3516d26a0e 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -7,7 +7,6 @@ * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. * Fixes mechanism to detect identifier in multi-line WebVTT captions. - ## 2.9.2 * Updates minimum supported SDK version to Flutter 3.19/Dart 3.3. From e4dc51e1ee3021b1f5d9f31cdad2c42a5eac2929 Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Fri, 21 Feb 2025 23:41:03 -0400 Subject: [PATCH 19/27] chore: optimize closed caption sorting by checking for file changes --- .../video_player/lib/video_player.dart | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index a0b9530308a0..19b03ea43789 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -379,6 +379,7 @@ class VideoPlayerController extends ValueNotifier { Future? _closedCaptionFileFuture; ClosedCaptionFile? _closedCaptionFile; + ClosedCaptionFile? _previousClosedCaptionFile; List? _sortedCaptions; Timer? _timer; @@ -791,12 +792,17 @@ class VideoPlayerController extends ValueNotifier { } void _sortClosedCaption() { - _sortedCaptions = _closedCaptionFile?.captions; + // Only sort if the file has changed + if (_previousClosedCaptionFile != _closedCaptionFile) { + _sortedCaptions = _closedCaptionFile?.captions; - /// Sort the captions by start time to allow a binary search. - _sortedCaptions?.sort((Caption a, Caption b) { - return a.start.compareTo(b.start); - }); + /// Sort the captions by start time to allow a binary search. + _sortedCaptions?.sort((Caption a, Caption b) { + return a.start.compareTo(b.start); + }); + + _previousClosedCaptionFile = _closedCaptionFile; + } } Future _updateClosedCaptionWithFuture( From ae5686b6f3e887e3b91123cad7e876fd00142b4b Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Tue, 13 May 2025 16:45:43 -0300 Subject: [PATCH 20/27] Refactor video player tests for clarity and consistency - Improved the structure of tests related to caption handling, ensuring that the input captions are unsorted and validating the expected order. - Enhanced the seeking tests to include checks for all captions and their respective offsets. - Added tests for playback status, buffering status, and completion updates to ensure accurate state management. - Updated the `VideoPlayerValue` tests to cover edge cases and ensure proper functionality of the `copyWith` method. - Introduced tests for `VideoProgressColors` and `VideoPlayerOptions` to validate their properties and behaviors. This refactor aims to enhance the readability and maintainability of the test suite while ensuring comprehensive coverage of the video player functionality. --- .../video_player/test/video_player_test.dart | 1109 +++++++++-------- 1 file changed, 559 insertions(+), 550 deletions(-) diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 62bb219eada6..29017c026685 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -809,554 +809,593 @@ void main() { }); test('works when seeking', () async { - test('makes sure the input captions are unsorted', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); - - await controller.initialize(); - final List captions = - (await controller.closedCaptionFile)!.captions.toList(); - - // Check that captions are not in sorted order. - bool isSorted = true; - for (int i = 0; i < captions.length - 1; i++) { - if (captions[i].start.compareTo(captions[i + 1].start) > 0) { - isSorted = false; - break; + test('makes sure the input captions are unsorted', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); + + await controller.initialize(); + final List captions = + (await controller.closedCaptionFile)!.captions.toList(); + + // Check that captions are not in sorted order. + bool isSorted = true; + for (int i = 0; i < captions.length - 1; i++) { + if (captions[i].start.compareTo(captions[i + 1].start) > 0) { + isSorted = false; + break; + } } - } - expect(isSorted, false, reason: 'Expected captions to be unsorted'); - expect(captions.map((Caption c) => c.text).toList(), - ['one', 'two', 'three', 'five', 'four'], - reason: 'Captions should be in original unsorted order'); - }); - test('works when seeking, includes all captions', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); - addTearDown(controller.dispose); + expect(isSorted, false, reason: 'Expected captions to be unsorted'); + expect(captions.map((Caption c) => c.text).toList(), + ['one', 'two', 'three', 'five', 'four'], + reason: 'Captions should be in original unsorted order'); + }); + test('works when seeking, includes all captions', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); + addTearDown(controller.dispose); - await controller.initialize(); - expect(controller.value.position, Duration.zero); - expect(controller.value.caption.text, ''); + await controller.initialize(); + expect(controller.value.position, Duration.zero); + expect(controller.value.caption.text, ''); - await controller.seekTo(const Duration(milliseconds: 100)); - expect(controller.value.caption.text, 'one'); + await controller.seekTo(const Duration(milliseconds: 100)); + expect(controller.value.caption.text, 'one'); - await controller.seekTo(const Duration(milliseconds: 250)); - expect(controller.value.caption.text, ''); + await controller.seekTo(const Duration(milliseconds: 250)); + expect(controller.value.caption.text, ''); - await controller.seekTo(const Duration(milliseconds: 300)); - expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 300)); + expect(controller.value.caption.text, 'two'); - await controller.seekTo(const Duration(milliseconds: 301)); - expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 301)); + expect(controller.value.caption.text, 'two'); - await controller.seekTo(const Duration(milliseconds: 400)); - expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 400)); + expect(controller.value.caption.text, 'two'); - await controller.seekTo(const Duration(milliseconds: 401)); - expect(controller.value.caption.text, ''); + await controller.seekTo(const Duration(milliseconds: 401)); + expect(controller.value.caption.text, ''); - await controller.seekTo(const Duration(milliseconds: 500)); - expect(controller.value.caption.text, 'three'); + await controller.seekTo(const Duration(milliseconds: 500)); + expect(controller.value.caption.text, 'three'); - await controller.seekTo(const Duration(milliseconds: 601)); - expect(controller.value.caption.text, 'four'); + await controller.seekTo(const Duration(milliseconds: 601)); + expect(controller.value.caption.text, 'four'); - await controller.seekTo(const Duration(milliseconds: 701)); - expect(controller.value.caption.text, 'five'); + await controller.seekTo(const Duration(milliseconds: 701)); + expect(controller.value.caption.text, 'five'); - await controller.seekTo(const Duration(milliseconds: 800)); - expect(controller.value.caption.text, 'five'); - await controller.seekTo(const Duration(milliseconds: 801)); - expect(controller.value.caption.text, ''); + await controller.seekTo(const Duration(milliseconds: 800)); + expect(controller.value.caption.text, 'five'); + await controller.seekTo(const Duration(milliseconds: 801)); + expect(controller.value.caption.text, ''); - // Test going back - await controller.seekTo(const Duration(milliseconds: 300)); - expect(controller.value.caption.text, 'two'); - }); + // Test going back + await controller.seekTo(const Duration(milliseconds: 300)); + expect(controller.value.caption.text, 'two'); + }); - test( - 'works when seeking with captionOffset positive, includes all captions', - () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); - addTearDown(controller.dispose); + test( + 'works when seeking with captionOffset positive, includes all captions', + () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); + addTearDown(controller.dispose); - await controller.initialize(); - controller.setCaptionOffset(const Duration(milliseconds: 100)); - expect(controller.value.position, Duration.zero); - expect(controller.value.caption.text, ''); + await controller.initialize(); + controller.setCaptionOffset(const Duration(milliseconds: 100)); + expect(controller.value.position, Duration.zero); + expect(controller.value.caption.text, ''); - await controller.seekTo(const Duration(milliseconds: 99)); - expect(controller.value.caption.text, 'one'); + await controller.seekTo(const Duration(milliseconds: 99)); + expect(controller.value.caption.text, 'one'); - await controller.seekTo(const Duration(milliseconds: 100)); - expect(controller.value.caption.text, 'one'); + await controller.seekTo(const Duration(milliseconds: 100)); + expect(controller.value.caption.text, 'one'); - await controller.seekTo(const Duration(milliseconds: 101)); - expect(controller.value.caption.text, ''); + await controller.seekTo(const Duration(milliseconds: 101)); + expect(controller.value.caption.text, ''); - await controller.seekTo(const Duration(milliseconds: 150)); - expect(controller.value.caption.text, ''); + await controller.seekTo(const Duration(milliseconds: 150)); + expect(controller.value.caption.text, ''); - await controller.seekTo(const Duration(milliseconds: 200)); - expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 200)); + expect(controller.value.caption.text, 'two'); - await controller.seekTo(const Duration(milliseconds: 201)); - expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 201)); + expect(controller.value.caption.text, 'two'); - await controller.seekTo(const Duration(milliseconds: 400)); - expect(controller.value.caption.text, 'three'); + await controller.seekTo(const Duration(milliseconds: 400)); + expect(controller.value.caption.text, 'three'); - await controller.seekTo(const Duration(milliseconds: 500)); - expect(controller.value.caption.text, 'three'); + await controller.seekTo(const Duration(milliseconds: 500)); + expect(controller.value.caption.text, 'three'); - await controller.seekTo(const Duration(milliseconds: 600)); - expect(controller.value.caption.text, 'five'); + await controller.seekTo(const Duration(milliseconds: 600)); + expect(controller.value.caption.text, 'five'); - await controller.seekTo(const Duration(milliseconds: 700)); - expect(controller.value.caption.text, 'five'); + await controller.seekTo(const Duration(milliseconds: 700)); + expect(controller.value.caption.text, 'five'); - await controller.seekTo(const Duration(milliseconds: 800)); - expect(controller.value.caption.text, ''); - }); - - test( - 'works when seeking with captionOffset negative, includes all captions', - () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); - addTearDown(controller.dispose); + await controller.seekTo(const Duration(milliseconds: 800)); + expect(controller.value.caption.text, ''); + }); - await controller.initialize(); - controller.setCaptionOffset(const Duration(milliseconds: -100)); - expect(controller.value.position, Duration.zero); - expect(controller.value.caption.text, ''); + test( + 'works when seeking with captionOffset negative, includes all captions', + () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); + addTearDown(controller.dispose); - await controller.seekTo(const Duration(milliseconds: 100)); - expect(controller.value.caption.text, ''); + await controller.initialize(); + controller.setCaptionOffset(const Duration(milliseconds: -100)); + expect(controller.value.position, Duration.zero); + expect(controller.value.caption.text, ''); - await controller.seekTo(const Duration(milliseconds: 200)); - expect(controller.value.caption.text, 'one'); + await controller.seekTo(const Duration(milliseconds: 100)); + expect(controller.value.caption.text, ''); - await controller.seekTo(const Duration(milliseconds: 250)); - expect(controller.value.caption.text, 'one'); + await controller.seekTo(const Duration(milliseconds: 200)); + expect(controller.value.caption.text, 'one'); - await controller.seekTo(const Duration(milliseconds: 300)); - expect(controller.value.caption.text, 'one'); + await controller.seekTo(const Duration(milliseconds: 250)); + expect(controller.value.caption.text, 'one'); - await controller.seekTo(const Duration(milliseconds: 301)); - expect(controller.value.caption.text, ''); + await controller.seekTo(const Duration(milliseconds: 300)); + expect(controller.value.caption.text, 'one'); - await controller.seekTo(const Duration(milliseconds: 400)); - expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 301)); + expect(controller.value.caption.text, ''); - await controller.seekTo(const Duration(milliseconds: 500)); - expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 400)); + expect(controller.value.caption.text, 'two'); - await controller.seekTo(const Duration(milliseconds: 600)); - expect(controller.value.caption.text, 'three'); + await controller.seekTo(const Duration(milliseconds: 500)); + expect(controller.value.caption.text, 'two'); - await controller.seekTo(const Duration(milliseconds: 700)); - expect(controller.value.caption.text, 'three'); - }); + await controller.seekTo(const Duration(milliseconds: 600)); + expect(controller.value.caption.text, 'three'); - test('setClosedCaptionFile loads caption file', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - ); - addTearDown(controller.dispose); + await controller.seekTo(const Duration(milliseconds: 700)); + expect(controller.value.caption.text, 'three'); + }); - await controller.initialize(); - expect(controller.closedCaptionFile, null); + test('setClosedCaptionFile loads caption file', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + ); + addTearDown(controller.dispose); + + await controller.initialize(); + expect(controller.closedCaptionFile, null); + + await controller.setClosedCaptionFile(_loadClosedCaption()); + expect( + (await controller.closedCaptionFile)!.captions, + (await _loadClosedCaption()).captions, + ); + }); - await controller.setClosedCaptionFile(_loadClosedCaption()); - expect( - (await controller.closedCaptionFile)!.captions, - (await _loadClosedCaption()).captions, - ); + test('setClosedCaptionFile removes/changes caption file', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); + addTearDown(controller.dispose); + + await controller.initialize(); + expect( + (await controller.closedCaptionFile)!.captions, + (await _loadClosedCaption()).captions, + ); + + await controller.setClosedCaptionFile(null); + expect(controller.closedCaptionFile, null); + }); }); - test('setClosedCaptionFile removes/changes caption file', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); - addTearDown(controller.dispose); + group('Platform callbacks', () { + testWidgets('playing completed', (WidgetTester tester) async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + ); + + await controller.initialize(); + const Duration nonzeroDuration = Duration(milliseconds: 100); + controller.value = + controller.value.copyWith(duration: nonzeroDuration); + expect(controller.value.isPlaying, isFalse); + await controller.play(); + expect(controller.value.isPlaying, isTrue); + final StreamController fakeVideoEventStream = + fakeVideoPlayerPlatform.streams[controller.textureId]!; + + fakeVideoEventStream + .add(VideoEvent(eventType: VideoEventType.completed)); + await tester.pumpAndSettle(); + + expect(controller.value.isPlaying, isFalse); + expect(controller.value.position, nonzeroDuration); + await tester.runAsync(controller.dispose); + }); - await controller.initialize(); - expect( - (await controller.closedCaptionFile)!.captions, - (await _loadClosedCaption()).captions, - ); + testWidgets('playback status', (WidgetTester tester) async { + final VideoPlayerController controller = + VideoPlayerController.network( + 'https://.0.0.1', + ); + await controller.initialize(); + expect(controller.value.isPlaying, isFalse); + final StreamController fakeVideoEventStream = + fakeVideoPlayerPlatform.streams[controller.textureId]!; + + fakeVideoEventStream.add(VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: true, + )); + await tester.pumpAndSettle(); + expect(controller.value.isPlaying, isTrue); + + fakeVideoEventStream.add(VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: false, + )); + await tester.pumpAndSettle(); + expect(controller.value.isPlaying, isFalse); + await tester.runAsync(controller.dispose); + }); - await controller.setClosedCaptionFile(null); - expect(controller.closedCaptionFile, null); + testWidgets('buffering status', (WidgetTester tester) async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + ); + + await controller.initialize(); + expect(controller.value.isBuffering, false); + expect(controller.value.buffered, isEmpty); + final StreamController fakeVideoEventStream = + fakeVideoPlayerPlatform.streams[controller.textureId]!; + + fakeVideoEventStream + .add(VideoEvent(eventType: VideoEventType.bufferingStart)); + await tester.pumpAndSettle(); + expect(controller.value.isBuffering, isTrue); + + const Duration bufferStart = Duration.zero; + const Duration bufferEnd = Duration(milliseconds: 500); + fakeVideoEventStream.add(VideoEvent( + eventType: VideoEventType.bufferingUpdate, + buffered: [ + DurationRange(bufferStart, bufferEnd), + ])); + await tester.pumpAndSettle(); + expect(controller.value.isBuffering, isTrue); + expect(controller.value.buffered.length, 1); + expect(controller.value.buffered[0].toString(), + DurationRange(bufferStart, bufferEnd).toString()); + + fakeVideoEventStream + .add(VideoEvent(eventType: VideoEventType.bufferingEnd)); + await tester.pumpAndSettle(); + expect(controller.value.isBuffering, isFalse); + await tester.runAsync(controller.dispose); + }); }); }); - group('Platform callbacks', () { - testWidgets('playing completed', (WidgetTester tester) async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - ); - - await controller.initialize(); - const Duration nonzeroDuration = Duration(milliseconds: 100); - controller.value = controller.value.copyWith(duration: nonzeroDuration); - expect(controller.value.isPlaying, isFalse); - await controller.play(); - expect(controller.value.isPlaying, isTrue); - final StreamController fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.textureId]!; + test('updates position', () async { + final VideoPlayerController controller = VideoPlayerController.networkUrl( + _localhostUri, + videoPlayerOptions: VideoPlayerOptions(), + ); - fakeVideoEventStream - .add(VideoEvent(eventType: VideoEventType.completed)); - await tester.pumpAndSettle(); + await controller.initialize(); - expect(controller.value.isPlaying, isFalse); - expect(controller.value.position, nonzeroDuration); - await tester.runAsync(controller.dispose); - }); + const Duration updatesInterval = Duration(milliseconds: 100); - testWidgets('playback status', (WidgetTester tester) async { - final VideoPlayerController controller = VideoPlayerController.network( - 'https://.0.0.1', - ); - await controller.initialize(); - expect(controller.value.isPlaying, isFalse); - final StreamController fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.textureId]!; + final List positions = []; + final Completer intervalUpdateCompleter = Completer(); - fakeVideoEventStream.add(VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: true, - )); - await tester.pumpAndSettle(); - expect(controller.value.isPlaying, isTrue); - - fakeVideoEventStream.add(VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: false, - )); - await tester.pumpAndSettle(); - expect(controller.value.isPlaying, isFalse); - await tester.runAsync(controller.dispose); + // Listen for position updates + controller.addListener(() { + positions.add(controller.value.position); + if (positions.length >= 3 && !intervalUpdateCompleter.isCompleted) { + intervalUpdateCompleter.complete(); + } }); + await controller.play(); + for (int i = 0; i < 3; i++) { + await Future.delayed(updatesInterval); + fakeVideoPlayerPlatform._positions[controller.textureId] = + Duration(milliseconds: i * updatesInterval.inMilliseconds); + } - testWidgets('buffering status', (WidgetTester tester) async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - ); - - await controller.initialize(); - expect(controller.value.isBuffering, false); - expect(controller.value.buffered, isEmpty); - final StreamController fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.textureId]!; - - fakeVideoEventStream - .add(VideoEvent(eventType: VideoEventType.bufferingStart)); - await tester.pumpAndSettle(); - expect(controller.value.isBuffering, isTrue); - - const Duration bufferStart = Duration.zero; - const Duration bufferEnd = Duration(milliseconds: 500); - fakeVideoEventStream.add(VideoEvent( - eventType: VideoEventType.bufferingUpdate, - buffered: [ - DurationRange(bufferStart, bufferEnd), - ])); - await tester.pumpAndSettle(); - expect(controller.value.isBuffering, isTrue); - expect(controller.value.buffered.length, 1); - expect(controller.value.buffered[0].toString(), - DurationRange(bufferStart, bufferEnd).toString()); + // Wait for at least 3 position updates + await intervalUpdateCompleter.future; - fakeVideoEventStream - .add(VideoEvent(eventType: VideoEventType.bufferingEnd)); - await tester.pumpAndSettle(); - expect(controller.value.isBuffering, isFalse); - await tester.runAsync(controller.dispose); - }); + // Verify that the intervals between updates are approximately correct + expect( + positions[1] - positions[0], greaterThanOrEqualTo(updatesInterval)); + expect( + positions[2] - positions[1], greaterThanOrEqualTo(updatesInterval)); }); - }); - - test('updates position', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - videoPlayerOptions: VideoPlayerOptions(), - ); - - await controller.initialize(); - const Duration updatesInterval = Duration(milliseconds: 100); + group('DurationRange', () { + test('uses given values', () { + const Duration start = Duration(seconds: 2); + const Duration end = Duration(seconds: 8); - final List positions = []; - final Completer intervalUpdateCompleter = Completer(); + final DurationRange range = DurationRange(start, end); - // Listen for position updates - controller.addListener(() { - positions.add(controller.value.position); - if (positions.length >= 3 && !intervalUpdateCompleter.isCompleted) { - intervalUpdateCompleter.complete(); - } - }); - await controller.play(); - for (int i = 0; i < 3; i++) { - await Future.delayed(updatesInterval); - fakeVideoPlayerPlatform._positions[controller.textureId] = - Duration(milliseconds: i * updatesInterval.inMilliseconds); - } - - // Wait for at least 3 position updates - await intervalUpdateCompleter.future; - - // Verify that the intervals between updates are approximately correct - expect(positions[1] - positions[0], greaterThanOrEqualTo(updatesInterval)); - expect(positions[2] - positions[1], greaterThanOrEqualTo(updatesInterval)); - }); + expect(range.start, start); + expect(range.end, end); + expect(range.toString(), contains('start: $start, end: $end')); + }); - group('DurationRange', () { - test('uses given values', () { - const Duration start = Duration(seconds: 2); - const Duration end = Duration(seconds: 8); + test('calculates fractions', () { + const Duration start = Duration(seconds: 2); + const Duration end = Duration(seconds: 8); + const Duration total = Duration(seconds: 10); - final DurationRange range = DurationRange(start, end); + final DurationRange range = DurationRange(start, end); - expect(range.start, start); - expect(range.end, end); - expect(range.toString(), contains('start: $start, end: $end')); + expect(range.startFraction(total), .2); + expect(range.endFraction(total), .8); + }); }); - test('calculates fractions', () { - const Duration start = Duration(seconds: 2); - const Duration end = Duration(seconds: 8); - const Duration total = Duration(seconds: 10); + group('VideoPlayerValue', () { + test('uninitialized()', () { + const VideoPlayerValue uninitialized = VideoPlayerValue.uninitialized(); + + expect(uninitialized.duration, equals(Duration.zero)); + expect(uninitialized.position, equals(Duration.zero)); + expect(uninitialized.caption, equals(Caption.none)); + expect(uninitialized.captionOffset, equals(Duration.zero)); + expect(uninitialized.buffered, isEmpty); + expect(uninitialized.isPlaying, isFalse); + expect(uninitialized.isLooping, isFalse); + expect(uninitialized.isBuffering, isFalse); + expect(uninitialized.volume, 1.0); + expect(uninitialized.playbackSpeed, 1.0); + expect(uninitialized.errorDescription, isNull); + expect(uninitialized.size, equals(Size.zero)); + expect(uninitialized.isInitialized, isFalse); + expect(uninitialized.hasError, isFalse); + expect(uninitialized.aspectRatio, 1.0); + }); - final DurationRange range = DurationRange(start, end); + test('erroneous()', () { + const String errorMessage = 'foo'; + const VideoPlayerValue error = VideoPlayerValue.erroneous(errorMessage); + + expect(error.duration, equals(Duration.zero)); + expect(error.position, equals(Duration.zero)); + expect(error.caption, equals(Caption.none)); + expect(error.captionOffset, equals(Duration.zero)); + expect(error.buffered, isEmpty); + expect(error.isPlaying, isFalse); + expect(error.isLooping, isFalse); + expect(error.isBuffering, isFalse); + expect(error.volume, 1.0); + expect(error.playbackSpeed, 1.0); + expect(error.errorDescription, errorMessage); + expect(error.size, equals(Size.zero)); + expect(error.isInitialized, isFalse); + expect(error.hasError, isTrue); + expect(error.aspectRatio, 1.0); + }); - expect(range.startFraction(total), .2); - expect(range.endFraction(total), .8); - }); - }); + test('toString()', () { + const Duration duration = Duration(seconds: 5); + const Size size = Size(400, 300); + const Duration position = Duration(seconds: 1); + const Caption caption = Caption( + text: 'foo', number: 0, start: Duration.zero, end: Duration.zero); + const Duration captionOffset = Duration(milliseconds: 250); + final List buffered = [ + DurationRange(Duration.zero, const Duration(seconds: 4)) + ]; + const bool isInitialized = true; + const bool isPlaying = true; + const bool isLooping = true; + const bool isBuffering = true; + const double volume = 0.5; + const double playbackSpeed = 1.5; + + final VideoPlayerValue value = VideoPlayerValue( + duration: duration, + size: size, + position: position, + caption: caption, + captionOffset: captionOffset, + buffered: buffered, + isInitialized: isInitialized, + isPlaying: isPlaying, + isLooping: isLooping, + isBuffering: isBuffering, + volume: volume, + playbackSpeed: playbackSpeed, + ); - group('VideoPlayerValue', () { - test('uninitialized()', () { - const VideoPlayerValue uninitialized = VideoPlayerValue.uninitialized(); - - expect(uninitialized.duration, equals(Duration.zero)); - expect(uninitialized.position, equals(Duration.zero)); - expect(uninitialized.caption, equals(Caption.none)); - expect(uninitialized.captionOffset, equals(Duration.zero)); - expect(uninitialized.buffered, isEmpty); - expect(uninitialized.isPlaying, isFalse); - expect(uninitialized.isLooping, isFalse); - expect(uninitialized.isBuffering, isFalse); - expect(uninitialized.volume, 1.0); - expect(uninitialized.playbackSpeed, 1.0); - expect(uninitialized.errorDescription, isNull); - expect(uninitialized.size, equals(Size.zero)); - expect(uninitialized.isInitialized, isFalse); - expect(uninitialized.hasError, isFalse); - expect(uninitialized.aspectRatio, 1.0); - }); + expect( + value.toString(), + 'VideoPlayerValue(duration: 0:00:05.000000, ' + 'size: Size(400.0, 300.0), ' + 'position: 0:00:01.000000, ' + 'caption: Caption(number: 0, start: 0:00:00.000000, end: 0:00:00.000000, text: foo), ' + 'captionOffset: 0:00:00.250000, ' + 'buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], ' + 'isInitialized: true, ' + 'isPlaying: true, ' + 'isLooping: true, ' + 'isBuffering: true, ' + 'volume: 0.5, ' + 'playbackSpeed: 1.5, ' + 'errorDescription: null, ' + 'isCompleted: false),'); + }); - test('erroneous()', () { - const String errorMessage = 'foo'; - const VideoPlayerValue error = VideoPlayerValue.erroneous(errorMessage); - - expect(error.duration, equals(Duration.zero)); - expect(error.position, equals(Duration.zero)); - expect(error.caption, equals(Caption.none)); - expect(error.captionOffset, equals(Duration.zero)); - expect(error.buffered, isEmpty); - expect(error.isPlaying, isFalse); - expect(error.isLooping, isFalse); - expect(error.isBuffering, isFalse); - expect(error.volume, 1.0); - expect(error.playbackSpeed, 1.0); - expect(error.errorDescription, errorMessage); - expect(error.size, equals(Size.zero)); - expect(error.isInitialized, isFalse); - expect(error.hasError, isTrue); - expect(error.aspectRatio, 1.0); - }); + group('copyWith()', () { + test('exact copy', () { + const VideoPlayerValue original = VideoPlayerValue.uninitialized(); + final VideoPlayerValue exactCopy = original.copyWith(); - test('toString()', () { - const Duration duration = Duration(seconds: 5); - const Size size = Size(400, 300); - const Duration position = Duration(seconds: 1); - const Caption caption = Caption( - text: 'foo', number: 0, start: Duration.zero, end: Duration.zero); - const Duration captionOffset = Duration(milliseconds: 250); - final List buffered = [ - DurationRange(Duration.zero, const Duration(seconds: 4)) - ]; - const bool isInitialized = true; - const bool isPlaying = true; - const bool isLooping = true; - const bool isBuffering = true; - const double volume = 0.5; - const double playbackSpeed = 1.5; - - final VideoPlayerValue value = VideoPlayerValue( - duration: duration, - size: size, - position: position, - caption: caption, - captionOffset: captionOffset, - buffered: buffered, - isInitialized: isInitialized, - isPlaying: isPlaying, - isLooping: isLooping, - isBuffering: isBuffering, - volume: volume, - playbackSpeed: playbackSpeed, - ); + expect(exactCopy.toString(), original.toString()); + }); + test('errorDescription is not persisted when copy with null', () { + const VideoPlayerValue original = VideoPlayerValue.erroneous('error'); + final VideoPlayerValue copy = + original.copyWith(errorDescription: null); - expect( - value.toString(), - 'VideoPlayerValue(duration: 0:00:05.000000, ' - 'size: Size(400.0, 300.0), ' - 'position: 0:00:01.000000, ' - 'caption: Caption(number: 0, start: 0:00:00.000000, end: 0:00:00.000000, text: foo), ' - 'captionOffset: 0:00:00.250000, ' - 'buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], ' - 'isInitialized: true, ' - 'isPlaying: true, ' - 'isLooping: true, ' - 'isBuffering: true, ' - 'volume: 0.5, ' - 'playbackSpeed: 1.5, ' - 'errorDescription: null, ' - 'isCompleted: false),'); - }); + expect(copy.errorDescription, null); + }); + test('errorDescription is changed when copy with another error', () { + const VideoPlayerValue original = VideoPlayerValue.erroneous('error'); + final VideoPlayerValue copy = + original.copyWith(errorDescription: 'new error'); - group('copyWith()', () { - test('exact copy', () { - const VideoPlayerValue original = VideoPlayerValue.uninitialized(); - final VideoPlayerValue exactCopy = original.copyWith(); + expect(copy.errorDescription, 'new error'); + }); + test('errorDescription is changed when copy with error', () { + const VideoPlayerValue original = VideoPlayerValue.uninitialized(); + final VideoPlayerValue copy = + original.copyWith(errorDescription: 'new error'); - expect(exactCopy.toString(), original.toString()); + expect(copy.errorDescription, 'new error'); + }); }); - test('errorDescription is not persisted when copy with null', () { - const VideoPlayerValue original = VideoPlayerValue.erroneous('error'); - final VideoPlayerValue copy = original.copyWith(errorDescription: null); - expect(copy.errorDescription, null); - }); - test('errorDescription is changed when copy with another error', () { - const VideoPlayerValue original = VideoPlayerValue.erroneous('error'); - final VideoPlayerValue copy = - original.copyWith(errorDescription: 'new error'); + group('aspectRatio', () { + test('640x480 -> 4:3', () { + const VideoPlayerValue value = VideoPlayerValue( + isInitialized: true, + size: Size(640, 480), + duration: Duration(seconds: 1), + ); + expect(value.aspectRatio, 4 / 3); + }); - expect(copy.errorDescription, 'new error'); - }); - test('errorDescription is changed when copy with error', () { - const VideoPlayerValue original = VideoPlayerValue.uninitialized(); - final VideoPlayerValue copy = - original.copyWith(errorDescription: 'new error'); + test('no size -> 1.0', () { + const VideoPlayerValue value = VideoPlayerValue( + isInitialized: true, + duration: Duration(seconds: 1), + ); + expect(value.aspectRatio, 1.0); + }); - expect(copy.errorDescription, 'new error'); + test('height = 0 -> 1.0', () { + const VideoPlayerValue value = VideoPlayerValue( + isInitialized: true, + size: Size(640, 0), + duration: Duration(seconds: 1), + ); + expect(value.aspectRatio, 1.0); + }); + + test('width = 0 -> 1.0', () { + const VideoPlayerValue value = VideoPlayerValue( + isInitialized: true, + size: Size(0, 480), + duration: Duration(seconds: 1), + ); + expect(value.aspectRatio, 1.0); + }); + + test('negative aspect ratio -> 1.0', () { + const VideoPlayerValue value = VideoPlayerValue( + isInitialized: true, + size: Size(640, -480), + duration: Duration(seconds: 1), + ); + expect(value.aspectRatio, 1.0); + }); }); }); - group('aspectRatio', () { - test('640x480 -> 4:3', () { - const VideoPlayerValue value = VideoPlayerValue( - isInitialized: true, - size: Size(640, 480), - duration: Duration(seconds: 1), + group('VideoPlayerOptions', () { + test('setMixWithOthers', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), ); - expect(value.aspectRatio, 4 / 3); + addTearDown(controller.dispose); + + await controller.initialize(); + expect(controller.videoPlayerOptions!.mixWithOthers, true); }); - test('no size -> 1.0', () { - const VideoPlayerValue value = VideoPlayerValue( - isInitialized: true, - duration: Duration(seconds: 1), + test('true allowBackgroundPlayback continues playback', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + videoPlayerOptions: VideoPlayerOptions( + allowBackgroundPlayback: true, + ), ); - expect(value.aspectRatio, 1.0); - }); + addTearDown(controller.dispose); - test('height = 0 -> 1.0', () { - const VideoPlayerValue value = VideoPlayerValue( - isInitialized: true, - size: Size(640, 0), - duration: Duration(seconds: 1), + await controller.initialize(); + await controller.play(); + verifyPlayStateRespondsToLifecycle( + controller, + shouldPlayInBackground: true, ); - expect(value.aspectRatio, 1.0); }); - test('width = 0 -> 1.0', () { - const VideoPlayerValue value = VideoPlayerValue( - isInitialized: true, - size: Size(0, 480), - duration: Duration(seconds: 1), + test('false allowBackgroundPlayback pauses playback', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + videoPlayerOptions: VideoPlayerOptions(), ); - expect(value.aspectRatio, 1.0); - }); + addTearDown(controller.dispose); - test('negative aspect ratio -> 1.0', () { - const VideoPlayerValue value = VideoPlayerValue( - isInitialized: true, - size: Size(640, -480), - duration: Duration(seconds: 1), + await controller.initialize(); + await controller.play(); + verifyPlayStateRespondsToLifecycle( + controller, + shouldPlayInBackground: false, ); - expect(value.aspectRatio, 1.0); }); }); - }); - group('VideoPlayerOptions', () { - test('setMixWithOthers', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), - ); - addTearDown(controller.dispose); + test('VideoProgressColors', () { + const Color playedColor = Color.fromRGBO(0, 0, 255, 0.75); + const Color bufferedColor = Color.fromRGBO(0, 255, 0, 0.5); + const Color backgroundColor = Color.fromRGBO(255, 255, 0, 0.25); - await controller.initialize(); - expect(controller.videoPlayerOptions!.mixWithOthers, true); - }); - - test('true allowBackgroundPlayback continues playback', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - videoPlayerOptions: VideoPlayerOptions( - allowBackgroundPlayback: true, - ), - ); - addTearDown(controller.dispose); + const VideoProgressColors colors = VideoProgressColors( + playedColor: playedColor, + bufferedColor: bufferedColor, + backgroundColor: backgroundColor); - await controller.initialize(); - await controller.play(); - verifyPlayStateRespondsToLifecycle( - controller, - shouldPlayInBackground: true, - ); + expect(colors.playedColor, playedColor); + expect(colors.bufferedColor, bufferedColor); + expect(colors.backgroundColor, backgroundColor); }); - test('false allowBackgroundPlayback pauses playback', () async { + test('isCompleted updates on video end', () async { final VideoPlayerController controller = VideoPlayerController.networkUrl( _localhostUri, videoPlayerOptions: VideoPlayerOptions(), @@ -1364,125 +1403,95 @@ void main() { addTearDown(controller.dispose); await controller.initialize(); - await controller.play(); - verifyPlayStateRespondsToLifecycle( - controller, - shouldPlayInBackground: false, - ); - }); - }); - test('VideoProgressColors', () { - const Color playedColor = Color.fromRGBO(0, 0, 255, 0.75); - const Color bufferedColor = Color.fromRGBO(0, 255, 0, 0.5); - const Color backgroundColor = Color.fromRGBO(255, 255, 0, 0.25); + final StreamController fakeVideoEventStream = + fakeVideoPlayerPlatform.streams[controller.textureId]!; - const VideoProgressColors colors = VideoProgressColors( - playedColor: playedColor, - bufferedColor: bufferedColor, - backgroundColor: backgroundColor); + bool currentIsCompleted = controller.value.isCompleted; - expect(colors.playedColor, playedColor); - expect(colors.bufferedColor, bufferedColor); - expect(colors.backgroundColor, backgroundColor); - }); - - test('isCompleted updates on video end', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - videoPlayerOptions: VideoPlayerOptions(), - ); - addTearDown(controller.dispose); - - await controller.initialize(); - - final StreamController fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.textureId]!; - - bool currentIsCompleted = controller.value.isCompleted; + final void Function() isCompletedTest = expectAsync0(() {}); - final void Function() isCompletedTest = expectAsync0(() {}); - - controller.addListener(() async { - if (currentIsCompleted != controller.value.isCompleted) { - currentIsCompleted = controller.value.isCompleted; - if (controller.value.isCompleted) { - isCompletedTest(); + controller.addListener(() async { + if (currentIsCompleted != controller.value.isCompleted) { + currentIsCompleted = controller.value.isCompleted; + if (controller.value.isCompleted) { + isCompletedTest(); + } } - } - }); - - fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.completed)); - }); - - test('isCompleted updates on video play after completed', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - videoPlayerOptions: VideoPlayerOptions(), - ); - addTearDown(controller.dispose); - - await controller.initialize(); + }); - final StreamController fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.textureId]!; + fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.completed)); + }); - bool currentIsCompleted = controller.value.isCompleted; + test('isCompleted updates on video play after completed', () async { + final VideoPlayerController controller = VideoPlayerController.networkUrl( + _localhostUri, + videoPlayerOptions: VideoPlayerOptions(), + ); + addTearDown(controller.dispose); - final void Function() isCompletedTest = expectAsync0(() {}, count: 2); - final void Function() isNoLongerCompletedTest = expectAsync0(() {}); - bool hasLooped = false; + await controller.initialize(); - controller.addListener(() async { - if (currentIsCompleted != controller.value.isCompleted) { - currentIsCompleted = controller.value.isCompleted; - if (controller.value.isCompleted) { - isCompletedTest(); - if (!hasLooped) { - fakeVideoEventStream.add(VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: true)); - hasLooped = !hasLooped; + final StreamController fakeVideoEventStream = + fakeVideoPlayerPlatform.streams[controller.textureId]!; + + bool currentIsCompleted = controller.value.isCompleted; + + final void Function() isCompletedTest = expectAsync0(() {}, count: 2); + final void Function() isNoLongerCompletedTest = expectAsync0(() {}); + bool hasLooped = false; + + controller.addListener(() async { + if (currentIsCompleted != controller.value.isCompleted) { + currentIsCompleted = controller.value.isCompleted; + if (controller.value.isCompleted) { + isCompletedTest(); + if (!hasLooped) { + fakeVideoEventStream.add(VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: true)); + hasLooped = !hasLooped; + } + } else { + isNoLongerCompletedTest(); } - } else { - isNoLongerCompletedTest(); } - } - }); + }); - fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.completed)); - }); + fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.completed)); + }); - test('isCompleted updates on video seek to end', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - videoPlayerOptions: VideoPlayerOptions(), - ); - addTearDown(controller.dispose); + test('isCompleted updates on video seek to end', () async { + final VideoPlayerController controller = VideoPlayerController.networkUrl( + _localhostUri, + videoPlayerOptions: VideoPlayerOptions(), + ); + addTearDown(controller.dispose); - await controller.initialize(); + await controller.initialize(); - bool currentIsCompleted = controller.value.isCompleted; + bool currentIsCompleted = controller.value.isCompleted; - final void Function() isCompletedTest = expectAsync0(() {}); + final void Function() isCompletedTest = expectAsync0(() {}); - controller.value = - controller.value.copyWith(duration: const Duration(seconds: 10)); + controller.value = + controller.value.copyWith(duration: const Duration(seconds: 10)); - controller.addListener(() async { - if (currentIsCompleted != controller.value.isCompleted) { - currentIsCompleted = controller.value.isCompleted; - if (controller.value.isCompleted) { - isCompletedTest(); + controller.addListener(() async { + if (currentIsCompleted != controller.value.isCompleted) { + currentIsCompleted = controller.value.isCompleted; + if (controller.value.isCompleted) { + isCompletedTest(); + } } - } - }); + }); - // This call won't update isCompleted. - // The test will fail if `isCompletedTest` is called more than once. - await controller.seekTo(const Duration(seconds: 10)); + // This call won't update isCompleted. + // The test will fail if `isCompletedTest` is called more than once. + await controller.seekTo(const Duration(seconds: 10)); - await controller.seekTo(const Duration(seconds: 20)); + await controller.seekTo(const Duration(seconds: 20)); + }); }); } From 8401b89fefd664dc46d52b3300e9ba439ec8518f Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Wed, 14 May 2025 14:48:23 -0300 Subject: [PATCH 21/27] Refactor closed caption handling in VideoPlayerController - Removed the unused `_previousClosedCaptionFile` variable and the `_sortClosedCaption` method. - Updated the `_updateClosedCaptionWithFuture` method to handle null closed caption files and sort captions directly after loading. - Improved the logic for updating the caption state to ensure proper handling of closed captions during playback. --- .../video_player/lib/video_player.dart | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index e172d0f1d407..460789803b62 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -378,7 +378,6 @@ class VideoPlayerController extends ValueNotifier { Future? _closedCaptionFileFuture; ClosedCaptionFile? _closedCaptionFile; - ClosedCaptionFile? _previousClosedCaptionFile; List? _sortedCaptions; Timer? _timer; @@ -509,7 +508,6 @@ class VideoPlayerController extends ValueNotifier { if (_closedCaptionFileFuture != null) { await _updateClosedCaptionWithFuture(_closedCaptionFileFuture); - _sortClosedCaption(); } void errorListener(Object obj) { @@ -786,29 +784,25 @@ class VideoPlayerController extends ValueNotifier { ) async { await _updateClosedCaptionWithFuture(closedCaptionFile); _closedCaptionFileFuture = closedCaptionFile; - _sortClosedCaption(); - } - - void _sortClosedCaption() { - // Only sort if the file has changed - if (_previousClosedCaptionFile != _closedCaptionFile) { - _sortedCaptions = _closedCaptionFile?.captions; - - /// Sort the captions by start time to allow a binary search. - _sortedCaptions?.sort((Caption a, Caption b) { - return a.start.compareTo(b.start); - }); - - _previousClosedCaptionFile = _closedCaptionFile; - } } Future _updateClosedCaptionWithFuture( Future? closedCaptionFile, ) async { - _closedCaptionFile = await closedCaptionFile; - - value = value.copyWith(caption: _getCaptionAt(value.position)); + if (closedCaptionFile != null) { + await closedCaptionFile.whenComplete(() async { + _closedCaptionFile = await closedCaptionFile; + _sortedCaptions = _closedCaptionFile?.captions; + _sortedCaptions?.sort((Caption a, Caption b) { + return a.start.compareTo(b.start); + }); + value = value.copyWith(caption: _getCaptionAt(value.position)); + }); + } else { + _closedCaptionFile = null; + _sortedCaptions = null; + value = value.copyWith(caption: Caption.none); + } } void _updatePosition(Duration position) { From 9363c100ec80f1bc26252f465e4223dab4c834cb Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Sun, 23 Nov 2025 10:57:39 -0400 Subject: [PATCH 22/27] Refactor closed caption handling in VideoPlayerController for improved clarity and performance --- .../video_player/lib/video_player.dart | 47 +-- .../video_player/test/video_player_test.dart | 275 +++++++++--------- 2 files changed, 167 insertions(+), 155 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 460789803b62..0aede0ffed01 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -735,10 +735,15 @@ class VideoPlayerController extends ValueNotifier { return Caption.none; } + final List? sortedCaptions = _sortedCaptions; + if (sortedCaptions == null) { + return Caption.none; + } + final Duration delayedPosition = position + value.captionOffset; final int captionIndex = collection.binarySearch( - _sortedCaptions!, + sortedCaptions, Caption( number: -1, start: delayedPosition, @@ -762,13 +767,7 @@ class VideoPlayerController extends ValueNotifier { return Caption.none; } - final Caption caption = _sortedCaptions![captionIndex]; - // check if it really fits within that caption's [start, end]. - if (caption.start <= delayedPosition && caption.end >= delayedPosition) { - return caption; - } - - return Caption.none; // No matching caption found + return sortedCaptions[captionIndex]; } /// Returns the file containing closed captions for the video, if any. @@ -782,22 +781,36 @@ class VideoPlayerController extends ValueNotifier { Future setClosedCaptionFile( Future? closedCaptionFile, ) async { - await _updateClosedCaptionWithFuture(closedCaptionFile); _closedCaptionFileFuture = closedCaptionFile; + + if (closedCaptionFile != null) { + _closedCaptionFile = await closedCaptionFile; + // Always sort when explicitly setting a new caption file + _sortedCaptions = List.from(_closedCaptionFile!.captions) + ..sort((Caption a, Caption b) { + return a.start.compareTo(b.start); + }); + value = value.copyWith(caption: _getCaptionAt(value.position)); + } else { + _closedCaptionFile = null; + _sortedCaptions = null; + value = value.copyWith(caption: Caption.none); + } } Future _updateClosedCaptionWithFuture( Future? closedCaptionFile, ) async { if (closedCaptionFile != null) { - await closedCaptionFile.whenComplete(() async { - _closedCaptionFile = await closedCaptionFile; - _sortedCaptions = _closedCaptionFile?.captions; - _sortedCaptions?.sort((Caption a, Caption b) { - return a.start.compareTo(b.start); - }); - value = value.copyWith(caption: _getCaptionAt(value.position)); - }); + _closedCaptionFile = await closedCaptionFile; + + // Only sort if we haven't sorted yet (first initialization) + _sortedCaptions ??= List.from(_closedCaptionFile!.captions) + ..sort((Caption a, Caption b) { + return a.start.compareTo(b.start); + }); + + value = value.copyWith(caption: _getCaptionAt(value.position)); } else { _closedCaptionFile = null; _sortedCaptions = null; diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 29017c026685..3698b40fc8a1 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -808,33 +808,33 @@ void main() { expect(recordedCaptions[300], 'two'); }); - test('works when seeking', () async { - test('makes sure the input captions are unsorted', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); + test('makes sure the input captions are unsorted', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); - await controller.initialize(); - final List captions = - (await controller.closedCaptionFile)!.captions.toList(); - - // Check that captions are not in sorted order. - bool isSorted = true; - for (int i = 0; i < captions.length - 1; i++) { - if (captions[i].start.compareTo(captions[i + 1].start) > 0) { - isSorted = false; - break; - } + await controller.initialize(); + final List captions = + (await controller.closedCaptionFile)!.captions.toList(); + + // Check that captions are not in sorted order. + bool isSorted = true; + for (int i = 0; i < captions.length - 1; i++) { + if (captions[i].start.compareTo(captions[i + 1].start) > 0) { + isSorted = false; + break; } + } - expect(isSorted, false, reason: 'Expected captions to be unsorted'); - expect(captions.map((Caption c) => c.text).toList(), - ['one', 'two', 'three', 'five', 'four'], - reason: 'Captions should be in original unsorted order'); - }); - test('works when seeking, includes all captions', () async { + expect(isSorted, false, reason: 'Expected captions to be unsorted'); + expect(captions.map((Caption c) => c.text).toList(), + ['one', 'two', 'three', 'five', 'four'], + reason: 'Captions should be in original unsorted order'); + }); + + test('works when seeking, includes all captions', () async { final VideoPlayerController controller = VideoPlayerController.networkUrl( _localhostUri, @@ -973,132 +973,131 @@ void main() { await controller.seekTo(const Duration(milliseconds: 700)); expect(controller.value.caption.text, 'three'); - }); + }); - test('setClosedCaptionFile loads caption file', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - ); - addTearDown(controller.dispose); + test('setClosedCaptionFile loads caption file', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + ); + addTearDown(controller.dispose); - await controller.initialize(); - expect(controller.closedCaptionFile, null); + await controller.initialize(); + expect(controller.closedCaptionFile, null); - await controller.setClosedCaptionFile(_loadClosedCaption()); - expect( - (await controller.closedCaptionFile)!.captions, - (await _loadClosedCaption()).captions, - ); - }); + await controller.setClosedCaptionFile(_loadClosedCaption()); + expect( + (await controller.closedCaptionFile)!.captions, + (await _loadClosedCaption()).captions, + ); + }); - test('setClosedCaptionFile removes/changes caption file', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); - addTearDown(controller.dispose); + test('setClosedCaptionFile removes/changes caption file', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); + addTearDown(controller.dispose); - await controller.initialize(); - expect( - (await controller.closedCaptionFile)!.captions, - (await _loadClosedCaption()).captions, - ); + await controller.initialize(); + expect( + (await controller.closedCaptionFile)!.captions, + (await _loadClosedCaption()).captions, + ); - await controller.setClosedCaptionFile(null); - expect(controller.closedCaptionFile, null); - }); + await controller.setClosedCaptionFile(null); + expect(controller.closedCaptionFile, null); + }); + }); + + group('Platform callbacks', () { + testWidgets('playing completed', (WidgetTester tester) async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + ); + + await controller.initialize(); + const Duration nonzeroDuration = Duration(milliseconds: 100); + controller.value = + controller.value.copyWith(duration: nonzeroDuration); + expect(controller.value.isPlaying, isFalse); + await controller.play(); + expect(controller.value.isPlaying, isTrue); + final StreamController fakeVideoEventStream = + fakeVideoPlayerPlatform.streams[controller.textureId]!; + + fakeVideoEventStream + .add(VideoEvent(eventType: VideoEventType.completed)); + await tester.pumpAndSettle(); + + expect(controller.value.isPlaying, isFalse); + expect(controller.value.position, nonzeroDuration); + await tester.runAsync(controller.dispose); }); - group('Platform callbacks', () { - testWidgets('playing completed', (WidgetTester tester) async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - ); + testWidgets('playback status', (WidgetTester tester) async { + final VideoPlayerController controller = + VideoPlayerController.network( + 'https://.0.0.1', + ); + await controller.initialize(); + expect(controller.value.isPlaying, isFalse); + final StreamController fakeVideoEventStream = + fakeVideoPlayerPlatform.streams[controller.textureId]!; - await controller.initialize(); - const Duration nonzeroDuration = Duration(milliseconds: 100); - controller.value = - controller.value.copyWith(duration: nonzeroDuration); - expect(controller.value.isPlaying, isFalse); - await controller.play(); - expect(controller.value.isPlaying, isTrue); - final StreamController fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.textureId]!; - - fakeVideoEventStream - .add(VideoEvent(eventType: VideoEventType.completed)); - await tester.pumpAndSettle(); - - expect(controller.value.isPlaying, isFalse); - expect(controller.value.position, nonzeroDuration); - await tester.runAsync(controller.dispose); - }); + fakeVideoEventStream.add(VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: true, + )); + await tester.pumpAndSettle(); + expect(controller.value.isPlaying, isTrue); - testWidgets('playback status', (WidgetTester tester) async { - final VideoPlayerController controller = - VideoPlayerController.network( - 'https://.0.0.1', - ); - await controller.initialize(); - expect(controller.value.isPlaying, isFalse); - final StreamController fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.textureId]!; - - fakeVideoEventStream.add(VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: true, - )); - await tester.pumpAndSettle(); - expect(controller.value.isPlaying, isTrue); - - fakeVideoEventStream.add(VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: false, - )); - await tester.pumpAndSettle(); - expect(controller.value.isPlaying, isFalse); - await tester.runAsync(controller.dispose); - }); + fakeVideoEventStream.add(VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: false, + )); + await tester.pumpAndSettle(); + expect(controller.value.isPlaying, isFalse); + await tester.runAsync(controller.dispose); + }); - testWidgets('buffering status', (WidgetTester tester) async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - ); + testWidgets('buffering status', (WidgetTester tester) async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + ); - await controller.initialize(); - expect(controller.value.isBuffering, false); - expect(controller.value.buffered, isEmpty); - final StreamController fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.textureId]!; - - fakeVideoEventStream - .add(VideoEvent(eventType: VideoEventType.bufferingStart)); - await tester.pumpAndSettle(); - expect(controller.value.isBuffering, isTrue); - - const Duration bufferStart = Duration.zero; - const Duration bufferEnd = Duration(milliseconds: 500); - fakeVideoEventStream.add(VideoEvent( - eventType: VideoEventType.bufferingUpdate, - buffered: [ - DurationRange(bufferStart, bufferEnd), - ])); - await tester.pumpAndSettle(); - expect(controller.value.isBuffering, isTrue); - expect(controller.value.buffered.length, 1); - expect(controller.value.buffered[0].toString(), - DurationRange(bufferStart, bufferEnd).toString()); - - fakeVideoEventStream - .add(VideoEvent(eventType: VideoEventType.bufferingEnd)); - await tester.pumpAndSettle(); - expect(controller.value.isBuffering, isFalse); - await tester.runAsync(controller.dispose); - }); + await controller.initialize(); + expect(controller.value.isBuffering, false); + expect(controller.value.buffered, isEmpty); + final StreamController fakeVideoEventStream = + fakeVideoPlayerPlatform.streams[controller.textureId]!; + + fakeVideoEventStream + .add(VideoEvent(eventType: VideoEventType.bufferingStart)); + await tester.pumpAndSettle(); + expect(controller.value.isBuffering, isTrue); + + const Duration bufferStart = Duration.zero; + const Duration bufferEnd = Duration(milliseconds: 500); + fakeVideoEventStream.add(VideoEvent( + eventType: VideoEventType.bufferingUpdate, + buffered: [ + DurationRange(bufferStart, bufferEnd), + ])); + await tester.pumpAndSettle(); + expect(controller.value.isBuffering, isTrue); + expect(controller.value.buffered.length, 1); + expect(controller.value.buffered[0].toString(), + DurationRange(bufferStart, bufferEnd).toString()); + + fakeVideoEventStream + .add(VideoEvent(eventType: VideoEventType.bufferingEnd)); + await tester.pumpAndSettle(); + expect(controller.value.isBuffering, isFalse); + await tester.runAsync(controller.dispose); }); }); From 6e2340d6c83d3d8348b022262dce094efe60254a Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Sun, 23 Nov 2025 11:06:14 -0400 Subject: [PATCH 23/27] Refactor caption sorting and enhance test coverage for binary search in VideoPlayerController --- .../video_player/lib/video_player.dart | 6 +- .../video_player/test/video_player_test.dart | 655 +++++++++++++----- 2 files changed, 487 insertions(+), 174 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index c8de4a4e7f78..9b93aeb170c3 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -838,9 +838,9 @@ class VideoPlayerController extends ValueNotifier { // Only sort if we haven't sorted yet (first initialization) _sortedCaptions ??= List.from(_closedCaptionFile!.captions) - ..sort((Caption a, Caption b) { - return a.start.compareTo(b.start); - }); + ..sort((Caption a, Caption b) { + return a.start.compareTo(b.start); + }); value = value.copyWith(caption: _getCaptionAt(value.position)); } else { diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 246ad89dba99..e4274adfb086 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -131,6 +131,40 @@ class _FakeClosedCaptionFile extends ClosedCaptionFile { } } +class _SingleCaptionFile extends ClosedCaptionFile { + @override + List get captions { + return [ + const Caption( + text: 'only', + number: 0, + start: Duration(milliseconds: 100), + end: Duration(milliseconds: 200), + ), + ]; + } +} + +class _OverlappingCaptionFile extends ClosedCaptionFile { + @override + List get captions { + return [ + const Caption( + text: 'first', + number: 0, + start: Duration(milliseconds: 100), + end: Duration(milliseconds: 300), + ), + const Caption( + text: 'second', + number: 1, + start: Duration(milliseconds: 200), + end: Duration(milliseconds: 400), + ), + ]; + } +} + void main() { late FakeVideoPlayerPlatform fakeVideoPlayerPlatform; @@ -916,13 +950,14 @@ void main() { test('makes sure the input captions are unsorted', () async { final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); await controller.initialize(); - final List captions = - (await controller.closedCaptionFile)!.captions.toList(); + final List captions = (await controller.closedCaptionFile)! + .captions + .toList(); // Check that captions are not in sorted order. bool isSorted = true; @@ -934,68 +969,70 @@ void main() { } expect(isSorted, false, reason: 'Expected captions to be unsorted'); - expect(captions.map((Caption c) => c.text).toList(), - ['one', 'two', 'three', 'five', 'four'], - reason: 'Captions should be in original unsorted order'); + expect( + captions.map((Caption c) => c.text).toList(), + ['one', 'two', 'three', 'five', 'four'], + reason: 'Captions should be in original unsorted order', + ); }); test('works when seeking, includes all captions', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); - addTearDown(controller.dispose); + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); + addTearDown(controller.dispose); - await controller.initialize(); - expect(controller.value.position, Duration.zero); - expect(controller.value.caption.text, ''); + await controller.initialize(); + expect(controller.value.position, Duration.zero); + expect(controller.value.caption.text, ''); - await controller.seekTo(const Duration(milliseconds: 100)); - expect(controller.value.caption.text, 'one'); + await controller.seekTo(const Duration(milliseconds: 100)); + expect(controller.value.caption.text, 'one'); - await controller.seekTo(const Duration(milliseconds: 250)); - expect(controller.value.caption.text, ''); + await controller.seekTo(const Duration(milliseconds: 250)); + expect(controller.value.caption.text, ''); - await controller.seekTo(const Duration(milliseconds: 300)); - expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 300)); + expect(controller.value.caption.text, 'two'); - await controller.seekTo(const Duration(milliseconds: 301)); - expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 301)); + expect(controller.value.caption.text, 'two'); - await controller.seekTo(const Duration(milliseconds: 400)); - expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 400)); + expect(controller.value.caption.text, 'two'); - await controller.seekTo(const Duration(milliseconds: 401)); - expect(controller.value.caption.text, ''); + await controller.seekTo(const Duration(milliseconds: 401)); + expect(controller.value.caption.text, ''); - await controller.seekTo(const Duration(milliseconds: 500)); - expect(controller.value.caption.text, 'three'); + await controller.seekTo(const Duration(milliseconds: 500)); + expect(controller.value.caption.text, 'three'); - await controller.seekTo(const Duration(milliseconds: 601)); - expect(controller.value.caption.text, 'four'); + await controller.seekTo(const Duration(milliseconds: 601)); + expect(controller.value.caption.text, 'four'); - await controller.seekTo(const Duration(milliseconds: 701)); - expect(controller.value.caption.text, 'five'); + await controller.seekTo(const Duration(milliseconds: 701)); + expect(controller.value.caption.text, 'five'); - await controller.seekTo(const Duration(milliseconds: 800)); - expect(controller.value.caption.text, 'five'); - await controller.seekTo(const Duration(milliseconds: 801)); - expect(controller.value.caption.text, ''); + await controller.seekTo(const Duration(milliseconds: 800)); + expect(controller.value.caption.text, 'five'); + await controller.seekTo(const Duration(milliseconds: 801)); + expect(controller.value.caption.text, ''); - // Test going back - await controller.seekTo(const Duration(milliseconds: 300)); - expect(controller.value.caption.text, 'two'); - }); + // Test going back + await controller.seekTo(const Duration(milliseconds: 300)); + expect(controller.value.caption.text, 'two'); + }); - test( - 'works when seeking with captionOffset positive, includes all captions', - () async { + test( + 'works when seeking with captionOffset positive, includes all captions', + () async { final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -1035,16 +1072,17 @@ void main() { await controller.seekTo(const Duration(milliseconds: 800)); expect(controller.value.caption.text, ''); - }); + }, + ); - test( - 'works when seeking with captionOffset negative, includes all captions', - () async { + test( + 'works when seeking with captionOffset negative, includes all captions', + () async { final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -1078,7 +1116,8 @@ void main() { await controller.seekTo(const Duration(milliseconds: 700)); expect(controller.value.caption.text, 'three'); - }); + }, + ); test('setClosedCaptionFile loads caption file', () async { final VideoPlayerController controller = @@ -1112,6 +1151,274 @@ void main() { await controller.setClosedCaptionFile(null); expect(controller.closedCaptionFile, null); }); + + test('binary search handles exact caption start time boundary', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); + addTearDown(controller.dispose); + + await controller.initialize(); + + // Seek to exact start times - should find the caption + await controller.seekTo(const Duration(milliseconds: 100)); + expect( + controller.value.caption.text, + 'one', + reason: 'Should find caption at exact start time (100ms)', + ); + + await controller.seekTo(const Duration(milliseconds: 300)); + expect( + controller.value.caption.text, + 'two', + reason: 'Should find caption at exact start time (300ms)', + ); + + await controller.seekTo(const Duration(milliseconds: 500)); + expect( + controller.value.caption.text, + 'three', + reason: 'Should find caption at exact start time (500ms)', + ); + + // At 600ms, "three" ends and "four" starts - binary search may find either + await controller.seekTo(const Duration(milliseconds: 600)); + expect( + ['three', 'four'].contains(controller.value.caption.text), + true, + reason: + 'Should find a caption at boundary (600ms) where two captions meet (got "${controller.value.caption.text}")', + ); + + await controller.seekTo(const Duration(milliseconds: 700)); + expect( + controller.value.caption.text, + 'five', + reason: 'Should find caption at exact start time (700ms)', + ); + }); + + test('binary search handles exact caption end time boundary', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); + addTearDown(controller.dispose); + + await controller.initialize(); + + // Seek to exact end times - should still find the caption + await controller.seekTo(const Duration(milliseconds: 200)); + expect( + controller.value.caption.text, + 'one', + reason: 'Should find caption at exact end time (200ms)', + ); + + await controller.seekTo(const Duration(milliseconds: 400)); + expect( + controller.value.caption.text, + 'two', + reason: 'Should find caption at exact end time (400ms)', + ); + + // At 600ms boundary where "three" ends and "four" starts + await controller.seekTo(const Duration(milliseconds: 600)); + expect( + ['three', 'four'].contains(controller.value.caption.text), + true, + reason: + 'Should find a caption at boundary (600ms) (got "${controller.value.caption.text}")', + ); + + // At 700ms boundary where "four" ends and "five" starts + await controller.seekTo(const Duration(milliseconds: 700)); + expect( + ['four', 'five'].contains(controller.value.caption.text), + true, + reason: + 'Should find a caption at boundary (700ms) (got "${controller.value.caption.text}")', + ); + + await controller.seekTo(const Duration(milliseconds: 800)); + expect( + controller.value.caption.text, + 'five', + reason: 'Should find caption at exact end time (800ms)', + ); + + // One millisecond past the end should not find the caption + await controller.seekTo(const Duration(milliseconds: 201)); + expect( + controller.value.caption.text, + '', + reason: + 'Should not find caption one millisecond past end time (201ms)', + ); + + await controller.seekTo(const Duration(milliseconds: 801)); + expect( + controller.value.caption.text, + '', + reason: + 'Should not find caption one millisecond past end time (801ms)', + ); + }); + + test('binary search handles gaps between captions', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); + addTearDown(controller.dispose); + + await controller.initialize(); + + // Test gaps between captions where no caption should be found + // Gap before first caption + await controller.seekTo(const Duration(milliseconds: 0)); + expect( + controller.value.caption.text, + '', + reason: 'Should return empty for position before first caption', + ); + + await controller.seekTo(const Duration(milliseconds: 99)); + expect( + controller.value.caption.text, + '', + reason: 'Should return empty for position before first caption', + ); + + // Gap between caption 1 (ends at 200) and caption 2 (starts at 300) + await controller.seekTo(const Duration(milliseconds: 250)); + expect( + controller.value.caption.text, + '', + reason: 'Should return empty for gap between captions 1 and 2', + ); + + // Gap between caption 2 (ends at 400) and caption 3 (starts at 500) + await controller.seekTo(const Duration(milliseconds: 450)); + expect( + controller.value.caption.text, + '', + reason: 'Should return empty for gap between captions 2 and 3', + ); + + // Gap after last caption + await controller.seekTo(const Duration(milliseconds: 900)); + expect( + controller.value.caption.text, + '', + reason: 'Should return empty for position after last caption', + ); + }); + + test('binary search works with single caption', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: Future.value( + _SingleCaptionFile(), + ), + ); + addTearDown(controller.dispose); + + await controller.initialize(); + + // Before caption + await controller.seekTo(const Duration(milliseconds: 99)); + expect( + controller.value.caption.text, + '', + reason: 'Should return empty before single caption', + ); + + // At start + await controller.seekTo(const Duration(milliseconds: 100)); + expect( + controller.value.caption.text, + 'only', + reason: 'Should find single caption at start', + ); + + // In middle + await controller.seekTo(const Duration(milliseconds: 150)); + expect( + controller.value.caption.text, + 'only', + reason: 'Should find single caption in middle', + ); + + // At end + await controller.seekTo(const Duration(milliseconds: 200)); + expect( + controller.value.caption.text, + 'only', + reason: 'Should find single caption at end', + ); + + // After caption + await controller.seekTo(const Duration(milliseconds: 201)); + expect( + controller.value.caption.text, + '', + reason: 'Should return empty after single caption', + ); + }); + + test('binary search handles overlapping captions', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: Future.value( + _OverlappingCaptionFile(), + ), + ); + addTearDown(controller.dispose); + + await controller.initialize(); + + // In first caption only + await controller.seekTo(const Duration(milliseconds: 100)); + expect( + controller.value.caption.text, + 'first', + reason: 'Should find first caption', + ); + + // In overlapping region - binary search should find one of them + // (the exact one depends on sort order, but it should find something) + await controller.seekTo(const Duration(milliseconds: 250)); + expect( + ['first', 'second'].contains(controller.value.caption.text), + true, + reason: + 'Should find a caption in overlapping region (got "${controller.value.caption.text}")', + ); + + // In second caption only + await controller.seekTo(const Duration(milliseconds: 350)); + expect( + controller.value.caption.text, + 'second', + reason: 'Should find second caption', + ); + + // After all captions + await controller.seekTo(const Duration(milliseconds: 401)); + expect( + controller.value.caption.text, + '', + reason: 'Should return empty after all captions', + ); + }); }); group('Platform callbacks', () { @@ -1121,8 +1428,7 @@ void main() { await controller.initialize(); const Duration nonzeroDuration = Duration(milliseconds: 100); - controller.value = - controller.value.copyWith(duration: nonzeroDuration); + controller.value = controller.value.copyWith(duration: nonzeroDuration); expect(controller.value.isPlaying, isFalse); await controller.play(); expect(controller.value.isPlaying, isTrue); @@ -1140,8 +1446,7 @@ void main() { }); testWidgets('playback status', (WidgetTester tester) async { - final VideoPlayerController controller = - VideoPlayerController.network( + final VideoPlayerController controller = VideoPlayerController.network( 'https://.0.0.1', ); await controller.initialize(); @@ -1223,29 +1528,33 @@ void main() { final List positions = []; final Completer intervalUpdateCompleter = Completer(); - // Listen for position updates - controller.addListener(() { - positions.add(controller.value.position); - if (positions.length >= 3 && !intervalUpdateCompleter.isCompleted) { - intervalUpdateCompleter.complete(); + // Listen for position updates + controller.addListener(() { + positions.add(controller.value.position); + if (positions.length >= 3 && !intervalUpdateCompleter.isCompleted) { + intervalUpdateCompleter.complete(); + } + }); + await controller.play(); + for (int i = 0; i < 3; i++) { + await Future.delayed(updatesInterval); + fakeVideoPlayerPlatform._positions[controller.playerId] = Duration( + milliseconds: i * updatesInterval.inMilliseconds, + ); } - }); - await controller.play(); - for (int i = 0; i < 3; i++) { - await Future.delayed(updatesInterval); - fakeVideoPlayerPlatform._positions[controller.playerId] = Duration( - milliseconds: i * updatesInterval.inMilliseconds, - ); - } // Wait for at least 3 position updates await intervalUpdateCompleter.future; // Verify that the intervals between updates are approximately correct expect( - positions[1] - positions[0], greaterThanOrEqualTo(updatesInterval)); + positions[1] - positions[0], + greaterThanOrEqualTo(updatesInterval), + ); expect( - positions[2] - positions[1], greaterThanOrEqualTo(updatesInterval)); + positions[2] - positions[1], + greaterThanOrEqualTo(updatesInterval), + ); }); group('DurationRange', () { @@ -1314,26 +1623,26 @@ void main() { expect(error.aspectRatio, 1.0); }); - test('toString()', () { - const Duration duration = Duration(seconds: 5); - const Size size = Size(400, 300); - const Duration position = Duration(seconds: 1); - const Caption caption = Caption( - text: 'foo', - number: 0, - start: Duration.zero, - end: Duration.zero, - ); - const Duration captionOffset = Duration(milliseconds: 250); - final List buffered = [ - DurationRange(Duration.zero, const Duration(seconds: 4)), - ]; - const bool isInitialized = true; - const bool isPlaying = true; - const bool isLooping = true; - const bool isBuffering = true; - const double volume = 0.5; - const double playbackSpeed = 1.5; + test('toString()', () { + const Duration duration = Duration(seconds: 5); + const Size size = Size(400, 300); + const Duration position = Duration(seconds: 1); + const Caption caption = Caption( + text: 'foo', + number: 0, + start: Duration.zero, + end: Duration.zero, + ); + const Duration captionOffset = Duration(milliseconds: 250); + final List buffered = [ + DurationRange(Duration.zero, const Duration(seconds: 4)), + ]; + const bool isInitialized = true; + const bool isPlaying = true; + const bool isLooping = true; + const bool isBuffering = true; + const double volume = 0.5; + const double playbackSpeed = 1.5; final VideoPlayerValue value = VideoPlayerValue( duration: duration, @@ -1350,24 +1659,24 @@ void main() { playbackSpeed: playbackSpeed, ); - expect( - value.toString(), - 'VideoPlayerValue(duration: 0:00:05.000000, ' - 'size: Size(400.0, 300.0), ' - 'position: 0:00:01.000000, ' - 'caption: Caption(number: 0, start: 0:00:00.000000, end: 0:00:00.000000, text: foo), ' - 'captionOffset: 0:00:00.250000, ' - 'buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], ' - 'isInitialized: true, ' - 'isPlaying: true, ' - 'isLooping: true, ' - 'isBuffering: true, ' - 'volume: 0.5, ' - 'playbackSpeed: 1.5, ' - 'errorDescription: null, ' - 'isCompleted: false),', - ); - }); + expect( + value.toString(), + 'VideoPlayerValue(duration: 0:00:05.000000, ' + 'size: Size(400.0, 300.0), ' + 'position: 0:00:01.000000, ' + 'caption: Caption(number: 0, start: 0:00:00.000000, end: 0:00:00.000000, text: foo), ' + 'captionOffset: 0:00:00.250000, ' + 'buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], ' + 'isInitialized: true, ' + 'isPlaying: true, ' + 'isLooping: true, ' + 'isBuffering: true, ' + 'volume: 0.5, ' + 'playbackSpeed: 1.5, ' + 'errorDescription: null, ' + 'isCompleted: false),', + ); + }); group('copyWith()', () { test('exact copy', () { @@ -1378,24 +1687,25 @@ void main() { }); test('errorDescription is not persisted when copy with null', () { const VideoPlayerValue original = VideoPlayerValue.erroneous('error'); - final VideoPlayerValue copy = - original.copyWith(errorDescription: null); + final VideoPlayerValue copy = original.copyWith( + errorDescription: null, + ); - expect(copy.errorDescription, null); - }); - test('errorDescription is changed when copy with another error', () { - const VideoPlayerValue original = VideoPlayerValue.erroneous('error'); - final VideoPlayerValue copy = original.copyWith( - errorDescription: 'new error', - ); + expect(copy.errorDescription, null); + }); + test('errorDescription is changed when copy with another error', () { + const VideoPlayerValue original = VideoPlayerValue.erroneous('error'); + final VideoPlayerValue copy = original.copyWith( + errorDescription: 'new error', + ); - expect(copy.errorDescription, 'new error'); - }); - test('errorDescription is changed when copy with error', () { - const VideoPlayerValue original = VideoPlayerValue.uninitialized(); - final VideoPlayerValue copy = original.copyWith( - errorDescription: 'new error', - ); + expect(copy.errorDescription, 'new error'); + }); + test('errorDescription is changed when copy with error', () { + const VideoPlayerValue original = VideoPlayerValue.uninitialized(); + final VideoPlayerValue copy = original.copyWith( + errorDescription: 'new error', + ); expect(copy.errorDescription, 'new error'); }); @@ -1452,21 +1762,24 @@ void main() { test('setMixWithOthers', () async { final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), - ); + _localhostUri, + videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), + ); addTearDown(controller.dispose); await controller.initialize(); expect(controller.videoPlayerOptions!.mixWithOthers, true); }); - test('true allowBackgroundPlayback continues playback', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - videoPlayerOptions: VideoPlayerOptions(allowBackgroundPlayback: true), - ); - addTearDown(controller.dispose); + test('true allowBackgroundPlayback continues playback', () async { + final VideoPlayerController controller = + VideoPlayerController.networkUrl( + _localhostUri, + videoPlayerOptions: VideoPlayerOptions( + allowBackgroundPlayback: true, + ), + ); + addTearDown(controller.dispose); await controller.initialize(); await controller.play(); @@ -1479,9 +1792,9 @@ void main() { test('false allowBackgroundPlayback pauses playback', () async { final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - videoPlayerOptions: VideoPlayerOptions(), - ); + _localhostUri, + videoPlayerOptions: VideoPlayerOptions(), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -1498,11 +1811,11 @@ void main() { const Color bufferedColor = Color.fromRGBO(0, 255, 0, 0.5); const Color backgroundColor = Color.fromRGBO(255, 255, 0, 0.25); - const VideoProgressColors colors = VideoProgressColors( - playedColor: playedColor, - bufferedColor: bufferedColor, - backgroundColor: backgroundColor, - ); + const VideoProgressColors colors = VideoProgressColors( + playedColor: playedColor, + bufferedColor: bufferedColor, + backgroundColor: backgroundColor, + ); expect(colors.playedColor, playedColor); expect(colors.bufferedColor, bufferedColor); @@ -1518,8 +1831,8 @@ void main() { await controller.initialize(); - final StreamController fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.playerId]!; + final StreamController fakeVideoEventStream = + fakeVideoPlayerPlatform.streams[controller.playerId]!; bool currentIsCompleted = controller.value.isCompleted; @@ -1546,8 +1859,8 @@ void main() { await controller.initialize(); - final StreamController fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.playerId]!; + final StreamController fakeVideoEventStream = + fakeVideoPlayerPlatform.streams[controller.playerId]!; bool currentIsCompleted = controller.value.isCompleted; @@ -1555,25 +1868,25 @@ void main() { final void Function() isNoLongerCompletedTest = expectAsync0(() {}); bool hasLooped = false; - controller.addListener(() async { - if (currentIsCompleted != controller.value.isCompleted) { - currentIsCompleted = controller.value.isCompleted; - if (controller.value.isCompleted) { - isCompletedTest(); - if (!hasLooped) { - fakeVideoEventStream.add( - VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: true, - ), - ); - hasLooped = !hasLooped; + controller.addListener(() async { + if (currentIsCompleted != controller.value.isCompleted) { + currentIsCompleted = controller.value.isCompleted; + if (controller.value.isCompleted) { + isCompletedTest(); + if (!hasLooped) { + fakeVideoEventStream.add( + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: true, + ), + ); + hasLooped = !hasLooped; + } + } else { + isNoLongerCompletedTest(); } - } else { - isNoLongerCompletedTest(); } - } - }); + }); fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.completed)); }); @@ -1591,9 +1904,9 @@ void main() { final void Function() isCompletedTest = expectAsync0(() {}); - controller.value = controller.value.copyWith( - duration: const Duration(seconds: 10), - ); + controller.value = controller.value.copyWith( + duration: const Duration(seconds: 10), + ); controller.addListener(() async { if (currentIsCompleted != controller.value.isCompleted) { From 862163cd8c4a8353d694cc9158cd7606376d4640 Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Sun, 23 Nov 2025 11:06:45 -0400 Subject: [PATCH 24/27] Refactor seekTo argument to use Duration.zero for clarity in video player tests --- packages/video_player/video_player/test/video_player_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index e4274adfb086..78765ceb4d0b 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -1281,7 +1281,7 @@ void main() { // Test gaps between captions where no caption should be found // Gap before first caption - await controller.seekTo(const Duration(milliseconds: 0)); + await controller.seekTo(Duration.zero); expect( controller.value.caption.text, '', From b802527ca933467a1b31967681c42acd68a6265f Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Sun, 23 Nov 2025 11:14:50 -0400 Subject: [PATCH 25/27] Bump version to 2.10.2 and update changelog for SDK and caption optimization --- packages/video_player/video_player/CHANGELOG.md | 2 +- packages/video_player/video_player/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 4ce10444b3d0..0596903ff549 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,4 +1,4 @@ -## NEXT +## 2.10.2 * Updates minimum supported SDK version to Flutter 3.32/Dart 3.8. * Optimizes caption retrieval with binary search. diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 8273f10e1621..6db3e51989f0 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, macOS and web. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.10.1 +version: 2.10.2 environment: sdk: ^3.8.0 From 81ab6911e82d5f2b666c3085d87b2d40dcd375f5 Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Fri, 9 Jan 2026 19:08:57 -0400 Subject: [PATCH 26/27] Refactor caption handling in VideoPlayerController to simplify logic and improve clarity, and address comments --- .../video_player/lib/video_player.dart | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 9b93aeb170c3..6e69f116e9c5 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -763,12 +763,8 @@ class VideoPlayerController extends ValueNotifier { /// [Caption]. Caption _getCaptionAt(Duration position) { - if (_closedCaptionFile == null || _sortedCaptions == null) { - return Caption.none; - } - final List? sortedCaptions = _sortedCaptions; - if (sortedCaptions == null) { + if (_closedCaptionFile == null || sortedCaptions == null) { return Caption.none; } @@ -814,20 +810,9 @@ class VideoPlayerController extends ValueNotifier { Future? closedCaptionFile, ) async { _closedCaptionFileFuture = closedCaptionFile; - - if (closedCaptionFile != null) { - _closedCaptionFile = await closedCaptionFile; - // Always sort when explicitly setting a new caption file - _sortedCaptions = List.from(_closedCaptionFile!.captions) - ..sort((Caption a, Caption b) { - return a.start.compareTo(b.start); - }); - value = value.copyWith(caption: _getCaptionAt(value.position)); - } else { - _closedCaptionFile = null; - _sortedCaptions = null; - value = value.copyWith(caption: Caption.none); - } + // Reset sorted captions to force re-sort when setting a new file + _sortedCaptions = null; + await _updateClosedCaptionWithFuture(closedCaptionFile); } Future _updateClosedCaptionWithFuture( From 6634a44f1ca21361cbc6a578c953c91e3faecf77 Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Fri, 9 Jan 2026 19:36:42 -0400 Subject: [PATCH 27/27] Refactor variable declarations in video_player_test.dart to use type inference for improved readability --- .../video_player/test/video_player_test.dart | 413 ++++++++---------- 1 file changed, 180 insertions(+), 233 deletions(-) diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 78765ceb4d0b..8c0b2ec8d9fa 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -189,7 +189,7 @@ void main() { } testWidgets('update texture', (WidgetTester tester) async { - final FakeController controller = FakeController(); + final controller = FakeController(); addTearDown(controller.dispose); await tester.pumpWidget(VideoPlayer(controller)); expect(find.byType(Texture), findsNothing); @@ -205,7 +205,7 @@ void main() { }); testWidgets('update controller', (WidgetTester tester) async { - final FakeController controller1 = FakeController(); + final controller1 = FakeController(); addTearDown(controller1.dispose); controller1.playerId = 101; await tester.pumpWidget(VideoPlayer(controller1)); @@ -216,7 +216,7 @@ void main() { findsOneWidget, ); - final FakeController controller2 = FakeController(); + final controller2 = FakeController(); addTearDown(controller2.dispose); controller2.playerId = 102; await tester.pumpWidget(VideoPlayer(controller2)); @@ -231,7 +231,7 @@ void main() { testWidgets( 'VideoPlayer still listens for controller changes when reparented', (WidgetTester tester) async { - final FakeController controller = FakeController(); + final controller = FakeController(); addTearDown(controller.dispose); final GlobalKey videoKey = GlobalKey(); final Widget videoPlayer = KeyedSubtree( @@ -264,7 +264,7 @@ void main() { testWidgets( 'VideoProgressIndicator still listens for controller changes after reparenting', (WidgetTester tester) async { - final FakeController controller = FakeController(); + final controller = FakeController(); addTearDown(controller.dispose); final GlobalKey key = GlobalKey(); final Widget progressIndicator = VideoProgressIndicator( @@ -295,7 +295,7 @@ void main() { testWidgets('VideoPlayer does not crash after loading 0-duration videos', ( WidgetTester tester, ) async { - final FakeController controller = FakeController(); + final controller = FakeController(); addTearDown(controller.dispose); controller.value = controller.value.copyWith( duration: Duration.zero, @@ -312,13 +312,13 @@ void main() { testWidgets('non-zero rotationCorrection value is used', ( WidgetTester tester, ) async { - final FakeController controller = FakeController.value( + final controller = FakeController.value( const VideoPlayerValue(duration: Duration.zero, rotationCorrection: 180), ); addTearDown(controller.dispose); controller.playerId = 1; await tester.pumpWidget(VideoPlayer(controller)); - final RotatedBox actualRotationCorrection = + final actualRotationCorrection = find.byType(RotatedBox).evaluate().single.widget as RotatedBox; final int actualQuarterTurns = actualRotationCorrection.quarterTurns; expect(actualQuarterTurns, equals(2)); @@ -327,7 +327,7 @@ void main() { testWidgets('no RotatedBox when rotationCorrection is zero', ( WidgetTester tester, ) async { - final FakeController controller = FakeController.value( + final controller = FakeController.value( const VideoPlayerValue(duration: Duration.zero), ); addTearDown(controller.dispose); @@ -338,7 +338,7 @@ void main() { group('ClosedCaption widget', () { testWidgets('uses a default text style', (WidgetTester tester) async { - const String text = 'foo'; + const text = 'foo'; await tester.pumpWidget( const MaterialApp(home: ClosedCaption(text: text)), ); @@ -349,8 +349,8 @@ void main() { }); testWidgets('uses given text and style', (WidgetTester tester) async { - const String text = 'foo'; - const TextStyle textStyle = TextStyle(fontSize: 14.725); + const text = 'foo'; + const textStyle = TextStyle(fontSize: 14.725); await tester.pumpWidget( const MaterialApp( home: ClosedCaption(text: text, textStyle: textStyle), @@ -375,7 +375,7 @@ void main() { testWidgets('Passes text contrast ratio guidelines', ( WidgetTester tester, ) async { - const String text = 'foo'; + const text = 'foo'; await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -393,9 +393,7 @@ void main() { group('VideoPlayerController', () { group('legacy initialize', () { test('network', () async { - final VideoPlayerController controller = VideoPlayerController.network( - 'https://127.0.0.1', - ); + final controller = VideoPlayerController.network('https://127.0.0.1'); await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); @@ -407,7 +405,7 @@ void main() { }); test('network with hint', () async { - final VideoPlayerController controller = VideoPlayerController.network( + final controller = VideoPlayerController.network( 'https://127.0.0.1', formatHint: VideoFormat.dash, ); @@ -425,7 +423,7 @@ void main() { }); test('network with some headers', () async { - final VideoPlayerController controller = VideoPlayerController.network( + final controller = VideoPlayerController.network( 'https://127.0.0.1', httpHeaders: {'Authorization': 'Bearer token'}, ); @@ -442,8 +440,9 @@ void main() { group('initialize', () { test('started app lifecycle observing', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl(Uri.parse('https://127.0.0.1')); + final controller = VideoPlayerController.networkUrl( + Uri.parse('https://127.0.0.1'), + ); addTearDown(controller.dispose); await controller.initialize(); await controller.play(); @@ -454,9 +453,7 @@ void main() { }); test('asset', () async { - final VideoPlayerController controller = VideoPlayerController.asset( - 'a.avi', - ); + final controller = VideoPlayerController.asset('a.avi'); await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSources[0].asset, 'a.avi'); @@ -464,8 +461,9 @@ void main() { }); test('network url', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl(Uri.parse('https://127.0.0.1')); + final controller = VideoPlayerController.networkUrl( + Uri.parse('https://127.0.0.1'), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -478,11 +476,10 @@ void main() { }); test('network url with hint', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - Uri.parse('https://127.0.0.1'), - formatHint: VideoFormat.dash, - ); + final controller = VideoPlayerController.networkUrl( + Uri.parse('https://127.0.0.1'), + formatHint: VideoFormat.dash, + ); addTearDown(controller.dispose); await controller.initialize(); @@ -498,11 +495,10 @@ void main() { }); test('network url with some headers', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - Uri.parse('https://127.0.0.1'), - httpHeaders: {'Authorization': 'Bearer token'}, - ); + final controller = VideoPlayerController.networkUrl( + Uri.parse('https://127.0.0.1'), + httpHeaders: {'Authorization': 'Bearer token'}, + ); addTearDown(controller.dispose); await controller.initialize(); @@ -519,22 +515,19 @@ void main() { () async { final Uri invalidUrl = Uri.parse('http://testing.com/invalid_url'); - final VideoPlayerController controller = - VideoPlayerController.networkUrl(invalidUrl); + final controller = VideoPlayerController.networkUrl(invalidUrl); addTearDown(controller.dispose); late Object error; fakeVideoPlayerPlatform.forceInitError = true; await controller.initialize().catchError((Object e) => error = e); - final PlatformException platformEx = error as PlatformException; + final platformEx = error as PlatformException; expect(platformEx.code, equals('VideoError')); }, ); test('file', () async { - final VideoPlayerController controller = VideoPlayerController.file( - File('a.avi'), - ); + final controller = VideoPlayerController.file(File('a.avi')); await controller.initialize(); final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; @@ -545,9 +538,7 @@ void main() { test( 'file with special characters', () async { - final VideoPlayerController controller = VideoPlayerController.file( - File('A #1 Hit.avi'), - ); + final controller = VideoPlayerController.file(File('A #1 Hit.avi')); await controller.initialize(); final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; @@ -568,7 +559,7 @@ void main() { test( 'file with headers (m3u8)', () async { - final VideoPlayerController controller = VideoPlayerController.file( + final controller = VideoPlayerController.file( File('a.avi'), httpHeaders: {'Authorization': 'Bearer token'}, ); @@ -593,8 +584,7 @@ void main() { test( 'successful initialize on controller with error clears error', () async { - final VideoPlayerController controller = - VideoPlayerController.network('https://127.0.0.1'); + final controller = VideoPlayerController.network('https://127.0.0.1'); fakeVideoPlayerPlatform.forceInitError = true; await controller.initialize().catchError((dynamic e) {}); expect(controller.value.hasError, equals(true)); @@ -607,8 +597,7 @@ void main() { test( 'given controller with error when initialization succeeds it should clear error', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl(_localhostUri); + final controller = VideoPlayerController.networkUrl(_localhostUri); addTearDown(controller.dispose); fakeVideoPlayerPlatform.forceInitError = true; @@ -622,7 +611,7 @@ void main() { }); test('contentUri', () async { - final VideoPlayerController controller = VideoPlayerController.contentUri( + final controller = VideoPlayerController.contentUri( Uri.parse('content://video'), ); await controller.initialize(); @@ -631,9 +620,7 @@ void main() { }); test('dispose', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - ); + final controller = VideoPlayerController.networkUrl(_localhostUri); addTearDown(controller.dispose); expect(controller.playerId, VideoPlayerController.kUninitializedPlayerId); @@ -647,9 +634,7 @@ void main() { }); test('calling dispose() on disposed controller does not throw', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - ); + final controller = VideoPlayerController.networkUrl(_localhostUri); addTearDown(controller.dispose); await controller.initialize(); @@ -659,7 +644,7 @@ void main() { }); test('play', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( + final controller = VideoPlayerController.networkUrl( Uri.parse('https://127.0.0.1'), ); addTearDown(controller.dispose); @@ -680,9 +665,7 @@ void main() { }); test('play before initialized does not call platform', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - ); + final controller = VideoPlayerController.networkUrl(_localhostUri); addTearDown(controller.dispose); expect(controller.value.isInitialized, isFalse); @@ -693,13 +676,11 @@ void main() { }); test('play restarts from beginning if video is at end', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - ); + final controller = VideoPlayerController.networkUrl(_localhostUri); addTearDown(controller.dispose); await controller.initialize(); - const Duration nonzeroDuration = Duration(milliseconds: 100); + const nonzeroDuration = Duration(milliseconds: 100); controller.value = controller.value.copyWith(duration: nonzeroDuration); await controller.seekTo(nonzeroDuration); expect(controller.value.isPlaying, isFalse); @@ -712,9 +693,7 @@ void main() { }); test('setLooping', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - ); + final controller = VideoPlayerController.networkUrl(_localhostUri); addTearDown(controller.dispose); await controller.initialize(); @@ -725,9 +704,7 @@ void main() { }); test('pause', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( - _localhostUri, - ); + final controller = VideoPlayerController.networkUrl(_localhostUri); addTearDown(controller.dispose); await controller.initialize(); @@ -742,8 +719,7 @@ void main() { group('seekTo', () { test('works', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl(_localhostUri); + final controller = VideoPlayerController.networkUrl(_localhostUri); addTearDown(controller.dispose); await controller.initialize(); @@ -755,8 +731,7 @@ void main() { }); test('before initialized does not call platform', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl(_localhostUri); + final controller = VideoPlayerController.networkUrl(_localhostUri); addTearDown(controller.dispose); expect(controller.value.isInitialized, isFalse); @@ -767,8 +742,7 @@ void main() { }); test('clamps values that are too high or low', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl(_localhostUri); + final controller = VideoPlayerController.networkUrl(_localhostUri); addTearDown(controller.dispose); await controller.initialize(); @@ -784,22 +758,20 @@ void main() { group('setVolume', () { test('works', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl(_localhostUri); + final controller = VideoPlayerController.networkUrl(_localhostUri); addTearDown(controller.dispose); await controller.initialize(); expect(controller.value.volume, 1.0); - const double volume = 0.5; + const volume = 0.5; await controller.setVolume(volume); expect(controller.value.volume, volume); }); test('clamps values that are too high or low', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl(_localhostUri); + final controller = VideoPlayerController.networkUrl(_localhostUri); addTearDown(controller.dispose); await controller.initialize(); @@ -815,22 +787,20 @@ void main() { group('setPlaybackSpeed', () { test('works', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl(_localhostUri); + final controller = VideoPlayerController.networkUrl(_localhostUri); addTearDown(controller.dispose); await controller.initialize(); expect(controller.value.playbackSpeed, 1.0); - const double speed = 1.5; + const speed = 1.5; await controller.setPlaybackSpeed(speed); expect(controller.value.playbackSpeed, speed); }); test('rejects negative values', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl(_localhostUri); + final controller = VideoPlayerController.networkUrl(_localhostUri); addTearDown(controller.dispose); await controller.initialize(); @@ -844,11 +814,10 @@ void main() { testWidgets('restarts on release if already playing', ( WidgetTester tester, ) async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl(_localhostUri); + final controller = VideoPlayerController.networkUrl(_localhostUri); await controller.initialize(); - final VideoProgressIndicator progressWidget = VideoProgressIndicator( + final progressWidget = VideoProgressIndicator( controller, allowScrubbing: true, ); @@ -877,11 +846,10 @@ void main() { testWidgets('does not restart when dragging to end', ( WidgetTester tester, ) async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl(_localhostUri); + final controller = VideoPlayerController.networkUrl(_localhostUri); await controller.initialize(); - final VideoProgressIndicator progressWidget = VideoProgressIndicator( + final progressWidget = VideoProgressIndicator( controller, allowScrubbing: true, ); @@ -908,17 +876,16 @@ void main() { group('caption', () { test('works when position updates', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); + final controller = VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); await controller.initialize(); await controller.play(); // Optionally record caption changes for later verification. - final Map recordedCaptions = {}; + final recordedCaptions = {}; controller.addListener(() { // Record the caption for the current position (in milliseconds). @@ -926,11 +893,11 @@ void main() { recordedCaptions[ms] = controller.value.caption.text; }); - const Duration updateInterval = Duration(milliseconds: 100); - const int totalDurationMs = 350; + const updateInterval = Duration(milliseconds: 100); + const totalDurationMs = 350; // Simulate continuous playback by incrementing in 50ms steps. - for (int ms = 0; ms <= totalDurationMs; ms += 50) { + for (var ms = 0; ms <= totalDurationMs; ms += 50) { fakeVideoPlayerPlatform._positions[controller.playerId] = Duration( milliseconds: ms, ); @@ -948,11 +915,10 @@ void main() { }); test('makes sure the input captions are unsorted', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); + final controller = VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); await controller.initialize(); final List captions = (await controller.closedCaptionFile)! @@ -960,8 +926,8 @@ void main() { .toList(); // Check that captions are not in sorted order. - bool isSorted = true; - for (int i = 0; i < captions.length - 1; i++) { + var isSorted = true; + for (var i = 0; i < captions.length - 1; i++) { if (captions[i].start.compareTo(captions[i + 1].start) > 0) { isSorted = false; break; @@ -977,11 +943,10 @@ void main() { }); test('works when seeking, includes all captions', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); + final controller = VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -1028,11 +993,10 @@ void main() { test( 'works when seeking with captionOffset positive, includes all captions', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); + final controller = VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -1078,11 +1042,10 @@ void main() { test( 'works when seeking with captionOffset negative, includes all captions', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); + final controller = VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -1120,8 +1083,7 @@ void main() { ); test('setClosedCaptionFile loads caption file', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl(_localhostUri); + final controller = VideoPlayerController.networkUrl(_localhostUri); addTearDown(controller.dispose); await controller.initialize(); @@ -1135,11 +1097,10 @@ void main() { }); test('setClosedCaptionFile removes/changes caption file', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); + final controller = VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -1153,11 +1114,10 @@ void main() { }); test('binary search handles exact caption start time boundary', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); + final controller = VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -1202,11 +1162,10 @@ void main() { }); test('binary search handles exact caption end time boundary', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); + final controller = VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -1270,11 +1229,10 @@ void main() { }); test('binary search handles gaps between captions', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: _loadClosedCaption(), - ); + final controller = VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: _loadClosedCaption(), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -1321,13 +1279,12 @@ void main() { }); test('binary search works with single caption', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: Future.value( - _SingleCaptionFile(), - ), - ); + final controller = VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: Future.value( + _SingleCaptionFile(), + ), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -1374,13 +1331,12 @@ void main() { }); test('binary search handles overlapping captions', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - closedCaptionFile: Future.value( - _OverlappingCaptionFile(), - ), - ); + final controller = VideoPlayerController.networkUrl( + _localhostUri, + closedCaptionFile: Future.value( + _OverlappingCaptionFile(), + ), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -1423,11 +1379,10 @@ void main() { group('Platform callbacks', () { testWidgets('playing completed', (WidgetTester tester) async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl(_localhostUri); + final controller = VideoPlayerController.networkUrl(_localhostUri); await controller.initialize(); - const Duration nonzeroDuration = Duration(milliseconds: 100); + const nonzeroDuration = Duration(milliseconds: 100); controller.value = controller.value.copyWith(duration: nonzeroDuration); expect(controller.value.isPlaying, isFalse); await controller.play(); @@ -1446,9 +1401,7 @@ void main() { }); testWidgets('playback status', (WidgetTester tester) async { - final VideoPlayerController controller = VideoPlayerController.network( - 'https://.0.0.1', - ); + final controller = VideoPlayerController.network('https://.0.0.1'); await controller.initialize(); expect(controller.value.isPlaying, isFalse); final StreamController fakeVideoEventStream = @@ -1475,8 +1428,7 @@ void main() { }); testWidgets('buffering status', (WidgetTester tester) async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl(_localhostUri); + final controller = VideoPlayerController.networkUrl(_localhostUri); await controller.initialize(); expect(controller.value.isBuffering, false); @@ -1491,7 +1443,7 @@ void main() { expect(controller.value.isBuffering, isTrue); const Duration bufferStart = Duration.zero; - const Duration bufferEnd = Duration(milliseconds: 500); + const bufferEnd = Duration(milliseconds: 500); fakeVideoEventStream.add( VideoEvent( eventType: VideoEventType.bufferingUpdate, @@ -1516,17 +1468,17 @@ void main() { }); test('updates position', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( + final controller = VideoPlayerController.networkUrl( _localhostUri, videoPlayerOptions: VideoPlayerOptions(), ); await controller.initialize(); - const Duration updatesInterval = Duration(milliseconds: 100); + const updatesInterval = Duration(milliseconds: 100); - final List positions = []; - final Completer intervalUpdateCompleter = Completer(); + final positions = []; + final intervalUpdateCompleter = Completer(); // Listen for position updates controller.addListener(() { @@ -1536,7 +1488,7 @@ void main() { } }); await controller.play(); - for (int i = 0; i < 3; i++) { + for (var i = 0; i < 3; i++) { await Future.delayed(updatesInterval); fakeVideoPlayerPlatform._positions[controller.playerId] = Duration( milliseconds: i * updatesInterval.inMilliseconds, @@ -1559,10 +1511,10 @@ void main() { group('DurationRange', () { test('uses given values', () { - const Duration start = Duration(seconds: 2); - const Duration end = Duration(seconds: 8); + const start = Duration(seconds: 2); + const end = Duration(seconds: 8); - final DurationRange range = DurationRange(start, end); + final range = DurationRange(start, end); expect(range.start, start); expect(range.end, end); @@ -1570,11 +1522,11 @@ void main() { }); test('calculates fractions', () { - const Duration start = Duration(seconds: 2); - const Duration end = Duration(seconds: 8); - const Duration total = Duration(seconds: 10); + const start = Duration(seconds: 2); + const end = Duration(seconds: 8); + const total = Duration(seconds: 10); - final DurationRange range = DurationRange(start, end); + final range = DurationRange(start, end); expect(range.startFraction(total), .2); expect(range.endFraction(total), .8); @@ -1583,7 +1535,7 @@ void main() { group('VideoPlayerValue', () { test('uninitialized()', () { - const VideoPlayerValue uninitialized = VideoPlayerValue.uninitialized(); + const uninitialized = VideoPlayerValue.uninitialized(); expect(uninitialized.duration, equals(Duration.zero)); expect(uninitialized.position, equals(Duration.zero)); @@ -1603,8 +1555,8 @@ void main() { }); test('erroneous()', () { - const String errorMessage = 'foo'; - const VideoPlayerValue error = VideoPlayerValue.erroneous(errorMessage); + const errorMessage = 'foo'; + const error = VideoPlayerValue.erroneous(errorMessage); expect(error.duration, equals(Duration.zero)); expect(error.position, equals(Duration.zero)); @@ -1624,27 +1576,27 @@ void main() { }); test('toString()', () { - const Duration duration = Duration(seconds: 5); - const Size size = Size(400, 300); - const Duration position = Duration(seconds: 1); - const Caption caption = Caption( + const duration = Duration(seconds: 5); + const size = Size(400, 300); + const position = Duration(seconds: 1); + const caption = Caption( text: 'foo', number: 0, start: Duration.zero, end: Duration.zero, ); - const Duration captionOffset = Duration(milliseconds: 250); - final List buffered = [ + const captionOffset = Duration(milliseconds: 250); + final buffered = [ DurationRange(Duration.zero, const Duration(seconds: 4)), ]; - const bool isInitialized = true; - const bool isPlaying = true; - const bool isLooping = true; - const bool isBuffering = true; - const double volume = 0.5; - const double playbackSpeed = 1.5; - - final VideoPlayerValue value = VideoPlayerValue( + const isInitialized = true; + const isPlaying = true; + const isLooping = true; + const isBuffering = true; + const volume = 0.5; + const playbackSpeed = 1.5; + + final value = VideoPlayerValue( duration: duration, size: size, position: position, @@ -1680,13 +1632,13 @@ void main() { group('copyWith()', () { test('exact copy', () { - const VideoPlayerValue original = VideoPlayerValue.uninitialized(); + const original = VideoPlayerValue.uninitialized(); final VideoPlayerValue exactCopy = original.copyWith(); expect(exactCopy.toString(), original.toString()); }); test('errorDescription is not persisted when copy with null', () { - const VideoPlayerValue original = VideoPlayerValue.erroneous('error'); + const original = VideoPlayerValue.erroneous('error'); final VideoPlayerValue copy = original.copyWith( errorDescription: null, ); @@ -1694,7 +1646,7 @@ void main() { expect(copy.errorDescription, null); }); test('errorDescription is changed when copy with another error', () { - const VideoPlayerValue original = VideoPlayerValue.erroneous('error'); + const original = VideoPlayerValue.erroneous('error'); final VideoPlayerValue copy = original.copyWith( errorDescription: 'new error', ); @@ -1702,7 +1654,7 @@ void main() { expect(copy.errorDescription, 'new error'); }); test('errorDescription is changed when copy with error', () { - const VideoPlayerValue original = VideoPlayerValue.uninitialized(); + const original = VideoPlayerValue.uninitialized(); final VideoPlayerValue copy = original.copyWith( errorDescription: 'new error', ); @@ -1713,7 +1665,7 @@ void main() { group('aspectRatio', () { test('640x480 -> 4:3', () { - const VideoPlayerValue value = VideoPlayerValue( + const value = VideoPlayerValue( isInitialized: true, size: Size(640, 480), duration: Duration(seconds: 1), @@ -1722,7 +1674,7 @@ void main() { }); test('no size -> 1.0', () { - const VideoPlayerValue value = VideoPlayerValue( + const value = VideoPlayerValue( isInitialized: true, duration: Duration(seconds: 1), ); @@ -1730,7 +1682,7 @@ void main() { }); test('height = 0 -> 1.0', () { - const VideoPlayerValue value = VideoPlayerValue( + const value = VideoPlayerValue( isInitialized: true, size: Size(640, 0), duration: Duration(seconds: 1), @@ -1739,7 +1691,7 @@ void main() { }); test('width = 0 -> 1.0', () { - const VideoPlayerValue value = VideoPlayerValue( + const value = VideoPlayerValue( isInitialized: true, size: Size(0, 480), duration: Duration(seconds: 1), @@ -1748,7 +1700,7 @@ void main() { }); test('negative aspect ratio -> 1.0', () { - const VideoPlayerValue value = VideoPlayerValue( + const value = VideoPlayerValue( isInitialized: true, size: Size(640, -480), duration: Duration(seconds: 1), @@ -1760,11 +1712,10 @@ void main() { group('VideoPlayerOptions', () { test('setMixWithOthers', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), - ); + final controller = VideoPlayerController.networkUrl( + _localhostUri, + videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -1772,13 +1723,10 @@ void main() { }); test('true allowBackgroundPlayback continues playback', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - videoPlayerOptions: VideoPlayerOptions( - allowBackgroundPlayback: true, - ), - ); + final controller = VideoPlayerController.networkUrl( + _localhostUri, + videoPlayerOptions: VideoPlayerOptions(allowBackgroundPlayback: true), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -1790,11 +1738,10 @@ void main() { }); test('false allowBackgroundPlayback pauses playback', () async { - final VideoPlayerController controller = - VideoPlayerController.networkUrl( - _localhostUri, - videoPlayerOptions: VideoPlayerOptions(), - ); + final controller = VideoPlayerController.networkUrl( + _localhostUri, + videoPlayerOptions: VideoPlayerOptions(), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -1807,11 +1754,11 @@ void main() { }); test('VideoProgressColors', () { - const Color playedColor = Color.fromRGBO(0, 0, 255, 0.75); - const Color bufferedColor = Color.fromRGBO(0, 255, 0, 0.5); - const Color backgroundColor = Color.fromRGBO(255, 255, 0, 0.25); + const playedColor = Color.fromRGBO(0, 0, 255, 0.75); + const bufferedColor = Color.fromRGBO(0, 255, 0, 0.5); + const backgroundColor = Color.fromRGBO(255, 255, 0, 0.25); - const VideoProgressColors colors = VideoProgressColors( + const colors = VideoProgressColors( playedColor: playedColor, bufferedColor: bufferedColor, backgroundColor: backgroundColor, @@ -1823,7 +1770,7 @@ void main() { }); test('isCompleted updates on video end', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( + final controller = VideoPlayerController.networkUrl( _localhostUri, videoPlayerOptions: VideoPlayerOptions(), ); @@ -1851,7 +1798,7 @@ void main() { }); test('isCompleted updates on video play after completed', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( + final controller = VideoPlayerController.networkUrl( _localhostUri, videoPlayerOptions: VideoPlayerOptions(), ); @@ -1866,7 +1813,7 @@ void main() { final void Function() isCompletedTest = expectAsync0(() {}, count: 2); final void Function() isNoLongerCompletedTest = expectAsync0(() {}); - bool hasLooped = false; + var hasLooped = false; controller.addListener(() async { if (currentIsCompleted != controller.value.isCompleted) { @@ -1892,7 +1839,7 @@ void main() { }); test('isCompleted updates on video seek to end', () async { - final VideoPlayerController controller = VideoPlayerController.networkUrl( + final controller = VideoPlayerController.networkUrl( _localhostUri, videoPlayerOptions: VideoPlayerOptions(), ); @@ -1942,7 +1889,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { @override Future create(DataSource dataSource) async { calls.add('create'); - final StreamController stream = StreamController(); + final stream = StreamController(); streams[nextPlayerId] = stream; if (forceInitError) { stream.addError( @@ -1967,7 +1914,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { @override Future createWithOptions(VideoCreationOptions options) async { calls.add('createWithOptions'); - final StreamController stream = StreamController(); + final stream = StreamController(); streams[nextPlayerId] = stream; if (forceInitError) { stream.addError(