feat(web): load original videos (#20041)

* 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>
This commit is contained in:
andre-antunesdesa 2025-10-24 15:03:51 -04:00 committed by GitHub
parent c73e3dacea
commit f721a62776
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 51 additions and 5 deletions

View File

@ -1540,6 +1540,9 @@
"play_memories": "Play memories", "play_memories": "Play memories",
"play_motion_photo": "Play Motion Photo", "play_motion_photo": "Play Motion Photo",
"play_or_pause_video": "Play or pause video", "play_or_pause_video": "Play or pause video",
"play_original_video": "Play original video",
"play_original_video_setting_description": "Prefer playback of original videos rather than transcoded videos. If original asset is not compatible it may not playback correctly.",
"play_transcoded_video": "Play transcoded video",
"please_auth_to_access": "Please authenticate to access", "please_auth_to_access": "Please authenticate to access",
"port": "Port", "port": "Port",
"preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_subtitle": "Manage the app's preferences",

View File

@ -56,6 +56,7 @@
mdiMagnifyPlusOutline, mdiMagnifyPlusOutline,
mdiPresentationPlay, mdiPresentationPlay,
mdiUpload, mdiUpload,
mdiVideoOutline,
} from '@mdi/js'; } from '@mdi/js';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -78,6 +79,8 @@
// export let showEditorHandler: () => void; // export let showEditorHandler: () => void;
onClose: () => void; onClose: () => void;
motionPhoto?: Snippet; motionPhoto?: Snippet;
playOriginalVideo: boolean;
setPlayOriginalVideo: (value: boolean) => void;
} }
let { let {
@ -97,6 +100,8 @@
onShowDetail, onShowDetail,
onClose, onClose,
motionPhoto, motionPhoto,
playOriginalVideo = false,
setPlayOriginalVideo,
}: Props = $props(); }: Props = $props();
const sharedLink = getSharedLink(); const sharedLink = getSharedLink();
@ -245,6 +250,15 @@
{#if !asset.isTrashed} {#if !asset.isTrashed}
<SetVisibilityAction asset={toTimelineAsset(asset)} {onAction} {preAction} /> <SetVisibilityAction asset={toTimelineAsset(asset)} {onAction} {preAction} />
{/if} {/if}
{#if asset.type === AssetTypeEnum.Video}
<MenuOption
icon={mdiVideoOutline}
onClick={() => setPlayOriginalVideo(!playOriginalVideo)}
text={playOriginalVideo ? $t('play_transcoded_video') : $t('play_original_video')}
/>
{/if}
<hr /> <hr />
<MenuOption <MenuOption
icon={mdiHeadSyncOutline} icon={mdiHeadSyncOutline}

View File

@ -11,7 +11,7 @@
import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { closeEditorCofirm } from '$lib/stores/asset-editor.store'; import { closeEditorCofirm } from '$lib/stores/asset-editor.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { isShowDetail } from '$lib/stores/preferences.store'; import { alwaysLoadOriginalVideo, isShowDetail } from '$lib/stores/preferences.store';
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { websocketEvents } from '$lib/stores/websocket'; import { websocketEvents } from '$lib/stores/websocket';
@ -110,6 +110,11 @@
let stack: StackResponseDto | null = $state(null); let stack: StackResponseDto | null = $state(null);
let zoomToggle = $state(() => void 0); let zoomToggle = $state(() => void 0);
let playOriginalVideo = $state($alwaysLoadOriginalVideo);
const setPlayOriginalVideo = (value: boolean) => {
playOriginalVideo = value;
};
const refreshStack = async () => { const refreshStack = async () => {
if (authManager.isSharedLink) { if (authManager.isSharedLink) {
@ -410,6 +415,8 @@
onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)} onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)}
onShowDetail={toggleDetailPanel} onShowDetail={toggleDetailPanel}
onClose={closeViewer} onClose={closeViewer}
{playOriginalVideo}
{setPlayOriginalVideo}
> >
{#snippet motionPhoto()} {#snippet motionPhoto()}
<MotionPhotoAction <MotionPhotoAction
@ -465,6 +472,7 @@
onClose={closeViewer} onClose={closeViewer}
onVideoEnded={() => navigateAsset()} onVideoEnded={() => navigateAsset()}
onVideoStarted={handleVideoStarted} onVideoStarted={handleVideoStarted}
{playOriginalVideo}
/> />
{/if} {/if}
{/key} {/key}
@ -480,6 +488,7 @@
onPreviousAsset={() => navigateAsset('previous')} onPreviousAsset={() => navigateAsset('previous')}
onNextAsset={() => navigateAsset('next')} onNextAsset={() => navigateAsset('next')}
onVideoEnded={() => (shouldPlayMotionPhoto = false)} onVideoEnded={() => (shouldPlayMotionPhoto = false)}
{playOriginalVideo}
/> />
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath {:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath
.toLowerCase() .toLowerCase()
@ -510,6 +519,7 @@
onClose={closeViewer} onClose={closeViewer}
onVideoEnded={() => navigateAsset()} onVideoEnded={() => navigateAsset()}
onVideoStarted={handleVideoStarted} onVideoStarted={handleVideoStarted}
{playOriginalVideo}
/> />
{/if} {/if}
{#if $slideshowState === SlideshowState.None && isShared && ((album && album.isActivityEnabled) || activityManager.commentCount > 0) && !activityManager.isLoading} {#if $slideshowState === SlideshowState.None && isShared && ((album && album.isActivityEnabled) || activityManager.commentCount > 0) && !activityManager.isLoading}

View File

@ -10,7 +10,7 @@
videoViewerMuted, videoViewerMuted,
videoViewerVolume, videoViewerVolume,
} from '$lib/stores/preferences.store'; } from '$lib/stores/preferences.store';
import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils'; import { getAssetOriginalUrl, getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
import { AssetMediaSize } from '@immich/sdk'; import { AssetMediaSize } from '@immich/sdk';
import { LoadingSpinner } from '@immich/ui'; import { LoadingSpinner } from '@immich/ui';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
@ -21,6 +21,7 @@
assetId: string; assetId: string;
loopVideo: boolean; loopVideo: boolean;
cacheKey: string | null; cacheKey: string | null;
playOriginalVideo: boolean;
onPreviousAsset?: () => void; onPreviousAsset?: () => void;
onNextAsset?: () => void; onNextAsset?: () => void;
onVideoEnded?: () => void; onVideoEnded?: () => void;
@ -32,6 +33,7 @@
assetId, assetId,
loopVideo, loopVideo,
cacheKey, cacheKey,
playOriginalVideo,
onPreviousAsset = () => {}, onPreviousAsset = () => {},
onNextAsset = () => {}, onNextAsset = () => {},
onVideoEnded = () => {}, onVideoEnded = () => {},
@ -48,7 +50,12 @@
onMount(() => { onMount(() => {
// Show video after mount to ensure fading in. // Show video after mount to ensure fading in.
showVideo = true; showVideo = true;
assetFileUrl = getAssetPlaybackUrl({ id: assetId, cacheKey }); });
$effect(() => {
assetFileUrl = playOriginalVideo
? getAssetOriginalUrl({ id: assetId, cacheKey })
: getAssetPlaybackUrl({ id: assetId, cacheKey });
if (videoPlayer) { if (videoPlayer) {
videoPlayer.load(); videoPlayer.load();
} }

View File

@ -1,13 +1,14 @@
<script lang="ts"> <script lang="ts">
import { ProjectionType } from '$lib/constants';
import VideoNativeViewer from '$lib/components/asset-viewer/video-native-viewer.svelte'; import VideoNativeViewer from '$lib/components/asset-viewer/video-native-viewer.svelte';
import VideoPanoramaViewer from '$lib/components/asset-viewer/video-panorama-viewer.svelte'; import VideoPanoramaViewer from '$lib/components/asset-viewer/video-panorama-viewer.svelte';
import { ProjectionType } from '$lib/constants';
interface Props { interface Props {
assetId: string; assetId: string;
projectionType: string | null | undefined; projectionType: string | null | undefined;
cacheKey: string | null; cacheKey: string | null;
loopVideo: boolean; loopVideo: boolean;
playOriginalVideo: boolean;
onClose?: () => void; onClose?: () => void;
onPreviousAsset?: () => void; onPreviousAsset?: () => void;
onNextAsset?: () => void; onNextAsset?: () => void;
@ -20,6 +21,7 @@
projectionType, projectionType,
cacheKey, cacheKey,
loopVideo, loopVideo,
playOriginalVideo,
onPreviousAsset, onPreviousAsset,
onClose, onClose,
onNextAsset, onNextAsset,
@ -35,6 +37,7 @@
{loopVideo} {loopVideo}
{cacheKey} {cacheKey}
{assetId} {assetId}
{playOriginalVideo}
{onPreviousAsset} {onPreviousAsset}
{onNextAsset} {onNextAsset}
{onVideoEnded} {onVideoEnded}

View File

@ -7,6 +7,7 @@
import { themeManager } from '$lib/managers/theme-manager.svelte'; import { themeManager } from '$lib/managers/theme-manager.svelte';
import { import {
alwaysLoadOriginalFile, alwaysLoadOriginalFile,
alwaysLoadOriginalVideo,
autoPlayVideo, autoPlayVideo,
locale, locale,
loopVideo, loopVideo,
@ -119,7 +120,13 @@
<div class="ms-4"> <div class="ms-4">
<SettingSwitch title={$t('loop_videos')} subtitle={$t('loop_videos_description')} bind:checked={$loopVideo} /> <SettingSwitch title={$t('loop_videos')} subtitle={$t('loop_videos_description')} bind:checked={$loopVideo} />
</div> </div>
<div class="ms-4">
<SettingSwitch
title={$t('play_original_video')}
subtitle={$t('play_original_video_setting_description')}
bind:checked={$alwaysLoadOriginalVideo}
/>
</div>
<div class="ms-4"> <div class="ms-4">
<SettingSwitch <SettingSwitch
title={$t('permanent_deletion_warning')} title={$t('permanent_deletion_warning')}

View File

@ -148,4 +148,6 @@ export const loopVideo = persisted<boolean>('loop-video', true, {});
export const autoPlayVideo = persisted<boolean>('auto-play-video', true, {}); export const autoPlayVideo = persisted<boolean>('auto-play-video', true, {});
export const alwaysLoadOriginalVideo = persisted<boolean>('always-load-original-video', false, {});
export const recentAlbumsDropdown = persisted<boolean>('recent-albums-open', true, {}); export const recentAlbumsDropdown = persisted<boolean>('recent-albums-open', true, {});