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 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<>(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue