mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
wip casting
This commit is contained in:
parent
6c3087b585
commit
4d86773ffe
@ -1778,6 +1778,7 @@
|
||||
"start_date": "Start date",
|
||||
"state": "State",
|
||||
"status": "Status",
|
||||
"stop_casting": "Stop casting",
|
||||
"stop_motion_photo": "Stop Motion Photo",
|
||||
"stop_photo_sharing": "Stop sharing your photos?",
|
||||
"stop_photo_sharing_description": "{partner} will no longer be able to access your photos.",
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/models/cast_manager_state.dart';
|
||||
|
||||
abstract interface class ICastDestinationService {
|
||||
@ -14,7 +15,7 @@ abstract interface class ICastDestinationService {
|
||||
void Function(String)? onReceiverName;
|
||||
void Function(CastState)? onCastState;
|
||||
|
||||
void loadMedia(String url, String sessionKey, bool reload);
|
||||
void loadMedia(Asset asset, bool reload);
|
||||
|
||||
void play();
|
||||
void pause();
|
||||
|
@ -20,6 +20,7 @@ import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart
|
||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
@ -355,6 +356,11 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
Timer(const Duration(milliseconds: 400), () {
|
||||
precacheNextImage(next);
|
||||
});
|
||||
|
||||
// send image to casting if the server has it
|
||||
if (newAsset.isRemote) {
|
||||
ref.read(castProvider.notifier).loadMedia(newAsset, false);
|
||||
}
|
||||
},
|
||||
builder: buildAsset,
|
||||
),
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/models/cast_manager_state.dart';
|
||||
import 'package:immich_mobile/services/gcast.service.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final castProvider = StateNotifierProvider<CastNotifier, CastManagerState>(
|
||||
(ref) => CastNotifier(ref.watch(gCastServiceProvider)),
|
||||
@ -46,8 +48,8 @@ class CastNotifier extends StateNotifier<CastManagerState> {
|
||||
state = state.copyWith(castState: castState);
|
||||
}
|
||||
|
||||
void loadMedia(String url, String sessionKey, bool reload) {
|
||||
_gCastService.loadMedia(url, sessionKey, reload);
|
||||
void loadMedia(Asset asset, bool reload) {
|
||||
_gCastService.loadMedia(asset, reload);
|
||||
}
|
||||
|
||||
Future<void> connect(CastDestinationType type, dynamic device) async {
|
||||
@ -59,16 +61,7 @@ class CastNotifier extends StateNotifier<CastManagerState> {
|
||||
}
|
||||
|
||||
Future<List<(String, CastDestinationType, dynamic)>> getDevices() async {
|
||||
// return _gCastService.getDevices();
|
||||
// delay for 2 seconds to simulate loading
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
return Future<List<(String, CastDestinationType, dynamic)>>.value([
|
||||
('Google Cast', CastDestinationType.googleCast, null),
|
||||
('Apple TV', CastDestinationType.googleCast, null),
|
||||
('Roku', CastDestinationType.googleCast, null),
|
||||
('Fire TV', CastDestinationType.googleCast, null),
|
||||
]);
|
||||
return _gCastService.getDevices();
|
||||
}
|
||||
|
||||
void play() {
|
||||
|
@ -29,7 +29,7 @@ class GCastRepository {
|
||||
});
|
||||
|
||||
// open the default receiver
|
||||
sendMessage({
|
||||
sendMessage(CastSession.kNamespaceReceiver, {
|
||||
'type': 'LAUNCH',
|
||||
'appId': 'CC1AD845',
|
||||
});
|
||||
@ -41,12 +41,12 @@ class GCastRepository {
|
||||
await _castSession?.close();
|
||||
}
|
||||
|
||||
void sendMessage(Map<String, dynamic> message) {
|
||||
void sendMessage(String namespace, Map<String, dynamic> message) {
|
||||
if (_castSession == null) {
|
||||
throw Exception("Cast session is not established");
|
||||
}
|
||||
|
||||
_castSession!.sendMessage(CastSession.kNamespaceReceiver, message);
|
||||
_castSession!.sendMessage(namespace, message);
|
||||
}
|
||||
|
||||
Future<List<CastDevice>> listDestinations() async {
|
||||
|
@ -33,6 +33,7 @@ class ApiService implements Authentication {
|
||||
late StacksApi stacksApi;
|
||||
late ViewApi viewApi;
|
||||
late MemoriesApi memoriesApi;
|
||||
late SessionsApi sessionsApi;
|
||||
|
||||
ApiService() {
|
||||
// The below line ensures that the api clients are initialized when the service is instantiated
|
||||
@ -70,6 +71,7 @@ class ApiService implements Authentication {
|
||||
stacksApi = StacksApi(_apiClient);
|
||||
viewApi = ViewApi(_apiClient);
|
||||
memoriesApi = MemoriesApi(_apiClient);
|
||||
sessionsApi = SessionsApi(_apiClient);
|
||||
}
|
||||
|
||||
Future<String> resolveAndSetEndpoint(String serverUrl) async {
|
||||
|
@ -2,17 +2,30 @@ import 'package:cast/device.dart';
|
||||
import 'package:cast/session.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/cast_destination_service.interface.dart';
|
||||
import 'package:immich_mobile/models/cast_manager_state.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/gcast.repository.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
import 'package:openapi/api.dart' as api;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
final gCastServiceProvider =
|
||||
Provider((ref) => GCastService(ref.watch(gCastRepositoryProvider)));
|
||||
final gCastServiceProvider = Provider(
|
||||
(ref) => GCastService(
|
||||
ref.watch(gCastRepositoryProvider), ref.watch(apiServiceProvider)),
|
||||
);
|
||||
|
||||
class GCastService implements ICastDestinationService {
|
||||
final GCastRepository _gCastRepository;
|
||||
final ApiService _apiService;
|
||||
|
||||
api.SessionCreateResponseDto? sessionKey;
|
||||
String? currentAssetId;
|
||||
bool isConnected = false;
|
||||
|
||||
@override
|
||||
void Function(bool)? onConnectionState;
|
||||
@ -25,7 +38,7 @@ class GCastService implements ICastDestinationService {
|
||||
@override
|
||||
void Function(CastState)? onCastState;
|
||||
|
||||
GCastService(this._gCastRepository) {
|
||||
GCastService(this._gCastRepository, this._apiService) {
|
||||
_gCastRepository.onCastStatus = _onCastStatusCallback;
|
||||
_gCastRepository.onCastMessage = _onCastMessageCallback;
|
||||
}
|
||||
@ -33,8 +46,10 @@ class GCastService implements ICastDestinationService {
|
||||
void _onCastStatusCallback(CastSessionState state) {
|
||||
if (state == CastSessionState.connected) {
|
||||
onConnectionState?.call(true);
|
||||
isConnected = true;
|
||||
} else if (state == CastSessionState.closed) {
|
||||
onConnectionState?.call(false);
|
||||
isConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +65,8 @@ class GCastService implements ICastDestinationService {
|
||||
|
||||
Future<void> connect(CastDevice device) async {
|
||||
await _gCastRepository.connect(device);
|
||||
|
||||
onReceiverName?.call(device.extras["fn"] ?? "Google Cast");
|
||||
}
|
||||
|
||||
@override
|
||||
@ -68,6 +85,8 @@ class GCastService implements ICastDestinationService {
|
||||
@override
|
||||
void disconnect() {
|
||||
_gCastRepository.disconnect();
|
||||
|
||||
onReceiverName?.call("");
|
||||
}
|
||||
|
||||
@override
|
||||
@ -79,8 +98,58 @@ class GCastService implements ICastDestinationService {
|
||||
}
|
||||
|
||||
@override
|
||||
void loadMedia(String url, String sessionKey, bool reload) {
|
||||
// TODO: implement loadMedia
|
||||
void loadMedia(Asset asset, bool reload) async {
|
||||
print("Casting media: ${asset.remoteId}");
|
||||
|
||||
if (!isConnected) {
|
||||
return;
|
||||
} else if (asset.remoteId == null) {
|
||||
return;
|
||||
} else if (asset.remoteId == currentAssetId && !reload) {
|
||||
return;
|
||||
}
|
||||
|
||||
// create a session key
|
||||
sessionKey ??= await _apiService.sessionsApi.createSession(
|
||||
api.SessionCreateDto(
|
||||
deviceOS: "Google Cast",
|
||||
deviceType: "Google Cast",
|
||||
duration: const Duration(minutes: 15).inSeconds,
|
||||
),
|
||||
);
|
||||
|
||||
final unauthenticatedUrl = asset.isVideo
|
||||
? getOriginalUrlForRemoteId(
|
||||
asset.remoteId!,
|
||||
)
|
||||
: getThumbnailUrlForRemoteId(
|
||||
asset.remoteId!,
|
||||
type: api.AssetMediaSize.thumbnail,
|
||||
);
|
||||
|
||||
final authenticatedURL =
|
||||
"$unauthenticatedUrl&sessionKey=${sessionKey?.token}";
|
||||
|
||||
// get image mime type
|
||||
final info = await _apiService.assetsApi.getAssetInfo(asset.remoteId!);
|
||||
final mimeType = info?.originalMimeType;
|
||||
|
||||
if (mimeType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_gCastRepository.sendMessage(CastSession.kNamespaceMedia, {
|
||||
"type": "LOAD",
|
||||
"media": {
|
||||
"contentId": authenticatedURL,
|
||||
"streamType": "LIVE",
|
||||
"contentType": mimeType,
|
||||
"contentUrl": authenticatedURL,
|
||||
},
|
||||
"autoplay": true,
|
||||
});
|
||||
|
||||
print("Sending message: $authenticatedURL");
|
||||
}
|
||||
|
||||
@override
|
||||
@ -103,7 +172,13 @@ class GCastService implements ICastDestinationService {
|
||||
final dests = await _gCastRepository.listDestinations();
|
||||
|
||||
return dests
|
||||
.map((device) => (device.name, CastDestinationType.googleCast, device))
|
||||
.map(
|
||||
(device) => (
|
||||
device.extras["fn"] ?? "Google Cast",
|
||||
CastDestinationType.googleCast,
|
||||
device
|
||||
),
|
||||
)
|
||||
.toList(growable: false);
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ class CastDialog extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final castManager = ref.watch(castProvider);
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text(
|
||||
"cast",
|
||||
@ -21,8 +23,6 @@ class CastDialog extends ConsumerWidget {
|
||||
child: FutureBuilder<List<(String, CastDestinationType, dynamic)>>(
|
||||
future: ref.read(castProvider.notifier).getDevices(),
|
||||
builder: (context, snapshot) {
|
||||
final cast = ref.read(castProvider.notifier);
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return Text(
|
||||
'Error: ${snapshot.error.toString()}',
|
||||
@ -50,14 +50,25 @@ class CastDialog extends ConsumerWidget {
|
||||
final deviceObj = found.$3;
|
||||
|
||||
return ListTile(
|
||||
title: Text(deviceName),
|
||||
title: Text(
|
||||
deviceName,
|
||||
style: TextStyle(
|
||||
color: castManager.receiverName == deviceName
|
||||
? context.colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
),
|
||||
leading: Icon(
|
||||
type == CastDestinationType.googleCast
|
||||
? Icons.cast
|
||||
: Icons.cast_connected,
|
||||
),
|
||||
trailing: castManager.isCasting &&
|
||||
castManager.receiverName == deviceName
|
||||
? Icon(Icons.check, color: context.colorScheme.primary)
|
||||
: null,
|
||||
onTap: () {
|
||||
cast.connect(type, deviceObj);
|
||||
ref.read(castProvider.notifier).connect(type, deviceObj);
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -66,12 +77,23 @@ class CastDialog extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (castManager.isCasting)
|
||||
TextButton(
|
||||
onPressed: () => ref.read(castProvider.notifier).disconnect(),
|
||||
child: Text(
|
||||
"stop_casting",
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(
|
||||
"close",
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.tertiary,
|
||||
color: context.colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
|
@ -6,6 +6,7 @@ import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart';
|
||||
@ -45,6 +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 comments = album != null &&
|
||||
album.remoteId != null &&
|
||||
asset.remoteId != null
|
||||
@ -174,10 +176,14 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context, builder: (context) => const CastDialog());
|
||||
context: context,
|
||||
builder: (context) => const CastDialog(),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.cast,
|
||||
castManager.isCasting
|
||||
? Icons.cast_connected_rounded
|
||||
: Icons.cast_rounded,
|
||||
size: 20.0,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
|
Loading…
x
Reference in New Issue
Block a user