diff --git a/mobile/lib/interfaces/cast_destination_service.interface.dart b/mobile/lib/interfaces/cast_destination_service.interface.dart index e00023d17fa..c649dacf963 100644 --- a/mobile/lib/interfaces/cast_destination_service.interface.dart +++ b/mobile/lib/interfaces/cast_destination_service.interface.dart @@ -5,8 +5,6 @@ abstract interface class ICastDestinationService { Future initialize(); CastDestinationType getType(); - bool isAvailable(); - void Function(bool)? onConnectionState; void Function(Duration)? onCurrentTime; @@ -15,12 +13,14 @@ abstract interface class ICastDestinationService { void Function(String)? onReceiverName; void Function(CastState)? onCastState; + Future connect(dynamic device); + void loadMedia(Asset asset, bool reload); void play(); void pause(); void seekTo(Duration position); - void disconnect(); + Future disconnect(); Future> getDevices(); } diff --git a/mobile/lib/models/cast/cast_manager_state.dart b/mobile/lib/models/cast/cast_manager_state.dart index 9f9c5190621..703ceb4c47e 100644 --- a/mobile/lib/models/cast/cast_manager_state.dart +++ b/mobile/lib/models/cast/cast_manager_state.dart @@ -11,7 +11,7 @@ class CastManagerState { final Duration currentTime; final Duration duration; - CastManagerState({ + const CastManagerState({ required this.isCasting, required this.receiverName, required this.castState, @@ -49,11 +49,12 @@ class CastManagerState { factory CastManagerState.fromMap(Map map) { return CastManagerState( - isCasting: map['isCasting'] ?? false, - receiverName: map['receiverName'] ?? '', - castState: map['castState'] ?? CastState.idle, - currentTime: Duration(seconds: map['currentTime']?.toInt() ?? 0), - duration: Duration(seconds: map['duration']?.toInt() ?? 0),); + isCasting: map['isCasting'] ?? false, + receiverName: map['receiverName'] ?? '', + castState: map['castState'] ?? CastState.idle, + currentTime: Duration(seconds: map['currentTime']?.toInt() ?? 0), + duration: Duration(seconds: map['duration']?.toInt() ?? 0), + ); } String toJson() => json.encode(toMap()); diff --git a/mobile/lib/models/sessions/session_create_response.model.dart b/mobile/lib/models/sessions/session_create_response.model.dart index 2eeeb51d42e..66b4c6c0714 100644 --- a/mobile/lib/models/sessions/session_create_response.model.dart +++ b/mobile/lib/models/sessions/session_create_response.model.dart @@ -8,7 +8,7 @@ class SessionCreateResponse { final String token; final String updatedAt; - SessionCreateResponse({ + const SessionCreateResponse({ required this.createdAt, required this.current, required this.deviceOS, diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart index 75eb278c5e5..8afa6ab4e3a 100644 --- a/mobile/lib/pages/common/native_video_viewer.page.dart +++ b/mobile/lib/pages/common/native_video_viewer.page.dart @@ -61,7 +61,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { final log = Logger('NativeVideoViewerPage'); - final cast = ref.watch(castProvider); + final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); Future createSource() async { if (!context.mounted) { @@ -394,7 +394,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { // This remains under the video to avoid flickering // For motion videos, this is the image portion of the asset Center(key: ValueKey(asset.id), child: image), - if (aspectRatio.value != null && !cast.isCasting) + if (aspectRatio.value != null && !isCasting) Visibility.maintain( key: ValueKey(asset), visible: isVisible.value, diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index a51694c0dbe..1bab1bef0f9 100644 --- a/mobile/lib/providers/cast.provider.dart +++ b/mobile/lib/providers/cast.provider.dart @@ -1,5 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/interfaces/cast_destination_service.interface.dart'; import 'package:immich_mobile/models/cast/cast_manager_state.dart'; import 'package:immich_mobile/services/gcast.service.dart'; @@ -8,13 +9,14 @@ final castProvider = StateNotifierProvider( ); class CastNotifier extends StateNotifier { - final GCastService _gCastService; + // more cast providers can be added here (ie Fcast) + final ICastDestinationService _gCastService; List<(String, CastDestinationType, dynamic)> discovered = List.empty(); CastNotifier(this._gCastService) : super( - CastManagerState( + const CastManagerState( isCasting: false, currentTime: Duration.zero, duration: Duration.zero, diff --git a/mobile/lib/services/gcast.service.dart b/mobile/lib/services/gcast.service.dart index d9c1d312855..f727cabce96 100644 --- a/mobile/lib/services/gcast.service.dart +++ b/mobile/lib/services/gcast.service.dart @@ -14,6 +14,7 @@ import 'package:immich_mobile/repositories/gcast.repository.dart'; import 'package:immich_mobile/repositories/sessions_api.repository.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/utils/url_helper.dart'; +// ignore: import_rule_openapi, we are only using the AssetMediaSize enum import 'package:openapi/api.dart'; final gCastServiceProvider = Provider( @@ -34,7 +35,6 @@ class GCastService implements ICastDestinationService { bool isConnected = false; int? _sessionId; Timer? _mediaStatusPollingTimer; - CastState? castState; @override void Function(bool)? onConnectionState; @@ -67,55 +67,58 @@ class GCastService implements ICastDestinationService { } void _onCastMessageCallback(Map message) { - final msgType = message['type']; + switch (message['type']) { + case "MEDIA_STATUS": + _handleMediaStatus(message); + break; + } + } - if (msgType == "MEDIA_STATUS") { - final statusList = (message['status'] as List) - .whereType>() - .toList(); + void _handleMediaStatus(Map message) { + final statusList = + (message['status'] as List).whereType>().toList(); - if (statusList.isEmpty) { - return; - } + if (statusList.isEmpty) { + return; + } - final status = statusList[0]; - switch (status['playerState']) { - case "PLAYING": - onCastState?.call(CastState.playing); - break; - case "PAUSED": - onCastState?.call(CastState.paused); - break; - case "BUFFERING": - onCastState?.call(CastState.buffering); - break; - case "IDLE": - onCastState?.call(CastState.idle); + final status = statusList[0]; + switch (status['playerState']) { + case "PLAYING": + onCastState?.call(CastState.playing); + break; + case "PAUSED": + onCastState?.call(CastState.paused); + break; + case "BUFFERING": + onCastState?.call(CastState.buffering); + break; + case "IDLE": + onCastState?.call(CastState.idle); - // stop polling for media status if the video finished playing - if (status["idleReason"] == "FINISHED") { - _mediaStatusPollingTimer?.cancel(); - } + // stop polling for media status if the video finished playing + if (status["idleReason"] == "FINISHED") { + _mediaStatusPollingTimer?.cancel(); + } - break; - } + break; + } - if (status["media"] != null && status["media"]["duration"] != null) { - final duration = Duration( - milliseconds: (status["media"]["duration"] * 1000 ?? 0).toInt(), - ); - onDuration?.call(duration); - } + if (status["media"] != null && status["media"]["duration"] != null) { + final duration = Duration( + milliseconds: (status["media"]["duration"] * 1000 ?? 0).toInt(), + ); + onDuration?.call(duration); + } - if (status["mediaSessionId"] != null) { - _sessionId = status["mediaSessionId"]; - } + if (status["mediaSessionId"] != null) { + _sessionId = status["mediaSessionId"]; + } - if (status["currentTime"] != null) { - final currentTime = - Duration(milliseconds: (status["currentTime"] * 1000 ?? 0).toInt()); - onCurrentTime?.call(currentTime); - } + if (status["currentTime"] != null) { + final currentTime = + Duration(milliseconds: (status["currentTime"] * 1000 ?? 0).toInt()); + onCurrentTime?.call(currentTime); } } @@ -139,20 +142,11 @@ class GCastService implements ICastDestinationService { } @override - void disconnect() { - _gCastRepository.disconnect(); - + Future disconnect() async { + await _gCastRepository.disconnect(); onReceiverName?.call(""); } - @override - bool isAvailable() { - // check if server URL is https - final serverUrl = punycodeDecodeUrl(Store.tryGet(StoreKey.serverEndpoint)); - - return serverUrl?.startsWith("https://") ?? false; - } - bool isSessionValid() { // check if we already have a session token // we should always have a expiration date @@ -220,6 +214,8 @@ class GCastService implements ICastDestinationService { "autoplay": true, }); + currentAssetId = asset.remoteId; + // we need to poll for media status since the cast device does not // send a message when the media is loaded for whatever reason // only do this on videos diff --git a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart index b87b761d0c3..d64e5071709 100644 --- a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart +++ b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart @@ -78,14 +78,15 @@ class CustomVideoPlayerControls extends HookConsumerWidget { } ref.read(castProvider.notifier).loadMedia(asset, true); } + return; + } + + if (state == VideoPlaybackState.playing) { + ref.read(videoPlayerControlsProvider.notifier).pause(); + } else if (state == VideoPlaybackState.completed) { + ref.read(videoPlayerControlsProvider.notifier).restart(); } else { - if (state == VideoPlaybackState.playing) { - ref.read(videoPlayerControlsProvider.notifier).pause(); - } else if (state == VideoPlaybackState.completed) { - ref.read(videoPlayerControlsProvider.notifier).restart(); - } else { - ref.read(videoPlayerControlsProvider.notifier).play(); - } + ref.read(videoPlayerControlsProvider.notifier).play(); } } diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart index 4b841fad968..52504a27dfc 100644 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart @@ -46,7 +46,7 @@ class TopControlAppBar extends HookConsumerWidget { const double iconSize = 22.0; final a = ref.watch(assetWatcher(asset)).value ?? asset; final album = ref.watch(currentAlbumProvider); - final castManager = ref.watch(castProvider); + final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); final comments = album != null && album.remoteId != null && asset.remoteId != null @@ -181,9 +181,7 @@ class TopControlAppBar extends HookConsumerWidget { ); }, icon: Icon( - castManager.isCasting - ? Icons.cast_connected_rounded - : Icons.cast_rounded, + isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, size: 20.0, color: Colors.grey[200], ), diff --git a/mobile/lib/widgets/asset_viewer/video_position.dart b/mobile/lib/widgets/asset_viewer/video_position.dart index d5e5bea0965..0e90669fe3d 100644 --- a/mobile/lib/widgets/asset_viewer/video_position.dart +++ b/mobile/lib/widgets/asset_viewer/video_position.dart @@ -71,16 +71,16 @@ class VideoPosition extends HookConsumerWidget { ref .read(castProvider.notifier) .seekTo(seekToDuration); - } else { - ref - .read(videoPlayerControlsProvider.notifier) - .position = seekToDuration.inSeconds.toDouble(); - - // This immediately updates the slider position without waiting for the video to update - ref - .read(videoPlaybackValueProvider.notifier) - .position = seekToDuration; + return; } + + ref + .read(videoPlayerControlsProvider.notifier) + .position = seekToDuration.inSeconds.toDouble(); + + // This immediately updates the slider position without waiting for the video to update + ref.read(videoPlaybackValueProvider.notifier).position = + seekToDuration; }, ), ),