Solved seconds/milliseconds on iOS and added seekTo to navigate through the video stream
This commit is contained in:
parent
ea9b025465
commit
67552c43dd
10 changed files with 130 additions and 4 deletions
|
|
@ -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<>(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -294,6 +294,7 @@ abstract class CastHostApi {
|
|||
void stop();
|
||||
void showTracksChooserDialog();
|
||||
void skipAd();
|
||||
void seekTo(int position);
|
||||
//endregion
|
||||
|
||||
//region RemoteMediaClient Queue
|
||||
|
|
|
|||
Loading…
Reference in a new issue