From f721a62776a0b2394c6bf82a69c19b153f5ab1be Mon Sep 17 00:00:00 2001
From: andre-antunesdesa <80642274+andre-antunesdesa@users.noreply.github.com>
Date: Fri, 24 Oct 2025 15:03:51 -0400
Subject: [PATCH] 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>
---
i18n/en.json | 3 +++
.../asset-viewer/asset-viewer-nav-bar.svelte | 14 ++++++++++++++
.../components/asset-viewer/asset-viewer.svelte | 12 +++++++++++-
.../asset-viewer/video-native-viewer.svelte | 11 +++++++++--
.../asset-viewer/video-wrapper-viewer.svelte | 5 ++++-
.../user-settings-page/app-settings.svelte | 9 ++++++++-
web/src/lib/stores/preferences.store.ts | 2 ++
7 files changed, 51 insertions(+), 5 deletions(-)
diff --git a/i18n/en.json b/i18n/en.json
index 8e8913263c..dcfa0dc17f 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1540,6 +1540,9 @@
"play_memories": "Play memories",
"play_motion_photo": "Play Motion Photo",
"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",
"port": "Port",
"preferences_settings_subtitle": "Manage the app's preferences",
diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
index d61af04db6..4a792d7945 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
@@ -56,6 +56,7 @@
mdiMagnifyPlusOutline,
mdiPresentationPlay,
mdiUpload,
+ mdiVideoOutline,
} from '@mdi/js';
import type { Snippet } from 'svelte';
import { t } from 'svelte-i18n';
@@ -78,6 +79,8 @@
// export let showEditorHandler: () => void;
onClose: () => void;
motionPhoto?: Snippet;
+ playOriginalVideo: boolean;
+ setPlayOriginalVideo: (value: boolean) => void;
}
let {
@@ -97,6 +100,8 @@
onShowDetail,
onClose,
motionPhoto,
+ playOriginalVideo = false,
+ setPlayOriginalVideo,
}: Props = $props();
const sharedLink = getSharedLink();
@@ -245,6 +250,15 @@
{#if !asset.isTrashed}
{/if}
+
+ {#if asset.type === AssetTypeEnum.Video}
+ setPlayOriginalVideo(!playOriginalVideo)}
+ text={playOriginalVideo ? $t('play_transcoded_video') : $t('play_original_video')}
+ />
+ {/if}
+
void 0);
+ let playOriginalVideo = $state($alwaysLoadOriginalVideo);
+
+ const setPlayOriginalVideo = (value: boolean) => {
+ playOriginalVideo = value;
+ };
const refreshStack = async () => {
if (authManager.isSharedLink) {
@@ -410,6 +415,8 @@
onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)}
onShowDetail={toggleDetailPanel}
onClose={closeViewer}
+ {playOriginalVideo}
+ {setPlayOriginalVideo}
>
{#snippet motionPhoto()}
navigateAsset()}
onVideoStarted={handleVideoStarted}
+ {playOriginalVideo}
/>
{/if}
{/key}
@@ -480,6 +488,7 @@
onPreviousAsset={() => navigateAsset('previous')}
onNextAsset={() => navigateAsset('next')}
onVideoEnded={() => (shouldPlayMotionPhoto = false)}
+ {playOriginalVideo}
/>
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath
.toLowerCase()
@@ -510,6 +519,7 @@
onClose={closeViewer}
onVideoEnded={() => navigateAsset()}
onVideoStarted={handleVideoStarted}
+ {playOriginalVideo}
/>
{/if}
{#if $slideshowState === SlideshowState.None && isShared && ((album && album.isActivityEnabled) || activityManager.commentCount > 0) && !activityManager.isLoading}
diff --git a/web/src/lib/components/asset-viewer/video-native-viewer.svelte b/web/src/lib/components/asset-viewer/video-native-viewer.svelte
index fd3bc1c44f..2ccfb59243 100644
--- a/web/src/lib/components/asset-viewer/video-native-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/video-native-viewer.svelte
@@ -10,7 +10,7 @@
videoViewerMuted,
videoViewerVolume,
} from '$lib/stores/preferences.store';
- import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
+ import { getAssetOriginalUrl, getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
import { AssetMediaSize } from '@immich/sdk';
import { LoadingSpinner } from '@immich/ui';
import { onDestroy, onMount } from 'svelte';
@@ -21,6 +21,7 @@
assetId: string;
loopVideo: boolean;
cacheKey: string | null;
+ playOriginalVideo: boolean;
onPreviousAsset?: () => void;
onNextAsset?: () => void;
onVideoEnded?: () => void;
@@ -32,6 +33,7 @@
assetId,
loopVideo,
cacheKey,
+ playOriginalVideo,
onPreviousAsset = () => {},
onNextAsset = () => {},
onVideoEnded = () => {},
@@ -48,7 +50,12 @@
onMount(() => {
// Show video after mount to ensure fading in.
showVideo = true;
- assetFileUrl = getAssetPlaybackUrl({ id: assetId, cacheKey });
+ });
+
+ $effect(() => {
+ assetFileUrl = playOriginalVideo
+ ? getAssetOriginalUrl({ id: assetId, cacheKey })
+ : getAssetPlaybackUrl({ id: assetId, cacheKey });
if (videoPlayer) {
videoPlayer.load();
}
diff --git a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte
index a5a94d85d4..748886d901 100644
--- a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte
@@ -1,13 +1,14 @@