Solved seconds/milliseconds on iOS and added seekTo to navigate through the video stream

This commit is contained in:
Angelo Cassano 2023-03-28 17:52:12 +02:00 committed by GitHub
parent ea9b025465
commit 67552c43dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 130 additions and 4 deletions

View file

@ -1572,6 +1572,8 @@ public class PlatformBridgeApis {
void skipAd(); void skipAd();
void seekTo(@NonNull Long position);
void queueAppendItem(@NonNull MediaQueueItem item); void queueAppendItem(@NonNull MediaQueueItem item);
void queueNextItem(); void queueNextItem();
@ -1839,6 +1841,33 @@ public class PlatformBridgeApis {
channel.setMessageHandler(null); channel.setMessageHandler(null);
} }
} }
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.CastHostApi.seekTo", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
try {
ArrayList<Object> args = (ArrayList<Object>) 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<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
{ {
BasicMessageChannel<Object> channel = BasicMessageChannel<Object> channel =
new BasicMessageChannel<>( new BasicMessageChannel<>(

View file

@ -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.cast.MessageCastingChannel
import com.gianlucaparadise.flutter_cast_framework.media.* import com.gianlucaparadise.flutter_cast_framework.media.*
import com.google.android.gms.cast.MediaError 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.MediaStatus.*
import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastSession import com.google.android.gms.cast.framework.CastSession
@ -394,6 +396,14 @@ class FlutterCastFrameworkPlugin : FlutterPlugin, MethodCallHandler, ActivityAwa
remoteMediaClient.skipAd() 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) { override fun queueAppendItem(item: PlatformBridgeApis.MediaQueueItem) {
val remoteMediaClient: RemoteMediaClient = remoteMediaClient ?: return val remoteMediaClient: RemoteMediaClient = remoteMediaClient ?: return

View file

@ -17,7 +17,7 @@ let kThumbnailHeight = 720
func getMediaLoadRequest(request: MediaLoadRequestData) -> GCKMediaLoadRequestData { func getMediaLoadRequest(request: MediaLoadRequestData) -> GCKMediaLoadRequestData {
let mediaRequestBuilder = GCKMediaLoadRequestDataBuilder.init() let mediaRequestBuilder = GCKMediaLoadRequestDataBuilder.init()
mediaRequestBuilder.autoplay = request.shouldAutoplay mediaRequestBuilder.autoplay = request.shouldAutoplay
mediaRequestBuilder.startTime = request.currentTime?.doubleValue ?? 0 mediaRequestBuilder.startTime = (request.currentTime?.doubleValue ?? 0) / 1000
mediaRequestBuilder.mediaInformation = getMediaInfo(mediaInfo: request.mediaInfo) mediaRequestBuilder.mediaInformation = getMediaInfo(mediaInfo: request.mediaInfo)

View file

@ -311,6 +311,7 @@ NSObject<FlutterMessageCodec> *CastHostApiGetCodec(void);
- (void)stopWithError:(FlutterError *_Nullable *_Nonnull)error; - (void)stopWithError:(FlutterError *_Nullable *_Nonnull)error;
- (void)showTracksChooserDialogWithError:(FlutterError *_Nullable *_Nonnull)error; - (void)showTracksChooserDialogWithError:(FlutterError *_Nullable *_Nonnull)error;
- (void)skipAdWithError:(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)queueAppendItemItem:(MediaQueueItem *)item error:(FlutterError *_Nullable *_Nonnull)error;
- (void)queueNextItemWithError:(FlutterError *_Nullable *_Nonnull)error; - (void)queueNextItemWithError:(FlutterError *_Nullable *_Nonnull)error;
- (void)queuePrevItemWithError:(FlutterError *_Nullable *_Nonnull)error; - (void)queuePrevItemWithError:(FlutterError *_Nullable *_Nonnull)error;

View file

@ -747,6 +747,25 @@ void CastHostApiSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<CastH
[channel setMessageHandler:nil]; [channel setMessageHandler:nil];
} }
} }
{
FlutterBasicMessageChannel *channel =
[[FlutterBasicMessageChannel alloc]
initWithName:@"dev.flutter.pigeon.CastHostApi.seekTo"
binaryMessenger:binaryMessenger
codec:CastHostApiGetCodec()];
if (api) {
NSCAssert([api respondsToSelector:@selector(seekToPosition:error:)], @"CastHostApi api (%@) doesn't respond to @selector(seekToPosition:error:)", api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
NSArray *args = message;
NSNumber *arg_position = GetNullableObjectAtIndex(args, 0);
FlutterError *error;
[api seekToPosition:arg_position error:&error];
callback(wrapResult(nil, error));
}];
} else {
[channel setMessageHandler:nil];
}
}
{ {
FlutterBasicMessageChannel *channel = FlutterBasicMessageChannel *channel =
[[FlutterBasicMessageChannel alloc] [[FlutterBasicMessageChannel alloc]

View file

@ -3,6 +3,7 @@ import UIKit
import GoogleCast import GoogleCast
public class SwiftFlutterCastFrameworkPlugin: NSObject, FlutterPlugin, GCKSessionManagerListener, CastHostApi, GCKRemoteMediaClientListener, GCKMediaQueueDelegate { public class SwiftFlutterCastFrameworkPlugin: NSObject, FlutterPlugin, GCKSessionManagerListener, CastHostApi, GCKRemoteMediaClientListener, GCKMediaQueueDelegate {
public static func register(with registrar: FlutterPluginRegistrar) { public static func register(with registrar: FlutterPluginRegistrar) {
let messenger : FlutterBinaryMessenger = registrar.messenger() let messenger : FlutterBinaryMessenger = registrar.messenger()
let flutterApi = CastFlutterApi.init(binaryMessenger: messenger) let flutterApi = CastFlutterApi.init(binaryMessenger: messenger)
@ -302,6 +303,15 @@ public class SwiftFlutterCastFrameworkPlugin: NSObject, FlutterPlugin, GCKSessio
castSession?.setDeviceMuted(isMuted) castSession?.setDeviceMuted(isMuted)
} }
public func seek(toPosition position: NSNumber, error: AutoreleasingUnsafeMutablePointer<FlutterError?>) {
let options = GCKMediaSeekOptions()
options.interval = position.doubleValue / 1000
options.resumeState = .unchanged
remoteMediaClient?.seek(with: options)
}
public func getCastDeviceWithError(_ error: AutoreleasingUnsafeMutablePointer<FlutterError?>) -> CastDevice? { public func getCastDeviceWithError(_ error: AutoreleasingUnsafeMutablePointer<FlutterError?>) -> CastDevice? {
let castDevice = castSession?.device let castDevice = castSession?.device

View file

@ -912,6 +912,28 @@ class CastHostApi {
} }
} }
Future<void> seekTo(int arg_progress) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.CastHostApi.seekTo', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_progress]) as List<Object?>?;
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<void> queueAppendItem(MediaQueueItem arg_item) async { Future<void> queueAppendItem(MediaQueueItem arg_item) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.CastHostApi.queueAppendItem', codec, 'dev.flutter.pigeon.CastHostApi.queueAppendItem', codec,

View file

@ -97,6 +97,11 @@ class RemoteMediaClient {
_hostApi.stop(); _hostApi.stop();
} }
/// Seeks the playback to the position.
void seekTo(int position) {
_hostApi.seekTo(position);
}
/// Returns the current media information /// Returns the current media information
Future<MediaInfo> getMediaInfo() async { Future<MediaInfo> getMediaInfo() async {
// FIXME: can remove future? we could avoid to call host and rely on listener callbacks (maybe onMetadataUpdated) // FIXME: can remove future? we could avoid to call host and rely on listener callbacks (maybe onMetadataUpdated)

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import '../../../../cast.dart'; import '../../../../cast.dart';
class MiniControllerProgress extends StatelessWidget { class MiniControllerProgress extends StatefulWidget {
final FlutterCastFramework castFramework; final FlutterCastFramework castFramework;
const MiniControllerProgress({ const MiniControllerProgress({
@ -10,10 +10,18 @@ class MiniControllerProgress extends StatelessWidget {
required this.castFramework, required this.castFramework,
}) : super(key: key); }) : super(key: key);
@override
State<MiniControllerProgress> createState() => _MiniControllerProgressState();
}
class _MiniControllerProgressState extends State<MiniControllerProgress> {
double? _newValue;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var remoteMediaClient = final remoteMediaClient =
this.castFramework.castContext.sessionManager.remoteMediaClient; this.widget.castFramework.castContext.sessionManager.remoteMediaClient;
return StreamBuilder<ProgressInfo>( return StreamBuilder<ProgressInfo>(
stream: remoteMediaClient.progressStream, stream: remoteMediaClient.progressStream,
@ -28,6 +36,18 @@ class MiniControllerProgress extends StatelessWidget {
// this is the denominator, can't be 0 // this is the denominator, can't be 0
final durationFix = duration == 0 ? 1 : duration; final durationFix = duration == 0 ? 1 : duration;
progressPercent = progress / durationFix; 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 { } else {
progressPercent = 0; 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);
}
} }

View file

@ -294,6 +294,7 @@ abstract class CastHostApi {
void stop(); void stop();
void showTracksChooserDialog(); void showTracksChooserDialog();
void skipAd(); void skipAd();
void seekTo(int position);
//endregion //endregion
//region RemoteMediaClient Queue //region RemoteMediaClient Queue