From 5487515d0f6198e6c94dc7e11a7a9f6d0e502f7f Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 1 Oct 2020 13:25:59 +0200 Subject: [PATCH 1/4] Added isDurationIndefinite to iOS --- packages/video_player/video_player/CHANGELOG.md | 4 ++++ .../video_player/ios/Classes/FLTVideoPlayerPlugin.m | 6 +++++- packages/video_player/video_player/lib/video_player.dart | 4 ++-- packages/video_player/video_player/pubspec.yaml | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index f7fad2648b3e..219fecf1de60 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.11.0+1 + +* Added isDurationIndefinite to support indefinite streams + ## 0.11.0 * Added option to set the video playback speed on the video controller. diff --git a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m index e6a4f6ccb0b7..062964b64a62 100644 --- a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m +++ b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m @@ -302,6 +302,10 @@ - (void)updatePlayingState { _displayLink.paused = !_isPlaying; } +- (bool)isDurationIndefinite { + return CMTIME_IS_INDEFINITE([[_player currentItem] duration]); +} + - (void)sendInitialized { if (_eventSink && !_isInitialized) { CGSize size = [self.player currentItem].presentationSize; @@ -313,7 +317,7 @@ - (void)sendInitialized { return; } // The player may be initialized but still needs to determine the duration. - if ([self duration] == 0) { + if ([self duration] == 0 && ![self isDurationIndefinite]) { return; } diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index bf98096af45d..31c95413ee64 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -819,12 +819,12 @@ class _VideoProgressIndicatorState extends State { fit: StackFit.passthrough, children: [ LinearProgressIndicator( - value: maxBuffering / duration, + value: duration > 0 ? maxBuffering / duration : 0, valueColor: AlwaysStoppedAnimation(colors.bufferedColor), backgroundColor: colors.backgroundColor, ), LinearProgressIndicator( - value: position / duration, + value: duration > 0 ? position / duration : 0, valueColor: AlwaysStoppedAnimation(colors.playedColor), backgroundColor: Colors.transparent, ), diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 973c0bc82589..30272c4c6b1f 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -4,7 +4,7 @@ description: Flutter plugin for displaying inline video with other Flutter # 0.10.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.11.0 +version: 0.11.0+1 homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player flutter: From cad3f308ab35a8a552413017f5eb5ef8f12ea65d Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 1 Oct 2020 13:47:18 +0200 Subject: [PATCH 2/4] Added isDurationIndefinite to Java and Dart --- .../flutter/plugins/videoplayer/VideoPlayer.java | 1 + .../video_player/example/lib/main.dart | 7 +++++-- .../example/test_driver/video_player_test.dart | 16 ++++++++++++++++ .../video_player/lib/video_player.dart | 8 ++++++++ packages/video_player/video_player/pubspec.yaml | 9 +++++---- .../lib/method_channel_video_player.dart | 1 + .../lib/video_player_platform_interface.dart | 8 ++++++++ 7 files changed, 44 insertions(+), 6 deletions(-) diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 8f8c898dea27..192869395f71 100644 --- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -256,6 +256,7 @@ private void sendInitialized() { Map event = new HashMap<>(); event.put("event", "initialized"); event.put("duration", exoPlayer.getDuration()); + event.put("isDurationIndefinite", exoPlayer.isCurrentWindowDynamic()); if (exoPlayer.getVideoFormat() != null) { Format videoFormat = exoPlayer.getVideoFormat(); diff --git a/packages/video_player/video_player/example/lib/main.dart b/packages/video_player/video_player/example/lib/main.dart index a99b9da6bd0c..5e4e32f934c8 100644 --- a/packages/video_player/video_player/example/lib/main.dart +++ b/packages/video_player/video_player/example/lib/main.dart @@ -218,13 +218,16 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { void initState() { super.initState(); _controller = VideoPlayerController.network( - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + // 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + 'https://rtmp.api.rt.com/hls/rtdru.m3u8', closedCaptionFile: _loadCaptions(), videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), ); _controller.addListener(() { - setState(() {}); + setState(() { + debugPrint('Indefinite = ${_controller.value.isDurationIndefinite}'); + }); }); _controller.setLooping(true); _controller.initialize(); diff --git a/packages/video_player/video_player/example/test_driver/video_player_test.dart b/packages/video_player/video_player/example/test_driver/video_player_test.dart index 47f3867d9019..4f26974b44b6 100644 --- a/packages/video_player/video_player/example/test_driver/video_player_test.dart +++ b/packages/video_player/video_player/example/test_driver/video_player_test.dart @@ -3,11 +3,14 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; + import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; +import 'package:video_player/video_player.dart'; Future main() async { final FlutterDriver driver = await FlutterDriver.connect(); + VideoPlayerController _controller; tearDownAll(() async { await driver.close(); }); @@ -24,4 +27,17 @@ Future main() async { final Health health = await driver.checkHealth(); expect(health.status, HealthStatus.ok); }, skip: 'Cirrus CI currently hangs while playing videos'); + + group('Livestream should have indefinite duration', () { + setUp(() async { + _controller = VideoPlayerController.network( + 'https://rtmp.api.rt.com/hls/rtdru.m3u8'); + }); + + test('Should initialize', () async { + await _controller.initialize(); + + expect(_controller.value.isDurationIndefinite, true); + }); + }); } diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 31c95413ee64..e03b093faa6c 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -36,6 +36,7 @@ class VideoPlayerValue { this.isPlaying = false, this.isLooping = false, this.isBuffering = false, + this.isDurationIndefinite = false, this.volume = 1.0, this.playbackSpeed = 1.0, this.errorDescription, @@ -75,6 +76,9 @@ class VideoPlayerValue { /// True if the video is currently buffering. final bool isBuffering; + /// True if the video has an undetermined duration, eg. a live stream. + final bool isDurationIndefinite; + /// The current volume of the playback. final double volume; @@ -122,6 +126,7 @@ class VideoPlayerValue { bool isPlaying, bool isLooping, bool isBuffering, + bool isDurationIndefinite, double volume, double playbackSpeed, String errorDescription, @@ -135,6 +140,7 @@ class VideoPlayerValue { isPlaying: isPlaying ?? this.isPlaying, isLooping: isLooping ?? this.isLooping, isBuffering: isBuffering ?? this.isBuffering, + isDurationIndefinite: isDurationIndefinite ?? this.isDurationIndefinite, volume: volume ?? this.volume, playbackSpeed: playbackSpeed ?? this.playbackSpeed, errorDescription: errorDescription ?? this.errorDescription, @@ -152,6 +158,7 @@ class VideoPlayerValue { 'isPlaying: $isPlaying, ' 'isLooping: $isLooping, ' 'isBuffering: $isBuffering, ' + 'isDurationIndefinite: $isDurationIndefinite, ' 'volume: $volume, ' 'playbackSpeed: $playbackSpeed, ' 'errorDescription: $errorDescription)'; @@ -292,6 +299,7 @@ class VideoPlayerController extends ValueNotifier { case VideoEventType.initialized: value = value.copyWith( duration: event.duration, + isDurationIndefinite: event.isDurationIndefinite, size: event.size, ); initializingCompleter.complete(null); diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 30272c4c6b1f..e2b662f818dd 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -15,19 +15,20 @@ flutter: pluginClass: VideoPlayerPlugin ios: pluginClass: FLTVideoPlayerPlugin - web: - default_package: video_player_web +# web: +# default_package: video_player_web dependencies: meta: ^1.0.5 - video_player_platform_interface: ^2.2.0 + video_player_platform_interface: # ^2.2.0 + path: ../video_player_platform_interface # The design on https://flutter.dev/go/federated-plugins was to leave # this constraint as "any". We cannot do it right now as it fails pub publish # validation, so we set a ^ constraint. # TODO(amirh): Revisit this (either update this part in the design or the pub tool). # https://github.com/flutter/flutter/issues/46264 - video_player_web: '>=0.1.4 <2.0.0' +# video_player_web: '>=0.1.4 <2.0.0' flutter: sdk: flutter diff --git a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart index 0ea443fb6e12..458d5ef57fab 100644 --- a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart +++ b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart @@ -105,6 +105,7 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { return VideoEvent( eventType: VideoEventType.initialized, duration: Duration(milliseconds: map['duration']), + isDurationIndefinite: map['isDurationIndefinite'], size: Size(map['width']?.toDouble() ?? 0.0, map['height']?.toDouble() ?? 0.0), ); diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 2757fb135af6..ec94ca726c39 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -218,6 +218,7 @@ class VideoEvent { VideoEvent({ @required this.eventType, this.duration, + this.isDurationIndefinite, this.size, this.buffered, }); @@ -229,6 +230,11 @@ class VideoEvent { /// /// Only used if [eventType] is [VideoEventType.initialized]. final Duration duration; + + /// Determines if the video has a defined duration + /// + /// Only used if [eventType] is [VideoEventType.initialized]. + final bool isDurationIndefinite; /// Size of the video. /// @@ -247,6 +253,7 @@ class VideoEvent { runtimeType == other.runtimeType && eventType == other.eventType && duration == other.duration && + isDurationIndefinite == other.isDurationIndefinite && size == other.size && listEquals(buffered, other.buffered); } @@ -255,6 +262,7 @@ class VideoEvent { int get hashCode => eventType.hashCode ^ duration.hashCode ^ + isDurationIndefinite.hashCode ^ size.hashCode ^ buffered.hashCode; } From 70b1af7e5a343a5ad359ac4e88cbebcece5af5ca Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 1 Oct 2020 13:49:03 +0200 Subject: [PATCH 3/4] Added isDurationIndefinite eventSink to iOS --- .../video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m index 062964b64a62..57665b991531 100644 --- a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m +++ b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m @@ -325,6 +325,7 @@ - (void)sendInitialized { _eventSink(@{ @"event" : @"initialized", @"duration" : @([self duration]), + @"isDurationIndefinite": @([self isDurationIndefinite]), @"width" : @(width), @"height" : @(height) }); From bbb19010c7be7d02a5f36ee4309867971a74552b Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 13 Oct 2020 11:50:26 +0200 Subject: [PATCH 4/4] WIP: Tests --- .../test_driver/video_player_test.dart | 16 +--- .../method_channel_video_player_test.dart | 80 +++++++++++++++++++ .../video_player/test/video_player_test.dart | 9 +++ .../lib/method_channel_video_player.dart | 5 +- 4 files changed, 93 insertions(+), 17 deletions(-) create mode 100644 packages/video_player/video_player/test/method_channel_video_player_test.dart diff --git a/packages/video_player/video_player/example/test_driver/video_player_test.dart b/packages/video_player/video_player/example/test_driver/video_player_test.dart index 4f26974b44b6..8ef9426b0faf 100644 --- a/packages/video_player/video_player/example/test_driver/video_player_test.dart +++ b/packages/video_player/video_player/example/test_driver/video_player_test.dart @@ -6,11 +6,10 @@ import 'dart:async'; import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; -import 'package:video_player/video_player.dart'; Future main() async { final FlutterDriver driver = await FlutterDriver.connect(); - VideoPlayerController _controller; + tearDownAll(() async { await driver.close(); }); @@ -27,17 +26,4 @@ Future main() async { final Health health = await driver.checkHealth(); expect(health.status, HealthStatus.ok); }, skip: 'Cirrus CI currently hangs while playing videos'); - - group('Livestream should have indefinite duration', () { - setUp(() async { - _controller = VideoPlayerController.network( - 'https://rtmp.api.rt.com/hls/rtdru.m3u8'); - }); - - test('Should initialize', () async { - await _controller.initialize(); - - expect(_controller.value.isDurationIndefinite, true); - }); - }); } diff --git a/packages/video_player/video_player/test/method_channel_video_player_test.dart b/packages/video_player/video_player/test/method_channel_video_player_test.dart new file mode 100644 index 000000000000..7b2b33787650 --- /dev/null +++ b/packages/video_player/video_player/test/method_channel_video_player_test.dart @@ -0,0 +1,80 @@ +import 'dart:async'; + +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/method_channel_video_player.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + +VideoEvent createVideoEvent({ + bool isIndefiniteStream, +}) { + if (isIndefiniteStream) { + return (VideoEvent( + eventType: VideoEventType.initialized, + duration: Duration(seconds: 0), + isDurationIndefinite: true, + )); + } else { + return (VideoEvent( + eventType: VideoEventType.unknown, + duration: Duration(seconds: 0), + isDurationIndefinite: false, + )); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Mock videoEvent', () { + final log = []; + + MethodChannelVideoPlayer methodChannelVideoPlayer; + + StreamSubscription videoEventStreamSubscription; + VideoPlayerController _controller; + + setUp(() async { + methodChannelVideoPlayer = MethodChannelVideoPlayer(); + + _controller = VideoPlayerController.network('https://127.0.0.1'); + + // Configure mock implementation for the EventChannel + MethodChannel(methodChannelVideoPlayer + .eventChannelFor(_controller.textureId) + .name) + .setMockMethodCallHandler((methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'listen': + await ServicesBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + methodChannelVideoPlayer + .eventChannelFor(_controller.textureId) + .name, + methodChannelVideoPlayer + .eventChannelFor(_controller.textureId) + .codec + .encodeSuccessEnvelope( + createVideoEvent(isIndefiniteStream: true)), + (_) {}); + break; + case 'cancel': + break; + default: + return null; + } + }); + }); + + tearDownAll(() async { + await videoEventStreamSubscription.cancel(); + }); + + test('Indefinite stream', () { + + expect(_controller.value.isDurationIndefinite, true); + }); + }); +} 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 35cab6204965..93f3f8133b6d 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -214,6 +214,15 @@ void main() { 'dash'); }); + test('network with stream', () async { + final VideoPlayerController controller = + VideoPlayerController.network('https://127.0.0.1/indefinite'); + + await controller.initialize(); + + expect(controller.value.isDurationIndefinite, true); + }); + test('init errors', () async { final VideoPlayerController controller = VideoPlayerController.network( 'http://testing.com/invalid_url', diff --git a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart index 458d5ef57fab..4b2d2fcc685f 100644 --- a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart +++ b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart @@ -96,7 +96,7 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { @override Stream videoEventsFor(int textureId) { - return _eventChannelFor(textureId) + return eventChannelFor(textureId) .receiveBroadcastStream() .map((dynamic event) { final Map map = event; @@ -142,7 +142,8 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { ); } - EventChannel _eventChannelFor(int textureId) { + /// Returns [EventChannel] for a specific texture id + EventChannel eventChannelFor(int textureId) { return EventChannel('flutter.io/videoPlayer/videoEvents$textureId'); }