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 seekTo(@NonNull Long position);
void queueAppendItem(@NonNull MediaQueueItem item);
void queueNextItem();
@ -1839,6 +1841,33 @@ public class PlatformBridgeApis {
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 =
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.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

View file

@ -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)

View file

@ -311,6 +311,7 @@ NSObject<FlutterMessageCodec> *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;

View file

@ -747,6 +747,25 @@ void CastHostApiSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<CastH
[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 alloc]

View file

@ -3,6 +3,7 @@ import UIKit
import GoogleCast
public class SwiftFlutterCastFrameworkPlugin: NSObject, FlutterPlugin, GCKSessionManagerListener, CastHostApi, GCKRemoteMediaClientListener, GCKMediaQueueDelegate {
public static func register(with registrar: FlutterPluginRegistrar) {
let messenger : FlutterBinaryMessenger = registrar.messenger()
let flutterApi = CastFlutterApi.init(binaryMessenger: messenger)
@ -302,6 +303,15 @@ public class SwiftFlutterCastFrameworkPlugin: NSObject, FlutterPlugin, GCKSessio
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? {
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 {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.CastHostApi.queueAppendItem', codec,

View file

@ -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<MediaInfo> getMediaInfo() async {
// 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';
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<MiniControllerProgress> createState() => _MiniControllerProgressState();
}
class _MiniControllerProgressState extends State<MiniControllerProgress> {
double? _newValue;
@override
Widget build(BuildContext context) {
var remoteMediaClient =
this.castFramework.castContext.sessionManager.remoteMediaClient;
final remoteMediaClient =
this.widget.castFramework.castContext.sessionManager.remoteMediaClient;
return StreamBuilder<ProgressInfo>(
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);
}
}

View file

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