immich/mobile/lib/widgets/asset_viewer/video_position.dart
Brandon Wees 5574b2dd39
feat(mobile): add cast support (#18341)
* initial cast framework complete and mocked cast dialog working

* wip casting

* casting works!

just need to add session key check and remote video controls

* cleanup of classes

* add session expiration checks

* cast dialog now shows connected device at top of list with a list header. Discovered devices are also cached for app session.

* cast video player finalized

* show fullsize assets on casting

* translation already happens on the text element

* remove prints

* fix lintings

* code review changes from @shenlong-tanwen

* fix connect method override

* fix alphabetization

* remove important

* filter chromecast audio devices

* fix some disconnect command ordering issues and unawaited futures

* remove prints

* only disconnect if we are connected

* don't try to reconnect if its the current device

* add cast button to top bar

* format sessions api

* more formatting issues fixed

* add snack bar to tell user that we cannot cast an asset that is not uploaded to server

* make casting icon change to primary color when casting is active

* only show casting snackbar if we are casting

* dont show cast button if asset is remote and we are not casting

* stop playing media if we seek to an asset that is not remote

* remove https check since it works with local http IP addresses

* remove unneeded imports

* fix recasting when socket closes

* fix info plist formatting

* only show cast button if there is an active websocket connection (ie the server is accessible)

* add device capability bitmask checks

* small comment about bitmask
2025-06-08 21:55:23 -05:00

132 lines
4.4 KiB
Dart

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
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 {
const VideoPosition({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
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()
: Column(
children: [
Padding(
// align with slider's inherent padding
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FormattedDuration(position),
FormattedDuration(duration),
],
),
),
Row(
children: [
Expanded(
child: Slider(
value: min(
position.inMicroseconds / duration.inMicroseconds * 100,
100,
),
min: 0,
max: 100,
thumbColor: Colors.white,
activeColor: Colors.white,
inactiveColor: whiteOpacity75,
onChangeStart: (value) {
final state =
ref.read(videoPlaybackValueProvider).state;
wasPlaying.value = state != VideoPlaybackState.paused;
ref.read(videoPlayerControlsProvider.notifier).pause();
},
onChangeEnd: (value) {
if (wasPlaying.value) {
ref.read(videoPlayerControlsProvider.notifier).play();
}
},
onChanged: (value) {
final seekToDuration = (duration * (value / 100.0));
if (isCasting) {
ref
.read(castProvider.notifier)
.seekTo(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;
},
),
),
],
),
],
);
}
}
class _VideoPositionPlaceholder extends StatelessWidget {
const _VideoPositionPlaceholder();
static void _onChangedDummy(_) {}
@override
Widget build(BuildContext context) {
return const Column(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FormattedDuration(Duration.zero),
FormattedDuration(Duration.zero),
],
),
),
Row(
children: [
Expanded(
child: Slider(
value: 0.0,
min: 0,
max: 100,
thumbColor: Colors.white,
activeColor: Colors.white,
inactiveColor: whiteOpacity75,
onChanged: _onChangedDummy,
),
),
],
),
],
);
}
}