mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 02:27:08 -04:00 
			
		
		
		
	* added user preference for always loading original video added ability to toggle between transcoded/original in the video viewer add fix to static check error * address PR comments * Update asset-viewer-nav-bar.svelte Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> --------- Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com> Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
		
			
				
	
	
		
			173 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Svelte
		
	
	
	
	
	
			
		
		
	
	
			173 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Svelte
		
	
	
	
	
	
| <script lang="ts">
 | |
|   import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
 | |
|   import VideoRemoteViewer from '$lib/components/asset-viewer/video-remote-viewer.svelte';
 | |
|   import { assetViewerFadeDuration } from '$lib/constants';
 | |
|   import { castManager } from '$lib/managers/cast-manager.svelte';
 | |
|   import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
 | |
|   import {
 | |
|     autoPlayVideo,
 | |
|     loopVideo as loopVideoPreference,
 | |
|     videoViewerMuted,
 | |
|     videoViewerVolume,
 | |
|   } from '$lib/stores/preferences.store';
 | |
|   import { getAssetOriginalUrl, getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
 | |
|   import { AssetMediaSize } from '@immich/sdk';
 | |
|   import { LoadingSpinner } from '@immich/ui';
 | |
|   import { onDestroy, onMount } from 'svelte';
 | |
|   import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures';
 | |
|   import { fade } from 'svelte/transition';
 | |
| 
 | |
|   interface Props {
 | |
|     assetId: string;
 | |
|     loopVideo: boolean;
 | |
|     cacheKey: string | null;
 | |
|     playOriginalVideo: boolean;
 | |
|     onPreviousAsset?: () => void;
 | |
|     onNextAsset?: () => void;
 | |
|     onVideoEnded?: () => void;
 | |
|     onVideoStarted?: () => void;
 | |
|     onClose?: () => void;
 | |
|   }
 | |
| 
 | |
|   let {
 | |
|     assetId,
 | |
|     loopVideo,
 | |
|     cacheKey,
 | |
|     playOriginalVideo,
 | |
|     onPreviousAsset = () => {},
 | |
|     onNextAsset = () => {},
 | |
|     onVideoEnded = () => {},
 | |
|     onVideoStarted = () => {},
 | |
|     onClose = () => {},
 | |
|   }: Props = $props();
 | |
| 
 | |
|   let videoPlayer: HTMLVideoElement | undefined = $state();
 | |
|   let isLoading = $state(true);
 | |
|   let assetFileUrl = $state('');
 | |
|   let isScrubbing = $state(false);
 | |
|   let showVideo = $state(false);
 | |
| 
 | |
|   onMount(() => {
 | |
|     // Show video after mount to ensure fading in.
 | |
|     showVideo = true;
 | |
|   });
 | |
| 
 | |
|   $effect(() => {
 | |
|     assetFileUrl = playOriginalVideo
 | |
|       ? getAssetOriginalUrl({ id: assetId, cacheKey })
 | |
|       : getAssetPlaybackUrl({ id: assetId, cacheKey });
 | |
|     if (videoPlayer) {
 | |
|       videoPlayer.load();
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   onDestroy(() => {
 | |
|     if (videoPlayer) {
 | |
|       videoPlayer.src = '';
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   const handleCanPlay = async (video: HTMLVideoElement) => {
 | |
|     try {
 | |
|       if (!video.paused && !isScrubbing) {
 | |
|         await video.play();
 | |
|         onVideoStarted();
 | |
|       }
 | |
|     } catch (error) {
 | |
|       if (error instanceof DOMException && error.name === 'NotAllowedError') {
 | |
|         await tryForceMutedPlay(video);
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // auto-play failed
 | |
|     } finally {
 | |
|       isLoading = false;
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   const tryForceMutedPlay = async (video: HTMLVideoElement) => {
 | |
|     if (video.muted) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       video.muted = true;
 | |
|       await handleCanPlay(video);
 | |
|     } catch {
 | |
|       // muted auto-play failed
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   const onSwipe = (event: SwipeCustomEvent) => {
 | |
|     if (event.detail.direction === 'left') {
 | |
|       onNextAsset();
 | |
|     }
 | |
|     if (event.detail.direction === 'right') {
 | |
|       onPreviousAsset();
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   let containerWidth = $state(0);
 | |
|   let containerHeight = $state(0);
 | |
| 
 | |
|   $effect(() => {
 | |
|     if (isFaceEditMode.value) {
 | |
|       videoPlayer?.pause();
 | |
|     }
 | |
|   });
 | |
| </script>
 | |
| 
 | |
| {#if showVideo}
 | |
|   <div
 | |
|     transition:fade={{ duration: assetViewerFadeDuration }}
 | |
|     class="flex h-full select-none place-content-center place-items-center"
 | |
|     bind:clientWidth={containerWidth}
 | |
|     bind:clientHeight={containerHeight}
 | |
|   >
 | |
|     {#if castManager.isCasting}
 | |
|       <div class="place-content-center h-full place-items-center">
 | |
|         <VideoRemoteViewer
 | |
|           poster={getAssetThumbnailUrl({ id: assetId, size: AssetMediaSize.Preview, cacheKey })}
 | |
|           {onVideoStarted}
 | |
|           {onVideoEnded}
 | |
|           {assetFileUrl}
 | |
|         />
 | |
|       </div>
 | |
|     {:else}
 | |
|       <video
 | |
|         bind:this={videoPlayer}
 | |
|         loop={$loopVideoPreference && loopVideo}
 | |
|         autoplay={$autoPlayVideo}
 | |
|         playsinline
 | |
|         controls
 | |
|         class="h-full object-contain"
 | |
|         {...useSwipe(onSwipe)}
 | |
|         oncanplay={(e) => handleCanPlay(e.currentTarget)}
 | |
|         onended={onVideoEnded}
 | |
|         onvolumechange={(e) => ($videoViewerMuted = e.currentTarget.muted)}
 | |
|         onseeking={() => (isScrubbing = true)}
 | |
|         onseeked={() => (isScrubbing = false)}
 | |
|         onplaying={(e) => {
 | |
|           e.currentTarget.focus();
 | |
|         }}
 | |
|         onclose={() => onClose()}
 | |
|         muted={$videoViewerMuted}
 | |
|         bind:volume={$videoViewerVolume}
 | |
|         poster={getAssetThumbnailUrl({ id: assetId, size: AssetMediaSize.Preview, cacheKey })}
 | |
|         src={assetFileUrl}
 | |
|       >
 | |
|       </video>
 | |
| 
 | |
|       {#if isLoading}
 | |
|         <div class="absolute flex place-content-center place-items-center">
 | |
|           <LoadingSpinner />
 | |
|         </div>
 | |
|       {/if}
 | |
| 
 | |
|       {#if isFaceEditMode.value}
 | |
|         <FaceEditor htmlElement={videoPlayer} {containerWidth} {containerHeight} {assetId} />
 | |
|       {/if}
 | |
|     {/if}
 | |
|   </div>
 | |
| {/if}
 |