| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:io'; |
| import 'dart:math' as math; |
| import 'dart:typed_data'; |
| |
| import 'package:flutter/foundation.dart' show kIsWeb; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:video_player/video_player.dart'; |
| import 'package:video_player_platform_interface/video_player_platform_interface.dart'; |
| |
| class FakeController extends ValueNotifier<VideoPlayerValue> |
| implements VideoPlayerController { |
| FakeController() : super(const VideoPlayerValue(duration: Duration.zero)); |
| |
| FakeController.value(super.value); |
| |
| @override |
| Future<void> dispose() async { |
| super.dispose(); |
| } |
| |
| @override |
| int textureId = VideoPlayerController.kUninitializedTextureId; |
| |
| @override |
| String get dataSource => ''; |
| |
| @override |
| Map<String, String> get httpHeaders => <String, String>{}; |
| |
| @override |
| DataSourceType get dataSourceType => DataSourceType.file; |
| |
| @override |
| String get package => ''; |
| |
| @override |
| Future<Duration> get position async => value.position; |
| |
| @override |
| Future<void> seekTo(Duration moment) async {} |
| |
| @override |
| Future<void> setVolume(double volume) async {} |
| |
| @override |
| Future<void> setPlaybackSpeed(double speed) async {} |
| |
| @override |
| Future<void> initialize() async {} |
| |
| @override |
| Future<void> pause() async {} |
| |
| @override |
| Future<void> play() async {} |
| |
| @override |
| Future<void> setLooping(bool looping) async {} |
| |
| @override |
| VideoFormat? get formatHint => null; |
| |
| @override |
| Future<ClosedCaptionFile> get closedCaptionFile => _loadClosedCaption(); |
| |
| @override |
| VideoPlayerOptions? get videoPlayerOptions => null; |
| |
| @override |
| void setCaptionOffset(Duration delay) {} |
| |
| @override |
| Future<void> setClosedCaptionFile( |
| Future<ClosedCaptionFile>? closedCaptionFile, |
| ) async {} |
| } |
| |
| Future<ClosedCaptionFile> _loadClosedCaption() async => |
| _FakeClosedCaptionFile(); |
| |
| class _FakeClosedCaptionFile extends ClosedCaptionFile { |
| @override |
| List<Caption> get captions { |
| return <Caption>[ |
| const Caption( |
| text: 'one', |
| number: 0, |
| start: Duration(milliseconds: 100), |
| end: Duration(milliseconds: 200), |
| ), |
| const Caption( |
| text: 'two', |
| number: 1, |
| start: Duration(milliseconds: 300), |
| end: Duration(milliseconds: 400), |
| ), |
| ]; |
| } |
| } |
| |
| void main() { |
| late FakeVideoPlayerPlatform fakeVideoPlayerPlatform; |
| |
| setUp(() { |
| fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); |
| VideoPlayerPlatform.instance = fakeVideoPlayerPlatform; |
| }); |
| |
| void verifyPlayStateRespondsToLifecycle( |
| VideoPlayerController controller, { |
| required bool shouldPlayInBackground, |
| }) { |
| expect(controller.value.isPlaying, true); |
| _ambiguate(WidgetsBinding.instance)! |
| .handleAppLifecycleStateChanged(AppLifecycleState.paused); |
| expect(controller.value.isPlaying, shouldPlayInBackground); |
| _ambiguate(WidgetsBinding.instance)! |
| .handleAppLifecycleStateChanged(AppLifecycleState.resumed); |
| expect(controller.value.isPlaying, true); |
| } |
| |
| testWidgets('update texture', (WidgetTester tester) async { |
| final FakeController controller = FakeController(); |
| await tester.pumpWidget(VideoPlayer(controller)); |
| expect(find.byType(Texture), findsNothing); |
| |
| controller.textureId = 123; |
| controller.value = controller.value.copyWith( |
| duration: const Duration(milliseconds: 100), |
| isInitialized: true, |
| ); |
| |
| await tester.pump(); |
| expect(find.byType(Texture), findsOneWidget); |
| }); |
| |
| testWidgets('update controller', (WidgetTester tester) async { |
| final FakeController controller1 = FakeController(); |
| controller1.textureId = 101; |
| await tester.pumpWidget(VideoPlayer(controller1)); |
| expect( |
| find.byWidgetPredicate( |
| (Widget widget) => widget is Texture && widget.textureId == 101, |
| ), |
| findsOneWidget); |
| |
| final FakeController controller2 = FakeController(); |
| controller2.textureId = 102; |
| await tester.pumpWidget(VideoPlayer(controller2)); |
| expect( |
| find.byWidgetPredicate( |
| (Widget widget) => widget is Texture && widget.textureId == 102, |
| ), |
| findsOneWidget); |
| }); |
| |
| testWidgets('non-zero rotationCorrection value is used', |
| (WidgetTester tester) async { |
| final FakeController controller = FakeController.value( |
| const VideoPlayerValue( |
| duration: Duration.zero, rotationCorrection: 180)); |
| controller.textureId = 1; |
| await tester.pumpWidget(VideoPlayer(controller)); |
| final Transform actualRotationCorrection = |
| find.byType(Transform).evaluate().single.widget as Transform; |
| final Float64List actualRotationCorrectionStorage = |
| actualRotationCorrection.transform.storage; |
| final Float64List expectedMatrixStorage = |
| Matrix4.rotationZ(math.pi).storage; |
| expect(actualRotationCorrectionStorage.length, |
| equals(expectedMatrixStorage.length)); |
| for (int i = 0; i < actualRotationCorrectionStorage.length; i++) { |
| expect(actualRotationCorrectionStorage[i], |
| moreOrLessEquals(expectedMatrixStorage[i])); |
| } |
| }); |
| |
| testWidgets('no transform when rotationCorrection is zero', |
| (WidgetTester tester) async { |
| final FakeController controller = |
| FakeController.value(const VideoPlayerValue(duration: Duration.zero)); |
| controller.textureId = 1; |
| await tester.pumpWidget(VideoPlayer(controller)); |
| expect(find.byType(Transform), findsNothing); |
| }); |
| |
| group('ClosedCaption widget', () { |
| testWidgets('uses a default text style', (WidgetTester tester) async { |
| const String text = 'foo'; |
| await tester |
| .pumpWidget(const MaterialApp(home: ClosedCaption(text: text))); |
| |
| final Text textWidget = tester.widget<Text>(find.text(text)); |
| expect(textWidget.style!.fontSize, 36.0); |
| expect(textWidget.style!.color, Colors.white); |
| }); |
| |
| testWidgets('uses given text and style', (WidgetTester tester) async { |
| const String text = 'foo'; |
| const TextStyle textStyle = TextStyle(fontSize: 14.725); |
| await tester.pumpWidget(const MaterialApp( |
| home: ClosedCaption( |
| text: text, |
| textStyle: textStyle, |
| ), |
| )); |
| expect(find.text(text), findsOneWidget); |
| |
| final Text textWidget = tester.widget<Text>(find.text(text)); |
| expect(textWidget.style!.fontSize, textStyle.fontSize); |
| }); |
| |
| testWidgets('handles null text', (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp(home: ClosedCaption())); |
| expect(find.byType(Text), findsNothing); |
| }); |
| |
| testWidgets('handles empty text', (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp(home: ClosedCaption(text: ''))); |
| expect(find.byType(Text), findsNothing); |
| }); |
| |
| testWidgets('Passes text contrast ratio guidelines', |
| (WidgetTester tester) async { |
| const String text = 'foo'; |
| await tester.pumpWidget(const MaterialApp( |
| home: Scaffold( |
| backgroundColor: Colors.white, |
| body: ClosedCaption(text: text), |
| ), |
| )); |
| expect(find.text(text), findsOneWidget); |
| |
| await expectLater(tester, meetsGuideline(textContrastGuideline)); |
| }, skip: isBrowser); |
| }); |
| |
| group('VideoPlayerController', () { |
| group('initialize', () { |
| test('started app lifecycle observing', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| await controller.play(); |
| verifyPlayStateRespondsToLifecycle(controller, |
| shouldPlayInBackground: false); |
| }); |
| |
| test('asset', () async { |
| final VideoPlayerController controller = VideoPlayerController.asset( |
| 'a.avi', |
| ); |
| await controller.initialize(); |
| |
| expect(fakeVideoPlayerPlatform.dataSources[0].asset, 'a.avi'); |
| expect(fakeVideoPlayerPlatform.dataSources[0].package, null); |
| }); |
| |
| test('network', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| |
| expect( |
| fakeVideoPlayerPlatform.dataSources[0].uri, |
| 'https://127.0.0.1', |
| ); |
| expect( |
| fakeVideoPlayerPlatform.dataSources[0].formatHint, |
| null, |
| ); |
| expect( |
| fakeVideoPlayerPlatform.dataSources[0].httpHeaders, |
| <String, String>{}, |
| ); |
| }); |
| |
| test('network with hint', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| formatHint: VideoFormat.dash, |
| ); |
| await controller.initialize(); |
| |
| expect( |
| fakeVideoPlayerPlatform.dataSources[0].uri, |
| 'https://127.0.0.1', |
| ); |
| expect( |
| fakeVideoPlayerPlatform.dataSources[0].formatHint, |
| VideoFormat.dash, |
| ); |
| expect( |
| fakeVideoPlayerPlatform.dataSources[0].httpHeaders, |
| <String, String>{}, |
| ); |
| }); |
| |
| test('network with some headers', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| httpHeaders: <String, String>{'Authorization': 'Bearer token'}, |
| ); |
| await controller.initialize(); |
| |
| expect( |
| fakeVideoPlayerPlatform.dataSources[0].uri, |
| 'https://127.0.0.1', |
| ); |
| expect( |
| fakeVideoPlayerPlatform.dataSources[0].formatHint, |
| null, |
| ); |
| expect( |
| fakeVideoPlayerPlatform.dataSources[0].httpHeaders, |
| <String, String>{'Authorization': 'Bearer token'}, |
| ); |
| }); |
| |
| test('init errors', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'http://testing.com/invalid_url', |
| ); |
| |
| late Object error; |
| fakeVideoPlayerPlatform.forceInitError = true; |
| await controller.initialize().catchError((Object e) => error = e); |
| final PlatformException platformEx = error as PlatformException; |
| expect(platformEx.code, equals('VideoError')); |
| }); |
| |
| test('file', () async { |
| final VideoPlayerController controller = |
| VideoPlayerController.file(File('a.avi')); |
| await controller.initialize(); |
| |
| final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; |
| expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri'); |
| expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri'); |
| }, skip: kIsWeb /* Web does not support file assets. */); |
| |
| test('file with special characters', () async { |
| final VideoPlayerController controller = |
| VideoPlayerController.file(File('A #1 Hit?.avi')); |
| await controller.initialize(); |
| |
| final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; |
| expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri'); |
| expect(uri.endsWith('/A%20%231%20Hit%3F.avi'), true, |
| reason: 'Actual string: $uri'); |
| }, skip: kIsWeb /* Web does not support file assets. */); |
| |
| test('file with headers (m3u8)', () async { |
| final VideoPlayerController controller = VideoPlayerController.file( |
| File('a.avi'), |
| httpHeaders: <String, String>{'Authorization': 'Bearer token'}, |
| ); |
| await controller.initialize(); |
| |
| final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; |
| expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri'); |
| expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri'); |
| |
| expect( |
| fakeVideoPlayerPlatform.dataSources[0].httpHeaders, |
| <String, String>{'Authorization': 'Bearer token'}, |
| ); |
| }, skip: kIsWeb /* Web does not support file assets. */); |
| test('successful initialize on controller with error clears error', |
| () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| fakeVideoPlayerPlatform.forceInitError = true; |
| await controller.initialize().catchError((dynamic e) {}); |
| expect(controller.value.hasError, equals(true)); |
| fakeVideoPlayerPlatform.forceInitError = false; |
| await controller.initialize(); |
| expect(controller.value.hasError, equals(false)); |
| }); |
| }); |
| |
| test('contentUri', () async { |
| final VideoPlayerController controller = |
| VideoPlayerController.contentUri(Uri.parse('content://video')); |
| await controller.initialize(); |
| |
| expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'content://video'); |
| }); |
| |
| test('dispose', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| expect( |
| controller.textureId, VideoPlayerController.kUninitializedTextureId); |
| expect(await controller.position, Duration.zero); |
| await controller.initialize(); |
| |
| await controller.dispose(); |
| |
| expect(controller.textureId, 0); |
| expect(await controller.position, isNull); |
| }); |
| |
| test('calling dispose() on disposed controller does not throw', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| |
| await controller.initialize(); |
| await controller.dispose(); |
| |
| expect(() async => controller.dispose(), returnsNormally); |
| }); |
| |
| test('play', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| expect(controller.value.isPlaying, isFalse); |
| await controller.play(); |
| |
| expect(controller.value.isPlaying, isTrue); |
| |
| // The two last calls will be "play" and then "setPlaybackSpeed". The |
| // reason for this is that "play" calls "setPlaybackSpeed" internally. |
| expect( |
| fakeVideoPlayerPlatform |
| .calls[fakeVideoPlayerPlatform.calls.length - 2], |
| 'play'); |
| expect(fakeVideoPlayerPlatform.calls.last, 'setPlaybackSpeed'); |
| }); |
| |
| test('play before initialized does not call platform', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| expect(controller.value.isInitialized, isFalse); |
| |
| await controller.play(); |
| |
| expect(fakeVideoPlayerPlatform.calls, isEmpty); |
| }); |
| |
| test('play restarts from beginning if video is at end', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| const Duration nonzeroDuration = Duration(milliseconds: 100); |
| controller.value = controller.value.copyWith(duration: nonzeroDuration); |
| await controller.seekTo(nonzeroDuration); |
| expect(controller.value.isPlaying, isFalse); |
| expect(controller.value.position, nonzeroDuration); |
| |
| await controller.play(); |
| |
| expect(controller.value.isPlaying, isTrue); |
| expect(controller.value.position, Duration.zero); |
| }); |
| |
| test('setLooping', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| expect(controller.value.isLooping, isFalse); |
| await controller.setLooping(true); |
| |
| expect(controller.value.isLooping, isTrue); |
| }); |
| |
| test('pause', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| await controller.play(); |
| expect(controller.value.isPlaying, isTrue); |
| |
| await controller.pause(); |
| |
| expect(controller.value.isPlaying, isFalse); |
| expect(fakeVideoPlayerPlatform.calls.last, 'pause'); |
| }); |
| |
| group('seekTo', () { |
| test('works', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| expect(await controller.position, Duration.zero); |
| |
| await controller.seekTo(const Duration(milliseconds: 500)); |
| |
| expect(await controller.position, const Duration(milliseconds: 500)); |
| }); |
| |
| test('before initialized does not call platform', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| expect(controller.value.isInitialized, isFalse); |
| |
| await controller.seekTo(const Duration(milliseconds: 500)); |
| |
| expect(fakeVideoPlayerPlatform.calls, isEmpty); |
| }); |
| |
| test('clamps values that are too high or low', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| expect(await controller.position, Duration.zero); |
| |
| await controller.seekTo(const Duration(seconds: 100)); |
| expect(await controller.position, const Duration(seconds: 1)); |
| |
| await controller.seekTo(const Duration(seconds: -100)); |
| expect(await controller.position, Duration.zero); |
| }); |
| }); |
| |
| group('setVolume', () { |
| test('works', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| expect(controller.value.volume, 1.0); |
| |
| const double 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.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| expect(controller.value.volume, 1.0); |
| |
| await controller.setVolume(-1); |
| expect(controller.value.volume, 0.0); |
| |
| await controller.setVolume(11); |
| expect(controller.value.volume, 1.0); |
| }); |
| }); |
| |
| group('setPlaybackSpeed', () { |
| test('works', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| expect(controller.value.playbackSpeed, 1.0); |
| |
| const double speed = 1.5; |
| await controller.setPlaybackSpeed(speed); |
| |
| expect(controller.value.playbackSpeed, speed); |
| }); |
| |
| test('rejects negative values', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| expect(controller.value.playbackSpeed, 1.0); |
| |
| expect(() => controller.setPlaybackSpeed(-1), throwsArgumentError); |
| }); |
| }); |
| |
| group('scrubbing', () { |
| testWidgets('restarts on release if already playing', |
| (WidgetTester tester) async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| final VideoProgressIndicator progressWidget = |
| VideoProgressIndicator(controller, allowScrubbing: true); |
| |
| await tester.pumpWidget(Directionality( |
| textDirection: TextDirection.ltr, |
| child: progressWidget, |
| )); |
| |
| await controller.play(); |
| expect(controller.value.isPlaying, isTrue); |
| |
| final Rect progressRect = tester.getRect(find.byWidget(progressWidget)); |
| await tester.dragFrom(progressRect.center, const Offset(1.0, 0.0)); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.value.position, lessThan(controller.value.duration)); |
| expect(controller.value.isPlaying, isTrue); |
| |
| await controller.pause(); |
| }); |
| |
| testWidgets('does not restart when dragging to end', |
| (WidgetTester tester) async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| final VideoProgressIndicator progressWidget = |
| VideoProgressIndicator(controller, allowScrubbing: true); |
| |
| await tester.pumpWidget(Directionality( |
| textDirection: TextDirection.ltr, |
| child: progressWidget, |
| )); |
| |
| await controller.play(); |
| expect(controller.value.isPlaying, isTrue); |
| |
| final Rect progressRect = tester.getRect(find.byWidget(progressWidget)); |
| await tester.dragFrom(progressRect.center, progressRect.centerRight); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.value.position, controller.value.duration); |
| expect(controller.value.isPlaying, isFalse); |
| }); |
| }); |
| |
| group('caption', () { |
| test('works when seeking', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| closedCaptionFile: _loadClosedCaption(), |
| ); |
| |
| 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: 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: 301)); |
| expect(controller.value.caption.text, 'two'); |
| |
| await controller.seekTo(const Duration(milliseconds: 500)); |
| expect(controller.value.caption.text, ''); |
| |
| 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('works when seeking with captionOffset positive', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| closedCaptionFile: _loadClosedCaption(), |
| ); |
| |
| 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: 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: 300)); |
| 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, ''); |
| |
| 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, ''); |
| }); |
| |
| test('works when seeking with captionOffset negative', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| closedCaptionFile: _loadClosedCaption(), |
| ); |
| |
| 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: 100)); |
| expect(controller.value.caption.text, ''); |
| |
| 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'); |
| |
| await controller.seekTo(const Duration(milliseconds: 600)); |
| expect(controller.value.caption.text, ''); |
| |
| await controller.seekTo(const Duration(milliseconds: 300)); |
| expect(controller.value.caption.text, 'one'); |
| }); |
| |
| test('setClosedCaptionFile loads caption file', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| |
| await controller.initialize(); |
| expect(controller.closedCaptionFile, null); |
| |
| await controller.setClosedCaptionFile(_loadClosedCaption()); |
| expect( |
| (await controller.closedCaptionFile)!.captions, |
| (await _loadClosedCaption()).captions, |
| ); |
| }); |
| |
| test('setClosedCaptionFile removes/changes caption file', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| closedCaptionFile: _loadClosedCaption(), |
| ); |
| |
| await controller.initialize(); |
| expect( |
| (await controller.closedCaptionFile)!.captions, |
| (await _loadClosedCaption()).captions, |
| ); |
| |
| await controller.setClosedCaptionFile(null); |
| expect(controller.closedCaptionFile, null); |
| }); |
| }); |
| |
| group('Platform callbacks', () { |
| testWidgets('playing completed', (WidgetTester tester) async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| 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<VideoEvent> fakeVideoEventStream = |
| fakeVideoPlayerPlatform.streams[controller.textureId]!; |
| |
| fakeVideoEventStream |
| .add(VideoEvent(eventType: VideoEventType.completed)); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.value.isPlaying, isFalse); |
| expect(controller.value.position, nonzeroDuration); |
| }); |
| |
| testWidgets('playback status', (WidgetTester tester) async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| expect(controller.value.isPlaying, isFalse); |
| final StreamController<VideoEvent> 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); |
| }); |
| |
| testWidgets('buffering status', (WidgetTester tester) async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| ); |
| await controller.initialize(); |
| expect(controller.value.isBuffering, false); |
| expect(controller.value.buffered, isEmpty); |
| final StreamController<VideoEvent> 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>[ |
| 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); |
| }); |
| }); |
| }); |
| |
| group('DurationRange', () { |
| test('uses given values', () { |
| const Duration start = Duration(seconds: 2); |
| const Duration end = Duration(seconds: 8); |
| |
| final DurationRange range = DurationRange(start, end); |
| |
| expect(range.start, start); |
| expect(range.end, end); |
| expect(range.toString(), contains('start: $start, end: $end')); |
| }); |
| |
| 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); |
| |
| expect(range.startFraction(total), .2); |
| expect(range.endFraction(total), .8); |
| }); |
| }); |
| |
| 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); |
| }); |
| |
| 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); |
| }); |
| |
| 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<DurationRange> buffered = <DurationRange>[ |
| 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( |
| 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)'); |
| }); |
| |
| group('copyWith()', () { |
| test('exact copy', () { |
| const VideoPlayerValue 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'); |
| 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, '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'); |
| }); |
| }); |
| |
| 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); |
| }); |
| |
| test('no size -> 1.0', () { |
| const VideoPlayerValue value = VideoPlayerValue( |
| isInitialized: true, |
| duration: Duration(seconds: 1), |
| ); |
| expect(value.aspectRatio, 1.0); |
| }); |
| |
| 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('VideoPlayerOptions', () { |
| test('setMixWithOthers', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true)); |
| await controller.initialize(); |
| expect(controller.videoPlayerOptions!.mixWithOthers, true); |
| }); |
| |
| test('true allowBackgroundPlayback continues playback', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| videoPlayerOptions: VideoPlayerOptions( |
| allowBackgroundPlayback: true, |
| ), |
| ); |
| await controller.initialize(); |
| await controller.play(); |
| verifyPlayStateRespondsToLifecycle( |
| controller, |
| shouldPlayInBackground: true, |
| ); |
| }); |
| |
| test('false allowBackgroundPlayback pauses playback', () async { |
| final VideoPlayerController controller = VideoPlayerController.network( |
| 'https://127.0.0.1', |
| videoPlayerOptions: VideoPlayerOptions(), |
| ); |
| 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); |
| |
| const VideoProgressColors colors = VideoProgressColors( |
| playedColor: playedColor, |
| bufferedColor: bufferedColor, |
| backgroundColor: backgroundColor); |
| |
| expect(colors.playedColor, playedColor); |
| expect(colors.bufferedColor, bufferedColor); |
| expect(colors.backgroundColor, backgroundColor); |
| }); |
| } |
| |
| class FakeVideoPlayerPlatform extends VideoPlayerPlatform { |
| Completer<bool> initialized = Completer<bool>(); |
| List<String> calls = <String>[]; |
| List<DataSource> dataSources = <DataSource>[]; |
| final Map<int, StreamController<VideoEvent>> streams = |
| <int, StreamController<VideoEvent>>{}; |
| bool forceInitError = false; |
| int nextTextureId = 0; |
| final Map<int, Duration> _positions = <int, Duration>{}; |
| |
| @override |
| Future<int?> create(DataSource dataSource) async { |
| calls.add('create'); |
| final StreamController<VideoEvent> stream = StreamController<VideoEvent>(); |
| streams[nextTextureId] = stream; |
| if (forceInitError) { |
| stream.addError(PlatformException( |
| code: 'VideoError', message: 'Video player had error XYZ')); |
| } else { |
| stream.add(VideoEvent( |
| eventType: VideoEventType.initialized, |
| size: const Size(100, 100), |
| duration: const Duration(seconds: 1))); |
| } |
| dataSources.add(dataSource); |
| return nextTextureId++; |
| } |
| |
| @override |
| Future<void> dispose(int textureId) async { |
| calls.add('dispose'); |
| } |
| |
| @override |
| Future<void> init() async { |
| calls.add('init'); |
| initialized.complete(true); |
| } |
| |
| @override |
| Stream<VideoEvent> videoEventsFor(int textureId) { |
| return streams[textureId]!.stream; |
| } |
| |
| @override |
| Future<void> pause(int textureId) async { |
| calls.add('pause'); |
| } |
| |
| @override |
| Future<void> play(int textureId) async { |
| calls.add('play'); |
| } |
| |
| @override |
| Future<Duration> getPosition(int textureId) async { |
| calls.add('position'); |
| return _positions[textureId] ?? Duration.zero; |
| } |
| |
| @override |
| Future<void> seekTo(int textureId, Duration position) async { |
| calls.add('seekTo'); |
| _positions[textureId] = position; |
| } |
| |
| @override |
| Future<void> setLooping(int textureId, bool looping) async { |
| calls.add('setLooping'); |
| } |
| |
| @override |
| Future<void> setVolume(int textureId, double volume) async { |
| calls.add('setVolume'); |
| } |
| |
| @override |
| Future<void> setPlaybackSpeed(int textureId, double speed) async { |
| calls.add('setPlaybackSpeed'); |
| } |
| |
| @override |
| Future<void> setMixWithOthers(bool mixWithOthers) async { |
| calls.add('setMixWithOthers'); |
| } |
| |
| @override |
| Widget buildView(int textureId) { |
| return Texture(textureId: textureId); |
| } |
| } |
| |
| /// This allows a value of type T or T? to be treated as a value of type T?. |
| /// |
| /// We use this so that APIs that have become non-nullable can still be used |
| /// with `!` and `?` on the stable branch. |
| T? _ambiguate<T>(T? value) => value; |