From 67552c43ddd2a6f62e389d842d4b48327e6164ed Mon Sep 17 00:00:00 2001 From: Angelo Cassano Date: Tue, 28 Mar 2023 17:52:12 +0200 Subject: [PATCH] Solved seconds/milliseconds on iOS and added seekTo to navigate through the video stream --- .../PlatformBridgeApis.java | 29 +++++++++++++++ .../FlutterCastFrameworkPlugin.kt | 10 ++++++ .../HostMediaLoadRequestDataHelper.swift | 2 +- ios/Classes/PlatformBridgeApis.h | 1 + ios/Classes/PlatformBridgeApis.m | 19 ++++++++++ .../SwiftFlutterCastFrameworkPlugin.swift | 10 ++++++ lib/src/PlatformBridgeApis.dart | 22 ++++++++++++ lib/src/cast/RemoteMediaClient.dart | 5 +++ .../MiniControllerProgress.dart | 35 +++++++++++++++++-- pigeon/PlatformBridgeApisDefinition.dart | 1 + 10 files changed, 130 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/gianlucaparadise/flutter_cast_framework/PlatformBridgeApis.java b/android/src/main/java/com/gianlucaparadise/flutter_cast_framework/PlatformBridgeApis.java index 470caf6..6fa3260 100644 --- a/android/src/main/java/com/gianlucaparadise/flutter_cast_framework/PlatformBridgeApis.java +++ b/android/src/main/java/com/gianlucaparadise/flutter_cast_framework/PlatformBridgeApis.java @@ -1572,6 +1572,8 @@ public class PlatformBridgeApis { void skipAd(); + void seekTo(@NonNull Long position); + void queueAppendItem(@NonNull MediaQueueItem item); void queueNextItem(); @@ -1839,6 +1841,33 @@ public class PlatformBridgeApis { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.CastHostApi.seekTo", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + ArrayList args = (ArrayList) message; + assert args != null; + Number positionArg = (Number) args.get(0); + if (positionArg == null) { + throw new NullPointerException("positionArg unexpectedly null."); + } + api.seekTo((positionArg == null) ? null : positionArg.longValue()); + wrapped.add(0, null); + } catch (Error | RuntimeException exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/FlutterCastFrameworkPlugin.kt b/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/FlutterCastFrameworkPlugin.kt index 16d67fd..b6cc15f 100644 --- a/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/FlutterCastFrameworkPlugin.kt +++ b/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/FlutterCastFrameworkPlugin.kt @@ -12,6 +12,8 @@ import com.gianlucaparadise.flutter_cast_framework.cast.CastDialogOpener import com.gianlucaparadise.flutter_cast_framework.cast.MessageCastingChannel import com.gianlucaparadise.flutter_cast_framework.media.* import com.google.android.gms.cast.MediaError +import com.google.android.gms.cast.MediaSeekOptions +import com.google.android.gms.cast.MediaSeekOptions.ResumeState import com.google.android.gms.cast.MediaStatus.* import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastSession @@ -394,6 +396,14 @@ class FlutterCastFrameworkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwa remoteMediaClient.skipAd() } + override fun seekTo(position: Long) { + val remoteMediaClient: RemoteMediaClient = remoteMediaClient ?: return + remoteMediaClient.seek(MediaSeekOptions.Builder() + .setPosition(position) + .setResumeState(MediaSeekOptions.RESUME_STATE_UNCHANGED) + .build()); + } + override fun queueAppendItem(item: PlatformBridgeApis.MediaQueueItem) { val remoteMediaClient: RemoteMediaClient = remoteMediaClient ?: return diff --git a/ios/Classes/HostMediaLoadRequestDataHelper.swift b/ios/Classes/HostMediaLoadRequestDataHelper.swift index f009292..952f469 100644 --- a/ios/Classes/HostMediaLoadRequestDataHelper.swift +++ b/ios/Classes/HostMediaLoadRequestDataHelper.swift @@ -17,7 +17,7 @@ let kThumbnailHeight = 720 func getMediaLoadRequest(request: MediaLoadRequestData) -> GCKMediaLoadRequestData { let mediaRequestBuilder = GCKMediaLoadRequestDataBuilder.init() mediaRequestBuilder.autoplay = request.shouldAutoplay - mediaRequestBuilder.startTime = request.currentTime?.doubleValue ?? 0 + mediaRequestBuilder.startTime = (request.currentTime?.doubleValue ?? 0) / 1000 mediaRequestBuilder.mediaInformation = getMediaInfo(mediaInfo: request.mediaInfo) diff --git a/ios/Classes/PlatformBridgeApis.h b/ios/Classes/PlatformBridgeApis.h index 57ba1fe..2cc57e4 100644 --- a/ios/Classes/PlatformBridgeApis.h +++ b/ios/Classes/PlatformBridgeApis.h @@ -311,6 +311,7 @@ NSObject *CastHostApiGetCodec(void); - (void)stopWithError:(FlutterError *_Nullable *_Nonnull)error; - (void)showTracksChooserDialogWithError:(FlutterError *_Nullable *_Nonnull)error; - (void)skipAdWithError:(FlutterError *_Nullable *_Nonnull)error; +- (void)seekToPosition:(NSNumber *)position error:(FlutterError *_Nullable *_Nonnull)error; - (void)queueAppendItemItem:(MediaQueueItem *)item error:(FlutterError *_Nullable *_Nonnull)error; - (void)queueNextItemWithError:(FlutterError *_Nullable *_Nonnull)error; - (void)queuePrevItemWithError:(FlutterError *_Nullable *_Nonnull)error; diff --git a/ios/Classes/PlatformBridgeApis.m b/ios/Classes/PlatformBridgeApis.m index f6c543c..3d648d9 100644 --- a/ios/Classes/PlatformBridgeApis.m +++ b/ios/Classes/PlatformBridgeApis.m @@ -747,6 +747,25 @@ void CastHostApiSetup(id binaryMessenger, NSObject) { + let options = GCKMediaSeekOptions() + options.interval = position.doubleValue / 1000 + options.resumeState = .unchanged + + remoteMediaClient?.seek(with: options) + } + + public func getCastDeviceWithError(_ error: AutoreleasingUnsafeMutablePointer) -> CastDevice? { let castDevice = castSession?.device diff --git a/lib/src/PlatformBridgeApis.dart b/lib/src/PlatformBridgeApis.dart index 387adaa..1ec0705 100644 --- a/lib/src/PlatformBridgeApis.dart +++ b/lib/src/PlatformBridgeApis.dart @@ -912,6 +912,28 @@ class CastHostApi { } } + Future seekTo(int arg_progress) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CastHostApi.seekTo', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_progress]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + Future queueAppendItem(MediaQueueItem arg_item) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.CastHostApi.queueAppendItem', codec, diff --git a/lib/src/cast/RemoteMediaClient.dart b/lib/src/cast/RemoteMediaClient.dart index b4aa380..b6ca5fd 100644 --- a/lib/src/cast/RemoteMediaClient.dart +++ b/lib/src/cast/RemoteMediaClient.dart @@ -97,6 +97,11 @@ class RemoteMediaClient { _hostApi.stop(); } + /// Seeks the playback to the position. + void seekTo(int position) { + _hostApi.seekTo(position); + } + /// Returns the current media information Future getMediaInfo() async { // FIXME: can remove future? we could avoid to call host and rely on listener callbacks (maybe onMetadataUpdated) diff --git a/lib/src/cast/widgets/mini_controller/MiniControllerProgress.dart b/lib/src/cast/widgets/mini_controller/MiniControllerProgress.dart index 7ce188e..659542f 100644 --- a/lib/src/cast/widgets/mini_controller/MiniControllerProgress.dart +++ b/lib/src/cast/widgets/mini_controller/MiniControllerProgress.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import '../../../../cast.dart'; -class MiniControllerProgress extends StatelessWidget { +class MiniControllerProgress extends StatefulWidget { final FlutterCastFramework castFramework; const MiniControllerProgress({ @@ -10,10 +10,18 @@ class MiniControllerProgress extends StatelessWidget { required this.castFramework, }) : super(key: key); + @override + State createState() => _MiniControllerProgressState(); +} + +class _MiniControllerProgressState extends State { + + double? _newValue; + @override Widget build(BuildContext context) { - var remoteMediaClient = - this.castFramework.castContext.sessionManager.remoteMediaClient; + final remoteMediaClient = + this.widget.castFramework.castContext.sessionManager.remoteMediaClient; return StreamBuilder( stream: remoteMediaClient.progressStream, @@ -28,6 +36,18 @@ class MiniControllerProgress extends StatelessWidget { // this is the denominator, can't be 0 final durationFix = duration == 0 ? 1 : duration; progressPercent = progress / durationFix; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 48.0), + child: Slider( + activeColor: Colors.red, + inactiveColor: Colors.white70, + value: _newValue ?? progressPercent, + onChangeStart: (value) => setState(() => _newValue = value), + onChangeEnd: (value) => _onChangeEnd(value, duration), + onChanged: (value) => setState(() => _newValue = value), + ), + ); } else { progressPercent = 0; } @@ -40,4 +60,13 @@ class MiniControllerProgress extends StatelessWidget { }, ); } + + void _onChangeEnd(double value, int duration) { + final remoteMediaClient = + this.widget.castFramework.castContext.sessionManager.remoteMediaClient; + + remoteMediaClient.seekTo((value * duration).toInt()); + + setState(() => _newValue = null); + } } diff --git a/pigeon/PlatformBridgeApisDefinition.dart b/pigeon/PlatformBridgeApisDefinition.dart index b7662cc..80bddd5 100644 --- a/pigeon/PlatformBridgeApisDefinition.dart +++ b/pigeon/PlatformBridgeApisDefinition.dart @@ -294,6 +294,7 @@ abstract class CastHostApi { void stop(); void showTracksChooserDialog(); void skipAd(); + void seekTo(int position); //endregion //region RemoteMediaClient Queue