mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(web): add support for casting (#18231)
* recreate #13966 * gcast button works * rewrote gcast-player to be GCastDestination and CastManager manages the interface between UI and casting destinations * remove unneeded imports * add "Connected to" translation * Remove css for cast launcher * fix tests * fix doc tests * fix the receiver application ID * remove casting app ID * remove cast button from nav bar It is now present at the following locations: - shared link album and single asset views - asset viewer (normal user) - album view (normal user) * part 1 of fixes from @danieldietzler code review * part 2 of code review changes from @danieldietzler and @jsram91 * cleanup documentation * onVideoStarted missing callback * add token expiry validation * cleanup logic and logging * small cleanup * rename to ICastDestination * cast button changes
This commit is contained in:
parent
12b7a079c1
commit
86db0aafe5
16 changed files with 708 additions and 36 deletions
103
web/src/lib/components/asset-viewer/video-remote-viewer.svelte
Normal file
103
web/src/lib/components/asset-viewer/video-remote-viewer.svelte
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import { castManager, CastState } from '$lib/managers/cast-manager.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { mdiCastConnected, mdiPause, mdiPlay } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
poster: string;
|
||||
assetFileUrl: string;
|
||||
onVideoStarted: () => void;
|
||||
onVideoEnded: () => void;
|
||||
}
|
||||
|
||||
let { poster, assetFileUrl, onVideoEnded, onVideoStarted }: Props = $props();
|
||||
|
||||
let previousPlayerState: CastState | null = $state(null);
|
||||
|
||||
const handlePlayPauseButton = async () => {
|
||||
switch (castManager.castState) {
|
||||
case CastState.PLAYING: {
|
||||
castManager.pause();
|
||||
break;
|
||||
}
|
||||
case CastState.IDLE: {
|
||||
await cast(assetFileUrl, true);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
castManager.play();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
if (assetFileUrl) {
|
||||
// this can't be in an async context with $effect
|
||||
void cast(assetFileUrl);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (castManager.castState === CastState.IDLE && previousPlayerState !== CastState.PAUSED) {
|
||||
onVideoEnded();
|
||||
}
|
||||
|
||||
previousPlayerState = castManager.castState;
|
||||
});
|
||||
|
||||
const cast = async (url: string, force: boolean = false) => {
|
||||
if (!url || !castManager.isCasting) {
|
||||
return;
|
||||
}
|
||||
const fullUrl = new URL(url, globalThis.location.href);
|
||||
|
||||
try {
|
||||
await castManager.loadMedia(fullUrl.href, force);
|
||||
onVideoStarted();
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to cast');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
function handleSeek(event: Event) {
|
||||
const newTime = Number.parseFloat((event.target as HTMLInputElement).value);
|
||||
castManager.seekTo(newTime);
|
||||
}
|
||||
</script>
|
||||
|
||||
<span class="flex items-center space-x-2 text-gray-200 text-2xl font-bold">
|
||||
<Icon path={mdiCastConnected} class="text-primary" size="36" />
|
||||
<span>{$t('connected_to')} {castManager.receiverName}</span>
|
||||
</span>
|
||||
|
||||
<img src={poster} alt="poster" class="rounded-xl m-4" />
|
||||
|
||||
<div class="flex place-content-center place-items-center">
|
||||
{#if castManager.castState == CastState.BUFFERING}
|
||||
<div class="p-3">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{:else}
|
||||
<CircleIconButton
|
||||
color="opaque"
|
||||
icon={castManager.castState == CastState.PLAYING ? mdiPause : mdiPlay}
|
||||
onclick={() => handlePlayPauseButton()}
|
||||
title={castManager.castState == CastState.PLAYING ? 'Pause' : 'Play'}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={castManager.duration}
|
||||
value={castManager.currentTime ?? 0}
|
||||
onchange={handleSeek}
|
||||
class="w-full h-4 bg-primary"
|
||||
/>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue