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
7 changed files with 51 additions and 5 deletions

View File

@@ -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",

View File

@@ -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}
<SetVisibilityAction asset={toTimelineAsset(asset)} {onAction} {preAction} />
{/if}
{#if asset.type === AssetTypeEnum.Video}
<MenuOption
icon={mdiVideoOutline}
onClick={() => setPlayOriginalVideo(!playOriginalVideo)}
text={playOriginalVideo ? $t('play_transcoded_video') : $t('play_original_video')}
/>
{/if}
<hr />
<MenuOption
icon={mdiHeadSyncOutline}

View File

@@ -11,7 +11,7 @@
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { closeEditorCofirm } from '$lib/stores/asset-editor.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 { user } from '$lib/stores/user.store';
import { websocketEvents } from '$lib/stores/websocket';
@@ -110,6 +110,11 @@
let stack: StackResponseDto | null = $state(null);
let zoomToggle = $state(() => 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()}
<MotionPhotoAction
@@ -465,6 +472,7 @@
onClose={closeViewer}
onVideoEnded={() => 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}

View File

@@ -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();
}

View File

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

View File

@@ -7,6 +7,7 @@
import { themeManager } from '$lib/managers/theme-manager.svelte';
import {
alwaysLoadOriginalFile,
alwaysLoadOriginalVideo,
autoPlayVideo,
locale,
loopVideo,
@@ -119,7 +120,13 @@
<div class="ms-4">
<SettingSwitch title={$t('loop_videos')} subtitle={$t('loop_videos_description')} bind:checked={$loopVideo} />
</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">
<SettingSwitch
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 alwaysLoadOriginalVideo = persisted<boolean>('always-load-original-video', false, {});
export const recentAlbumsDropdown = persisted<boolean>('recent-albums-open', true, {});