diff --git a/docs/docs/features/casting.md b/docs/docs/features/casting.md
index 2a6785dc6c..708af7b194 100644
--- a/docs/docs/features/casting.md
+++ b/docs/docs/features/casting.md
@@ -1,19 +1,23 @@
-# Chromecast support
+# Casting support
-Immich supports the Google's Cast protocol so that photos and videos can be cast to devices such as a Chromecast and a Nest Hub. This feature is considered experimental and has several important limitations listed below. Currently, this feature is only supported by the web client, support on Android and iOS is planned for the future.
+Immich supports both the FCast and Google Cast protocols, meaning photos and videos in Immich can be cast to devices such as a Chromecast, Nest Hub, or an FCast receiver. This feature is considered experimental and has several important limitations listed below. This feature is supported by the web client, as well as the mobile app via FCast.
-## Enable Google Cast Support
+## Mobile FCast support
-Google Cast support is disabled by default. The web UI uses Google-provided scripts and must retrieve them from Google servers when the page loads. This is a privacy concern for some and is thus opt-in.
+The mobile app also supports FCast for casting media to both FCast receivers and Google Cast devices such as a Google TV or Nest Hub. Devices are automatically discovered over your local network.
-You can enable Google Cast support through `Account Settings > Features > Cast > Google Cast`
+## Enable web support
+
+Casting from the web client is currently only possible via Google Cast, so enabling web support means enabling Google Cast. Google Cast support is disabled by default, since it requires the web UI to load Google-provided scripts from Google's servers when the page loads. This is a privacy concern for some and is thus opt-in.
+
+You can enable Google Cast support through `Account Settings > Features > Cast > Google Cast`.
## Limitations
-To use casting with Immich, there are a few prerequisites:
+To use Google Cast with Immich, there are a few prerequisites:
1. Your instance must be accessed via an HTTPS connection in order for the casting menu to show.
-2. Your instance must be publicly accessible via HTTPS and a DNS record for the server must be accessible via Google's DNS servers (`8.8.8.8` and `8.8.4.4`)
-3. Videos must be in a format that is compatible with Google Cast. For more info, check out [Google's documentation](https://developers.google.com/cast/docs/media)
+2. Your instance must be publicly accessible via HTTPS and a DNS record for the server must be accessible via Google's DNS servers (`8.8.8.8` and `8.8.4.4`).
+3. Videos must be in a format that is compatible with Google Cast. For more info, check out [Google's documentation](https://developers.google.com/cast/docs/media).
diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock
index 75eda67ce1..db2e58f1c9 100644
--- a/mobile/ios/Podfile.lock
+++ b/mobile/ios/Podfile.lock
@@ -1,7 +1,4 @@
PODS:
- - bonsoir_darwin (0.0.1):
- - Flutter
- - FlutterMacOS
- cupertino_http (0.0.1):
- Flutter
- FlutterMacOS
@@ -24,7 +21,6 @@ PODS:
- share_handler_ios_models (0.0.9)
DEPENDENCIES:
- - bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
- Flutter (from `Flutter`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
@@ -35,8 +31,6 @@ DEPENDENCIES:
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
EXTERNAL SOURCES:
- bonsoir_darwin:
- :path: ".symlinks/plugins/bonsoir_darwin/darwin"
cupertino_http:
:path: ".symlinks/plugins/cupertino_http/darwin"
Flutter:
@@ -55,7 +49,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_handler_ios/ios/Models"
SPEC CHECKSUMS:
- bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index 31feda2e11..4e96aaeac2 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -127,6 +127,7 @@
_googlecast._tcp
_CC1AD845._googlecast._tcp
+ _fcast._tcp
NSCameraUsageDescription
We need to access the camera to let you take beautiful video using this app
diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart
index c2a13c3ec2..f5586daeeb 100644
--- a/mobile/lib/constants/constants.dart
+++ b/mobile/lib/constants/constants.dart
@@ -55,3 +55,9 @@ const int kLibraryTabIndex = 3;
// Workaround for SQLite's variable limit (SQLITE_MAX_VARIABLE_NUMBER = 32766)
const int kDriftMaxChunk = 32000;
+
+// cast constants
+const String kCastDeviceType = "Cast";
+const String kCastDeviceOS = "Cast";
+const Duration kCastSessionDuration = Duration(minutes: 15);
+const Duration kCastSessionRenewalBuffer = Duration(minutes: 1);
diff --git a/mobile/lib/models/cast/cast_manager_state.dart b/mobile/lib/models/cast/cast_manager_state.dart
index 0fd3dc877b..50b95734a2 100644
--- a/mobile/lib/models/cast/cast_manager_state.dart
+++ b/mobile/lib/models/cast/cast_manager_state.dart
@@ -1,16 +1,31 @@
+import 'package:fcast_sender_sdk/fcast_sender_sdk.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'cast_manager_state.freezed.dart';
-enum CastDestinationType { googleCast }
+enum CastDestinationType { googleCast, fCast }
-enum CastState { idle, playing, paused, buffering }
+enum CastState {
+ idle,
+ playing,
+ paused,
+ buffering;
+
+ static CastState fromPlaybackState(PlaybackState state) => switch (state) {
+ PlaybackState.playing => CastState.playing,
+ PlaybackState.paused => CastState.paused,
+ PlaybackState.buffering => CastState.buffering,
+ PlaybackState.idle || PlaybackState.ended => CastState.idle,
+ };
+}
+
+typedef CastDestination = (String name, CastDestinationType type, dynamic device);
@freezed
abstract class CastManagerState with _$CastManagerState {
const factory CastManagerState({
required bool isCasting,
- required String receiverName,
+ required String? receiverName,
required CastState castState,
required Duration currentTime,
required Duration duration,
diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart
index 776888146b..eaa700b46b 100644
--- a/mobile/lib/providers/cast.provider.dart
+++ b/mobile/lib/providers/cast.provider.dart
@@ -3,33 +3,30 @@ import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/models/cast/cast_manager_state.dart';
-import 'package:immich_mobile/services/gcast.service.dart';
+import 'package:immich_mobile/services/cast.service.dart';
final castProvider = StateNotifierProvider(
- (ref) => CastNotifier(ref.watch(gCastServiceProvider)),
+ (ref) => CastNotifier(ref.watch(castServiceProvider)),
);
class CastNotifier extends StateNotifier {
- // more cast providers can be added here (ie Fcast)
- final GCastService _gCastService;
+ final CastService _castService;
- List<(String, CastDestinationType, dynamic)> discovered = List.empty();
-
- CastNotifier(this._gCastService)
+ CastNotifier(this._castService)
: super(
const CastManagerState(
isCasting: false,
currentTime: Duration.zero,
duration: Duration.zero,
- receiverName: '',
+ receiverName: null,
castState: CastState.idle,
),
) {
- _gCastService.onConnectionState = _onConnectionState;
- _gCastService.onCurrentTime = _onCurrentTime;
- _gCastService.onDuration = _onDuration;
- _gCastService.onReceiverName = _onReceiverName;
- _gCastService.onCastState = _onCastState;
+ _castService.onConnectionState = _onConnectionState;
+ _castService.onCurrentTime = _onCurrentTime;
+ _castService.onDuration = _onDuration;
+ _castService.onReceiverName = _onReceiverName;
+ _castService.onCastState = _onCastState;
}
void _onConnectionState(bool isCasting) {
@@ -44,7 +41,7 @@ class CastNotifier extends StateNotifier {
state = state.copyWith(duration: duration);
}
- void _onReceiverName(String receiverName) {
+ void _onReceiverName(String? receiverName) {
state = state.copyWith(receiverName: receiverName);
}
@@ -53,22 +50,15 @@ class CastNotifier extends StateNotifier {
}
void loadMedia(RemoteAsset asset, bool reload) {
- unawaited(_gCastService.loadMedia(asset, reload));
+ unawaited(_castService.loadMedia(asset, reload));
}
- Future connect(CastDestinationType type, dynamic device) async {
- switch (type) {
- case CastDestinationType.googleCast:
- await _gCastService.connect(device);
- }
+ Future connect(dynamic device) async {
+ await _castService.connect(device);
}
- Future> getDevices() async {
- if (discovered.isEmpty) {
- discovered = await _gCastService.getDevices();
- }
-
- return discovered;
+ Future> getDevices() {
+ return _castService.getDevices();
}
void toggle() {
@@ -82,22 +72,22 @@ class CastNotifier extends StateNotifier {
}
void play() {
- _gCastService.play();
+ _castService.play();
}
void pause() {
- _gCastService.pause();
+ _castService.pause();
}
void seekTo(Duration position) {
- _gCastService.seekTo(position);
+ _castService.seekTo(position);
}
void stop() {
- _gCastService.stop();
+ _castService.stop();
}
Future disconnect() async {
- await _gCastService.disconnect();
+ await _castService.disconnect();
}
}
diff --git a/mobile/lib/repositories/cast.repository.dart b/mobile/lib/repositories/cast.repository.dart
new file mode 100644
index 0000000000..f36ec4d6ad
--- /dev/null
+++ b/mobile/lib/repositories/cast.repository.dart
@@ -0,0 +1,96 @@
+import 'dart:async';
+import 'package:fcast_sender_sdk/fcast_sender_sdk.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+
+final castRepositoryProvider = Provider((_) => CastRepository());
+
+class CastRepository {
+ CastContext? _castContext;
+ CastingDevice? _activeDevice;
+
+ void Function(DeviceConnectionState)? onConnectionState;
+ void Function(DeviceEvent)? onDeviceEvent;
+
+ final Map<(String, ProtocolType), (DeviceInfo, int?)> _discoveredDevices = {};
+ int _currentDeviceGeneration = 0;
+ late final Future _initialized;
+
+ void init() {
+ _initialized = _initialize();
+ }
+
+ Future connect(DeviceInfo deviceInfo) async {
+ _activeDevice?.disconnect();
+ final device = _castContext!.createDeviceFromInfo(info: deviceInfo);
+ _activeDevice = device;
+
+ _currentDeviceGeneration += 1;
+ final thisDeviceGeneration = _currentDeviceGeneration;
+ device.connect(
+ eventHandler: DeviceEventHandler(
+ onEvent: (event) {
+ if (thisDeviceGeneration != _currentDeviceGeneration) {
+ return;
+ }
+
+ if (event is DeviceEvent_ConnectionStateChanged) {
+ onConnectionState?.call(event.newState);
+ }
+
+ onDeviceEvent?.call(event);
+ },
+ ),
+ reconnectIntervalMillis: 1000,
+ );
+ }
+
+ Future disconnect() async {
+ final previousDevice = _activeDevice;
+ if (previousDevice == null) {
+ return;
+ }
+
+ _activeDevice = null;
+ _currentDeviceGeneration += 1;
+
+ if (previousDevice.isReady()) {
+ previousDevice.stopPlayback();
+
+ await Future.delayed(const Duration(milliseconds: 500));
+ }
+
+ previousDevice.disconnect();
+ onConnectionState?.call(const DeviceConnectionState.disconnected());
+ }
+
+ void loadMedia(LoadRequest request) => _activeDevice?.load(request: request);
+ void play() => _activeDevice?.resumePlayback();
+ void pause() => _activeDevice?.pausePlayback();
+ void stop() => _activeDevice?.stopPlayback();
+ void seekTo(Duration position) => _activeDevice?.seek(timeSeconds: position.inSeconds.toDouble());
+
+ Future> listDestinations() async {
+ await _initialized;
+
+ return _discoveredDevices.values.toList(growable: false);
+ }
+
+ Future _initialize() async {
+ await FCastSenderSdkLib.init();
+ _castContext = CastContext();
+
+ final discoverer = DeviceDiscoverer();
+ discoverer.eventStreamController.stream.listen((event) {
+ switch (event) {
+ case DiscoveryEventDeviceAdded(:final deviceInfo, :final gcastCaps) ||
+ DiscoveryEventDeviceUpdated(:final deviceInfo, :final gcastCaps):
+ _discoveredDevices[(deviceInfo.name, deviceInfo.protocol)] = (deviceInfo, gcastCaps);
+ case DiscoveryEventDeviceRemoved():
+ _discoveredDevices.removeWhere((key, _) => key.$1 == event.name);
+ }
+ });
+
+ await discoverer.init();
+ await Future.delayed(const Duration(seconds: 3));
+ }
+}
diff --git a/mobile/lib/repositories/gcast.repository.dart b/mobile/lib/repositories/gcast.repository.dart
deleted file mode 100644
index b8ffe79b04..0000000000
--- a/mobile/lib/repositories/gcast.repository.dart
+++ /dev/null
@@ -1,68 +0,0 @@
-import 'package:cast/device.dart';
-import 'package:cast/discovery_service.dart';
-import 'package:cast/session.dart';
-import 'package:cast/session_manager.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-
-final gCastRepositoryProvider = Provider((_) {
- return GCastRepository();
-});
-
-class GCastRepository {
- CastSession? _castSession;
-
- void Function(CastSessionState)? onCastStatus;
- void Function(Map)? onCastMessage;
-
- Map? _receiverStatus;
-
- GCastRepository();
-
- Future connect(CastDevice device) async {
- _castSession = await CastSessionManager().startSession(device);
-
- _castSession?.stateStream.listen((state) {
- onCastStatus?.call(state);
- });
-
- _castSession?.messageStream.listen((message) {
- onCastMessage?.call(message);
- if (message['type'] == 'RECEIVER_STATUS') {
- _receiverStatus = message;
- }
- });
-
- // open the default receiver
- sendMessage(CastSession.kNamespaceReceiver, {'type': 'LAUNCH', 'appId': 'CC1AD845'});
- }
-
- Future disconnect() async {
- final sessionID = getSessionId();
-
- sendMessage(CastSession.kNamespaceReceiver, {'type': "STOP", "sessionId": sessionID});
-
- // wait 500ms to ensure the stop command is processed
- await Future.delayed(const Duration(milliseconds: 500));
-
- await _castSession?.close();
- }
-
- String? getSessionId() {
- if (_receiverStatus == null) {
- return null;
- }
- return _receiverStatus!['status']['applications'][0]['sessionId'];
- }
-
- void sendMessage(String namespace, Map message) {
- if (_castSession == null) {
- throw Exception("Cast session is not established");
- }
-
- _castSession!.sendMessage(namespace, message);
- }
-
- Future> listDestinations() async {
- return await CastDiscoveryService().search(timeout: const Duration(seconds: 3));
- }
-}
diff --git a/mobile/lib/services/cast.service.dart b/mobile/lib/services/cast.service.dart
new file mode 100644
index 0000000000..4dfc25291c
--- /dev/null
+++ b/mobile/lib/services/cast.service.dart
@@ -0,0 +1,194 @@
+import 'package:fcast_sender_sdk/fcast_sender_sdk.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/constants/constants.dart';
+import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
+import 'package:immich_mobile/models/cast/cast_manager_state.dart';
+import 'package:immich_mobile/models/sessions/session_create_response.model.dart';
+import 'package:immich_mobile/repositories/asset_api.repository.dart';
+import 'package:immich_mobile/repositories/cast.repository.dart';
+import 'package:immich_mobile/repositories/sessions_api.repository.dart';
+import 'package:immich_mobile/utils/image_url_builder.dart';
+// ignore: import_rule_openapi, we are only using the AssetMediaSize enum
+import 'package:openapi/api.dart';
+
+final castServiceProvider = Provider(
+ (ref) => CastService(
+ ref.watch(castRepositoryProvider),
+ ref.watch(sessionsAPIRepositoryProvider),
+ ref.watch(assetApiRepositoryProvider),
+ ),
+);
+
+class CastService {
+ final CastRepository _castRepository;
+ final SessionsAPIRepository _sessionsApiService;
+ final AssetApiRepository _assetApiRepository;
+
+ SessionCreateResponse? sessionKey;
+ String? currentAssetId;
+ bool isConnected = false;
+
+ void Function(bool)? onConnectionState;
+
+ void Function(Duration)? onCurrentTime;
+
+ void Function(Duration)? onDuration;
+
+ void Function(String?)? onReceiverName;
+
+ void Function(CastState)? onCastState;
+
+ CastService(this._castRepository, this._sessionsApiService, this._assetApiRepository) {
+ _castRepository.onConnectionState = _onConnectionState;
+ _castRepository.onDeviceEvent = _onDeviceEvent;
+ _castRepository.init();
+ }
+
+ void _onConnectionState(DeviceConnectionState state) {
+ if (state is DeviceConnectionState_Connected) {
+ isConnected = true;
+
+ onConnectionState?.call(true);
+ } else if (state is DeviceConnectionState_Disconnected) {
+ isConnected = false;
+ currentAssetId = null;
+
+ onConnectionState?.call(false);
+ onReceiverName?.call(null);
+ }
+ }
+
+ void _onDeviceEvent(DeviceEvent event) {
+ switch (event) {
+ case DeviceEvent_PlaybackStateChanged():
+ _handlePlaybackState(event.newPlaybackState);
+ case DeviceEvent_TimeChanged():
+ onCurrentTime?.call(Duration(seconds: event.newTime.toInt()));
+ case DeviceEvent_DurationChanged():
+ onDuration?.call(Duration(seconds: event.newDuration.toInt()));
+ default:
+ break;
+ }
+ }
+
+ void _handlePlaybackState(PlaybackState state) {
+ onCastState?.call(CastState.fromPlaybackState(state));
+ }
+
+ Future connect(dynamic device) async {
+ await _castRepository.connect(device);
+
+ onReceiverName?.call(device.name);
+ }
+
+ Future disconnect() async {
+ onReceiverName?.call(null);
+ currentAssetId = null;
+ await _castRepository.disconnect();
+ }
+
+ bool isSessionValid() {
+ // check if we already have a session token
+ // we should always have a expiration date
+ if (sessionKey == null || sessionKey?.expiresAt == null) {
+ return false;
+ }
+
+ final tokenExpiration = DateTime.parse(sessionKey!.expiresAt!);
+
+ // we want to make sure we have at least 1 minute remaining in the session
+ // this is to account for network latency and other delays when sending the request
+ final bufferedExpiration = tokenExpiration.subtract(kCastSessionRenewalBuffer);
+
+ return bufferedExpiration.isAfter(DateTime.now());
+ }
+
+ Future loadMedia(RemoteAsset asset, bool reload) async {
+ if (!isConnected) {
+ return;
+ } else if (asset.id == currentAssetId && !reload) {
+ return;
+ }
+
+ // create a session key
+ if (!isSessionValid()) {
+ sessionKey = await _sessionsApiService.createSession(
+ kCastDeviceType,
+ kCastDeviceOS,
+ duration: kCastSessionDuration.inSeconds,
+ );
+ }
+
+ // get image mime type
+ final mimeType = await _assetApiRepository.getAssetMIMEType(asset.id);
+
+ if (mimeType == null) {
+ return;
+ }
+
+ final baseUrl = asset.isVideo
+ ? getPlaybackUrlForRemoteId(asset.id)
+ : getThumbnailUrlForRemoteId(asset.id, type: AssetMediaSize.fullsize);
+
+ final authenticatedUrl = "$baseUrl&sessionKey=${sessionKey?.token}";
+
+ final request = asset.isVideo
+ ? LoadRequest.video(contentType: mimeType, url: authenticatedUrl, resumePosition: 0.0)
+ : LoadRequest.image(contentType: mimeType, url: authenticatedUrl);
+
+ _castRepository.loadMedia(request);
+
+ currentAssetId = asset.id;
+ }
+
+ void play() {
+ _castRepository.play();
+ }
+
+ void pause() {
+ _castRepository.pause();
+ }
+
+ void seekTo(Duration position) {
+ _castRepository.seekTo(position);
+ }
+
+ void stop() {
+ _castRepository.stop();
+
+ currentAssetId = null;
+ }
+
+ bool hasDisplayCapability(int capabilities) => (capabilities & 0x01) != 0;
+
+ Future> getDevices() async {
+ final dests = await _castRepository.listDestinations();
+
+ final fCastDevices = dests.where((dest) => dest.$1.protocol == ProtocolType.fCast).map((dest) => dest.$1);
+ final fCastNames = fCastDevices.map((device) => device.name).toSet();
+ final fCastAddresses = fCastDevices.expand((device) => device.addresses).toSet();
+
+ bool hasFCastTwin(DeviceInfo device) =>
+ fCastNames.contains(device.name) || device.addresses.any(fCastAddresses.contains);
+
+ return dests
+ .where((dest) {
+ final (device, gcastCaps) = dest;
+
+ return switch (device.protocol) {
+ ProtocolType.fCast => true,
+ ProtocolType.chromecast => hasDisplayCapability(gcastCaps ?? 0) && !hasFCastTwin(device),
+ };
+ })
+ .map((dest) {
+ final device = dest.$1;
+ final type = switch (device.protocol) {
+ ProtocolType.fCast => CastDestinationType.fCast,
+ ProtocolType.chromecast => CastDestinationType.googleCast,
+ };
+
+ return (device.name, type, device as dynamic);
+ })
+ .toList(growable: false);
+ }
+}
diff --git a/mobile/lib/services/gcast.service.dart b/mobile/lib/services/gcast.service.dart
deleted file mode 100644
index d9fc44a34d..0000000000
--- a/mobile/lib/services/gcast.service.dart
+++ /dev/null
@@ -1,244 +0,0 @@
-import 'dart:async';
-
-import 'package:cast/session.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
-import 'package:immich_mobile/models/cast/cast_manager_state.dart';
-import 'package:immich_mobile/models/sessions/session_create_response.model.dart';
-import 'package:immich_mobile/repositories/asset_api.repository.dart';
-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';
-// ignore: import_rule_openapi, we are only using the AssetMediaSize enum
-import 'package:openapi/api.dart';
-
-final gCastServiceProvider = Provider(
- (ref) => GCastService(
- ref.watch(gCastRepositoryProvider),
- ref.watch(sessionsAPIRepositoryProvider),
- ref.watch(assetApiRepositoryProvider),
- ),
-);
-
-class GCastService {
- final GCastRepository _gCastRepository;
- final SessionsAPIRepository _sessionsApiService;
- final AssetApiRepository _assetApiRepository;
-
- SessionCreateResponse? sessionKey;
- String? currentAssetId;
- bool isConnected = false;
- int? _sessionId;
- Timer? _mediaStatusPollingTimer;
-
- void Function(bool)? onConnectionState;
-
- void Function(Duration)? onCurrentTime;
-
- void Function(Duration)? onDuration;
-
- void Function(String)? onReceiverName;
-
- void Function(CastState)? onCastState;
-
- GCastService(this._gCastRepository, this._sessionsApiService, this._assetApiRepository) {
- _gCastRepository.onCastStatus = _onCastStatusCallback;
- _gCastRepository.onCastMessage = _onCastMessageCallback;
- }
-
- void _onCastStatusCallback(CastSessionState state) {
- if (state == CastSessionState.connected) {
- onConnectionState?.call(true);
- isConnected = true;
- } else if (state == CastSessionState.closed) {
- onConnectionState?.call(false);
- isConnected = false;
- onReceiverName?.call("");
- currentAssetId = null;
- }
- }
-
- void _onCastMessageCallback(Map message) {
- switch (message['type']) {
- case "MEDIA_STATUS":
- _handleMediaStatus(message);
- }
- }
-
- void _handleMediaStatus(Map message) {
- final statusList = (message['status'] as List).whereType