diff --git a/mobile/ios/Podfile b/mobile/ios/Podfile
index f697d17411..5841bf987b 100644
--- a/mobile/ios/Podfile
+++ b/mobile/ios/Podfile
@@ -27,12 +27,30 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
flutter_ios_podfile_setup
+def ensure_fcast_sender_sdk_podspec
+ plugin_ios_dir = File.expand_path(File.join('.symlinks', 'plugins', 'fcast_sender_sdk', 'ios'), __dir__)
+ original_podspec = File.join(plugin_ios_dir, 'fcast_sender_sdk_flutter_plugin.podspec')
+ expected_podspec = File.join(plugin_ios_dir, 'fcast_sender_sdk.podspec')
+
+ return unless File.exist?(original_podspec)
+
+ contents = File.read(original_podspec)
+ fixed_contents = contents
+ .sub("s.name = 'fcast_sender_sdk_flutter_plugin'", "s.name = 'fcast_sender_sdk'")
+ .gsub('libfcast_sender_sdk_flutter_plugin.a', 'libfcast_sender_sdk.a')
+
+ return if File.exist?(expected_podspec) && File.read(expected_podspec) == fixed_contents
+
+ File.write(expected_podspec, fixed_contents)
+end
+
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
-
+ ensure_fcast_sender_sdk_podspec
+
# share_handler addition start
target 'ShareExtension' do
inherit! :search_paths
@@ -49,7 +67,7 @@ post_install do |installer|
end
end
end
-
+
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock
index 75eda67ce1..d2e9e2ae64 100644
--- a/mobile/ios/Podfile.lock
+++ b/mobile/ios/Podfile.lock
@@ -1,10 +1,9 @@
PODS:
- - bonsoir_darwin (0.0.1):
- - Flutter
- - FlutterMacOS
- cupertino_http (0.0.1):
- Flutter
- FlutterMacOS
+ - fcast_sender_sdk (0.0.1):
+ - Flutter
- Flutter (1.0.0)
- flutter_local_notifications (0.0.1):
- Flutter
@@ -24,8 +23,8 @@ 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`)
+ - fcast_sender_sdk (from `.symlinks/plugins/fcast_sender_sdk/ios`)
- Flutter (from `Flutter`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
@@ -35,10 +34,10 @@ 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"
+ fcast_sender_sdk:
+ :path: ".symlinks/plugins/fcast_sender_sdk/ios"
Flutter:
:path: Flutter
flutter_local_notifications:
@@ -55,8 +54,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_handler_ios/ios/Models"
SPEC CHECKSUMS:
- bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
+ fcast_sender_sdk: 8bf227a5cbcedaea2e580a633ff0f706f1e90328
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
@@ -65,6 +64,6 @@ SPEC CHECKSUMS:
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
-PODFILE CHECKSUM: 3c43a700a4bffb4120bf696cad263aefd4bb3c8c
+PODFILE CHECKSUM: 44895462563291c3e4328a856c6482a25165b507
-COCOAPODS: 1.16.2
+COCOAPODS: 1.17.0
diff --git a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index a9381e541a..e1c39435fa 100644
--- a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git",
"state" : {
- "revision" : "60d9bb85c94ce6e7fc4406cd32529fd12bdb7809",
- "version" : "6.14.0"
+ "revision" : "84a79bc375a301169390ac110c868f06c857b83f",
+ "version" : "6.27.0"
}
},
{
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index da29f0f979..37c56fda05 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/models/cast/cast_manager_state.dart b/mobile/lib/models/cast/cast_manager_state.dart
index 9727bc7ed8..d4f7cf7516 100644
--- a/mobile/lib/models/cast/cast_manager_state.dart
+++ b/mobile/lib/models/cast/cast_manager_state.dart
@@ -1,6 +1,6 @@
import 'dart:convert';
-enum CastDestinationType { googleCast }
+enum CastDestinationType { googleCast, fCast }
enum CastState { idle, playing, paused, buffering }
diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart
index b298514d67..2a69b5d28c 100644
--- a/mobile/lib/providers/cast.provider.dart
+++ b/mobile/lib/providers/cast.provider.dart
@@ -1,19 +1,17 @@
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,
@@ -23,11 +21,11 @@ class CastNotifier extends StateNotifier {
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) {
@@ -51,23 +49,15 @@ class CastNotifier extends StateNotifier {
}
void loadMedia(RemoteAsset asset, bool reload) {
- _gCastService.loadMedia(asset, reload);
+ _castService.loadMedia(asset, reload);
}
- Future connect(CastDestinationType type, dynamic device) async {
- switch (type) {
- case CastDestinationType.googleCast:
- await _gCastService.connect(device);
- break;
- }
+ 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() {
@@ -81,22 +71,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..a809f702e0
--- /dev/null
+++ b/mobile/lib/repositories/cast.repository.dart
@@ -0,0 +1,99 @@
+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? _device;
+
+ void Function(DeviceConnectionState)? onConnectionState;
+ void Function(DeviceEvent)? onDeviceEvent;
+
+ final Map<(String, ProtocolType), (DeviceInfo, int?)> _discoveredDevices = {};
+ int _currentDeviceGeneration = 0;
+ Future? _initialized;
+
+ Future connect(DeviceInfo deviceInfo) async {
+ await _ensureInitialized();
+
+ _device?.disconnect();
+ final device = _castContext!.createDeviceFromInfo(info: deviceInfo);
+ _device = device;
+
+ 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 device = _device;
+ if (device == null) {
+ return;
+ }
+
+ _device = null;
+ _currentDeviceGeneration++;
+
+ if (device.isReady()) {
+ device.stopPlayback();
+
+ await Future.delayed(const Duration(milliseconds: 500));
+ }
+
+ device.disconnect();
+ onConnectionState?.call(const DeviceConnectionState.disconnected());
+ }
+
+ void loadMedia(LoadRequest request) => _device?.load(request: request);
+ void play() => _device?.resumePlayback();
+ void pause() => _device?.pausePlayback();
+ void stop() => _device?.stopPlayback();
+ void seekTo(Duration position) => _device?.seek(timeSeconds: position.inMilliseconds / 1000);
+
+ Future> listDestinations() async {
+ final isFirstScan = _initialized == null;
+ await _ensureInitialized();
+
+ if (isFirstScan) {
+ await Future.delayed(const Duration(seconds: 3));
+ }
+
+ return _discoveredDevices.values.toList(growable: false);
+ }
+
+ Future _ensureInitialized() => _initialized ??= _initialize();
+
+ 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();
+ }
+}
diff --git a/mobile/lib/repositories/gcast.repository.dart b/mobile/lib/repositories/gcast.repository.dart
deleted file mode 100644
index db3e0f45d0..0000000000
--- a/mobile/lib/repositories/gcast.repository.dart
+++ /dev/null
@@ -1,68 +0,0 @@
-import 'package:cast/device.dart';
-import 'package:cast/session.dart';
-import 'package:cast/session_manager.dart';
-import 'package:cast/discovery_service.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..529dc8c504
--- /dev/null
+++ b/mobile/lib/services/cast.service.dart
@@ -0,0 +1,205 @@
+ import 'package:fcast_sender_sdk/fcast_sender_sdk.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/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 = _onCastStatusCallback;
+ _castRepository.onDeviceEvent = _onDeviceEventCallback;
+ }
+
+ void _onCastStatusCallback(DeviceConnectionState state) {
+ if (state is DeviceConnectionState_Connected) {
+ onConnectionState?.call(true);
+ isConnected = true;
+ } else if (state is DeviceConnectionState_Disconnected) {
+ onConnectionState?.call(false);
+ isConnected = false;
+ onReceiverName?.call("");
+ currentAssetId = null;
+ }
+ }
+
+ void _onDeviceEventCallback(DeviceEvent event) {
+ switch (event) {
+ case DeviceEvent_PlaybackStateChanged():
+ _handlePlaybackState(event.newPlaybackState);
+ break;
+ case DeviceEvent_TimeChanged():
+ onCurrentTime?.call(Duration(milliseconds: (event.newTime * 1000).toInt()));
+ break;
+ case DeviceEvent_DurationChanged():
+ onDuration?.call(Duration(milliseconds: (event.newDuration * 1000).toInt()));
+ break;
+ default:
+ break;
+ }
+ }
+
+ void _handlePlaybackState(PlaybackState state) {
+ switch (state) {
+ case PlaybackState.playing:
+ onCastState?.call(CastState.playing);
+ break;
+ case PlaybackState.paused:
+ onCastState?.call(CastState.paused);
+ break;
+ case PlaybackState.buffering:
+ onCastState?.call(CastState.buffering);
+ break;
+ case PlaybackState.idle:
+ onCastState?.call(CastState.idle);
+ break;
+ }
+ }
+
+ Future connect(dynamic device) async {
+ await _castRepository.connect(device);
+
+ onReceiverName?.call(device.name);
+ }
+
+ Future disconnect() async {
+ onReceiverName?.call("");
+ 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 10 seconds remaining in the session
+ // this is to account for network latency and other delays when sending the request
+ final bufferedExpiration = tokenExpiration.subtract(const Duration(seconds: 10));
+
+ return bufferedExpiration.isAfter(DateTime.now());
+ }
+
+ void 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(
+ "Cast",
+ "Cast",
+ duration: const Duration(minutes: 15).inSeconds,
+ );
+ }
+
+ final unauthenticatedUrl = asset.isVideo
+ ? getPlaybackUrlForRemoteId(asset.id)
+ : getThumbnailUrlForRemoteId(asset.id, type: AssetMediaSize.fullsize);
+
+ final authenticatedURL = "$unauthenticatedUrl&sessionKey=${sessionKey?.token}";
+
+ // get image mime type
+ final mimeType = await _assetApiRepository.getAssetMIMEType(asset.id);
+
+ if (mimeType == null) {
+ return;
+ }
+
+ 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;
+ }
+
+ // 0x01 is display capability bitmask
+ bool isDisplay(int ca) => (ca & 0x01) != 0;
+
+ Future> getDevices() async {
+ final dests = await _castRepository.listDestinations();
+
+ final fCastNames = dests
+ .where((dest) => dest.$1.protocol == ProtocolType.fCast)
+ .map((dest) => dest.$1.name)
+ .toSet();
+
+ return dests
+ .where((dest) {
+ final (device, gcastCaps) = dest;
+
+ if (device.protocol == ProtocolType.fCast) {
+ return true;
+ }
+
+ return isDisplay(gcastCaps ?? 0) && !fCastNames.contains(device.name);
+ })
+ .map((dest) {
+ final device = dest.$1;
+ final type = device.protocol == ProtocolType.fCast
+ ? CastDestinationType.fCast
+ : 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 dcf7685237..0000000000
--- a/mobile/lib/services/gcast.service.dart
+++ /dev/null
@@ -1,250 +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);
- break;
- }
- }
-
- void _handleMediaStatus(Map message) {
- final statusList = (message['status'] as List).whereType