mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
cast video player finalized
This commit is contained in:
parent
8a31c68ddd
commit
93d59cd41a
@ -53,7 +53,7 @@ class CastManagerState {
|
||||
receiverName: map['receiverName'] ?? '',
|
||||
castState: map['castState'] ?? CastState.idle,
|
||||
currentTime: Duration(seconds: map['currentTime']?.toInt() ?? 0),
|
||||
duration: Duration(seconds: map['duration']?.toInt() ?? 0));
|
||||
duration: Duration(seconds: map['duration']?.toInt() ?? 0),);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
@ -13,6 +13,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/video_player_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/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/asset.service.dart';
|
||||
@ -60,6 +61,8 @@ class NativeVideoViewerPage extends HookConsumerWidget {
|
||||
|
||||
final log = Logger('NativeVideoViewerPage');
|
||||
|
||||
final cast = ref.watch(castProvider);
|
||||
|
||||
Future<VideoSource?> createSource() async {
|
||||
if (!context.mounted) {
|
||||
return null;
|
||||
@ -391,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)
|
||||
if (aspectRatio.value != null && !cast.isCasting)
|
||||
Visibility.maintain(
|
||||
key: ValueKey(asset),
|
||||
visible: isVisible.value,
|
||||
|
@ -43,8 +43,7 @@ class GCastRepository {
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
final sessionID =
|
||||
_receiverStatus?['status']['applications'][0]['sessionId'];
|
||||
final sessionID = getSessionId();
|
||||
|
||||
sendMessage(CastSession.kNamespaceReceiver, {
|
||||
'type': "STOP",
|
||||
@ -54,6 +53,13 @@ class GCastRepository {
|
||||
await _castSession?.close();
|
||||
}
|
||||
|
||||
String? getSessionId() {
|
||||
if (_receiverStatus == null) {
|
||||
return null;
|
||||
}
|
||||
return _receiverStatus!['status']['applications'][0]['sessionId'];
|
||||
}
|
||||
|
||||
void sendMessage(String namespace, Map<String, dynamic> message) {
|
||||
if (_castSession == null) {
|
||||
throw Exception("Cast session is not established");
|
||||
|
@ -20,7 +20,7 @@ class SessionsAPIRepository extends ApiRepository
|
||||
@override
|
||||
Future<SessionCreateResponse> createSession(
|
||||
String deviceType, String deviceOS,
|
||||
{int? duration}) async {
|
||||
{int? duration,}) async {
|
||||
final dto = await checkNull(
|
||||
_api.createSession(
|
||||
SessionCreateDto(
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cast/device.dart';
|
||||
import 'package:cast/session.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@ -29,6 +31,9 @@ class GCastService implements ICastDestinationService {
|
||||
SessionCreateResponse? sessionKey;
|
||||
String? currentAssetId;
|
||||
bool isConnected = false;
|
||||
int? _sessionId;
|
||||
Timer? _mediaStatusPollingTimer;
|
||||
CastState? castState;
|
||||
|
||||
@override
|
||||
void Function(bool)? onConnectionState;
|
||||
@ -62,6 +67,54 @@ class GCastService implements ICastDestinationService {
|
||||
|
||||
void _onCastMessageCallback(Map<String, dynamic> message) {
|
||||
final msgType = message['type'];
|
||||
|
||||
if (msgType == "MEDIA_STATUS") {
|
||||
final statusList = (message['status'] as List)
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.toList();
|
||||
|
||||
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);
|
||||
|
||||
// stop polling for media status if the video finished playing
|
||||
if (status["idleReason"] == "FINISHED") {
|
||||
_mediaStatusPollingTimer?.cancel();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
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["currentTime"] != null) {
|
||||
final currentTime =
|
||||
Duration(milliseconds: (status["currentTime"] * 1000 ?? 0).toInt());
|
||||
onCurrentTime?.call(currentTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connect(CastDevice device) async {
|
||||
@ -161,21 +214,50 @@ class GCastService implements ICastDestinationService {
|
||||
},
|
||||
"autoplay": true,
|
||||
});
|
||||
|
||||
// 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
|
||||
_mediaStatusPollingTimer?.cancel();
|
||||
|
||||
if (asset.isVideo) {
|
||||
_mediaStatusPollingTimer =
|
||||
Timer.periodic(const Duration(milliseconds: 500), (timer) {
|
||||
if (isConnected) {
|
||||
_gCastRepository.sendMessage(CastSession.kNamespaceMedia, {
|
||||
"type": "GET_STATUS",
|
||||
"mediaSessionId": _sessionId,
|
||||
});
|
||||
} else {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void play() {
|
||||
// TODO: implement play
|
||||
_gCastRepository.sendMessage(CastSession.kNamespaceMedia, {
|
||||
"type": "PLAY",
|
||||
"mediaSessionId": _sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void pause() {
|
||||
// TODO: implement pause
|
||||
_gCastRepository.sendMessage(CastSession.kNamespaceMedia, {
|
||||
"type": "PAUSE",
|
||||
"mediaSessionId": _sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void seekTo(Duration position) {
|
||||
// TODO: implement seekTo
|
||||
_gCastRepository.sendMessage(CastSession.kNamespaceMedia, {
|
||||
"type": "SEEK",
|
||||
"mediaSessionId": _sessionId,
|
||||
"currentTime": position.inSeconds,
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -1,9 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/cast/cast_manager_state.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_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/utils/hooks/timer_hook.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/center_play_button.dart';
|
||||
import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
|
||||
@ -25,6 +28,8 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
|
||||
final VideoPlaybackState state =
|
||||
ref.watch(videoPlaybackValueProvider.select((value) => value.state));
|
||||
|
||||
final cast = ref.watch(castProvider);
|
||||
|
||||
// A timer to hide the controls
|
||||
final hideTimer = useTimer(
|
||||
hideTimerDuration,
|
||||
@ -42,7 +47,8 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
|
||||
}
|
||||
},
|
||||
);
|
||||
final showBuffering = state == VideoPlaybackState.buffering;
|
||||
final showBuffering =
|
||||
state == VideoPlaybackState.buffering && !cast.isCasting;
|
||||
|
||||
/// Shows the controls and starts the timer to hide them
|
||||
void showControlsAndStartHideTimer() {
|
||||
@ -59,12 +65,28 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
|
||||
/// Toggles between playing and pausing depending on the state of the video
|
||||
void togglePlay() {
|
||||
showControlsAndStartHideTimer();
|
||||
if (state == VideoPlaybackState.playing) {
|
||||
ref.read(videoPlayerControlsProvider.notifier).pause();
|
||||
} else if (state == VideoPlaybackState.completed) {
|
||||
ref.read(videoPlayerControlsProvider.notifier).restart();
|
||||
|
||||
if (cast.isCasting) {
|
||||
if (cast.castState == CastState.playing) {
|
||||
ref.read(castProvider.notifier).pause();
|
||||
} else if (cast.castState == CastState.paused) {
|
||||
ref.read(castProvider.notifier).play();
|
||||
} else if (cast.castState == CastState.idle) {
|
||||
// resend the play command since its finished
|
||||
final asset = ref.read(currentAssetProvider);
|
||||
if (asset == null) {
|
||||
return;
|
||||
}
|
||||
ref.read(castProvider.notifier).loadMedia(asset, true);
|
||||
}
|
||||
} else {
|
||||
ref.read(videoPlayerControlsProvider.notifier).play();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,7 +111,8 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
|
||||
backgroundColor: Colors.black54,
|
||||
iconColor: Colors.white,
|
||||
isFinished: state == VideoPlaybackState.completed,
|
||||
isPlaying: state == VideoPlaybackState.playing,
|
||||
isPlaying: state == VideoPlaybackState.playing ||
|
||||
(cast.isCasting && cast.castState == CastState.playing),
|
||||
show: assetIsVideo && showControls,
|
||||
onPressed: togglePlay,
|
||||
),
|
||||
|
@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/colors.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_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/widgets/asset_viewer/formatted_duration.dart';
|
||||
|
||||
class VideoPosition extends HookConsumerWidget {
|
||||
@ -13,9 +14,16 @@ class VideoPosition extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final (position, duration) = ref.watch(
|
||||
videoPlaybackValueProvider.select((v) => (v.position, v.duration)),
|
||||
);
|
||||
final isCasting = ref.watch(castProvider).isCasting;
|
||||
|
||||
final (position, duration) = isCasting
|
||||
? ref.watch(
|
||||
castProvider.select((c) => (c.currentTime, c.duration)),
|
||||
)
|
||||
: ref.watch(
|
||||
videoPlaybackValueProvider.select((v) => (v.position, v.duration)),
|
||||
);
|
||||
|
||||
final wasPlaying = useRef<bool>(true);
|
||||
return duration == Duration.zero
|
||||
? const _VideoPositionPlaceholder()
|
||||
@ -57,15 +65,22 @@ class VideoPosition extends HookConsumerWidget {
|
||||
}
|
||||
},
|
||||
onChanged: (value) {
|
||||
final inSeconds =
|
||||
(duration * (value / 100.0)).inSeconds;
|
||||
final position = inSeconds.toDouble();
|
||||
ref
|
||||
.read(videoPlayerControlsProvider.notifier)
|
||||
.position = position;
|
||||
// This immediately updates the slider position without waiting for the video to update
|
||||
ref.read(videoPlaybackValueProvider.notifier).position =
|
||||
Duration(seconds: inSeconds);
|
||||
final seekToDuration = (duration * (value / 100.0));
|
||||
|
||||
if (isCasting) {
|
||||
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;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
Loading…
x
Reference in New Issue
Block a user