diff --git a/docs/docs/features/casting.md b/docs/docs/features/casting.md index 91a5cd014d..5b49fdd9a6 100644 --- a/docs/docs/features/casting.md +++ b/docs/docs/features/casting.md @@ -4,11 +4,12 @@ Immich supports the Google's Cast protocol so that photos and videos can be cast ## Casting -When you use Chrome and a chromecast is found on your local network, there will be a Cast button in the top navigation bar and in the asset viewer. Click the button and Chrome will pop up a menu to select the device to cast to. Now, browse Immich normally and your photos and videos should show up on the casted device! +When you use Chrome and a chromecast is found on your local network, there will be a Cast button in the top navigation bar and in the asset viewer. Click the button and Chrome will pop up a menu to select the device to cast to. Now, browse Immich normally and your photos and videos should show up on the casted device! When you are finished casting, click the cast menu again and press the stop button. ## Network requirements + Your Immich server must be reachable by the Chromecast, not just your browser. If you are away from your home network you therefore can't cast your vacation photo to your friend's TV if only your computer is connected to a VPN. Your Immich server address must be resolvable by the Google DNS servers `8.8.8.8` and `8.8.4.4`, so you can't use an internal domain name on your internal DNS server. @@ -20,14 +21,15 @@ You also need a real TLS certificate such as from LetsEncrypt. Self-signed certi At this stage there are some important limitations to be aware of. ### Video playback + Depending on the device you are casting to, you need to consider the video codecs, resolutions, and framerates are supported please see [this list](https://developers.google.com/cast/docs/media#video_codecs). For instance, Chromecast generation 1 and 2 only support playback of 1080p/30 videos while 3rd gen supports 1080p/60 but only when using H.264. - -Immich does not support on-the-fly transcoding, so you'll need to preselect the transcoded video format in the transcoding settings. For instance, if you will be casting to a 3rd gen Chromecast, all videos in your Immich instance must be transcoded to H.264 with at most 1080p at 60 fps. + +Immich does not support on-the-fly transcoding, so you'll need to preselect the transcoded video format in the transcoding settings. For instance, if you will be casting to a 3rd gen Chromecast, all videos in your Immich instance must be transcoded to H.264 with at most 1080p at 60 fps. In the Video Transcoding settings, set the `Transcode Policy` to `Videos higher than target resolution or not in an accepted format`. Then select your target resolution. We currently don't have a way to set the target framerate, so your high refresh rate videos will have a hard time playing on older devices - ### Other limitations + - No preloading which means navigating between assets is slow - No support for slideshows - No video playback controls like pause, resume, and seek @@ -39,4 +41,4 @@ First, ensure you are using Chrome since other browsers will never be supported Next, check the network requirements above. -If you have issues playing video, check the video playback limitations above. \ No newline at end of file +If you have issues playing video, check the video playback limitations above. diff --git a/docs/src/components/timeline.tsx b/docs/src/components/timeline.tsx index 374d2d88fa..32b15edb59 100644 --- a/docs/src/components/timeline.tsx +++ b/docs/src/components/timeline.tsx @@ -49,7 +49,7 @@ export function Timeline({ items }: Props): JSX.Element {
{cardIcon === 'immich' ? ( - + ) : ( )} diff --git a/docs/src/pages/roadmap.tsx b/docs/src/pages/roadmap.tsx index 1f07e45122..b54753370d 100644 --- a/docs/src/pages/roadmap.tsx +++ b/docs/src/pages/roadmap.tsx @@ -234,6 +234,13 @@ const roadmap: Item[] = [ ]; const milestones: Item[] = [ + { + icon: mdiStar, + iconColor: 'gold', + title: '50,000 Stars', + description: 'Reached 50K Stars on GitHub!', + getDateLabel: withLanguage(new Date(2024, 10, 1)), + }, withRelease({ icon: mdiFaceRecognition, title: 'Metadata Face Import', diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 9da36e8b89..55d8e0fa00 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -207,7 +207,7 @@ SPEC CHECKSUMS: geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 + isar_flutter_libs: fdf730ca925d05687f36d7f1d355e482529ed097 MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9 package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index e1598cb50f..c8cc4d9227 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -401,7 +401,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 181; + CURRENT_PROJECT_VERSION = 182; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -543,7 +543,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 181; + CURRENT_PROJECT_VERSION = 182; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -571,7 +571,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 181; + CURRENT_PROJECT_VERSION = 182; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 59a83c7e2e..ef15a45a7c 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,11 +58,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.119.0 + 1.120.0 CFBundleSignature ???? CFBundleVersion - 181 + 182 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/web/package-lock.json b/web/package-lock.json index 8ff92a11c4..4baf6bd00c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -70,6 +70,7 @@ "tslib": "^2.6.2", "typescript": "^5.5.0", "vite": "^5.1.4", + "vite-tsconfig-paths": "^5.1.0", "vitest": "^2.0.5" } }, @@ -8193,6 +8194,27 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true }, + "node_modules/tsconfck": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.4.tgz", + "integrity": "sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", @@ -8443,6 +8465,26 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.0.tgz", + "integrity": "sha512-Y1PLGHCJfAq1Zf4YIGEsmuU/NCX1epoZx9zwSr32Gjn3aalwQHRKr5aUmbo6r0JHeHkqmWpmDg7WOynhYXw1og==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/vitefu": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.3.tgz", diff --git a/web/package.json b/web/package.json index d116f5e9eb..8a8694cb97 100644 --- a/web/package.json +++ b/web/package.json @@ -61,6 +61,7 @@ "tslib": "^2.6.2", "typescript": "^5.5.0", "vite": "^5.1.4", + "vite-tsconfig-paths": "^5.1.0", "vitest": "^2.0.5" }, "type": "module", diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 76fc213737..b2e5e4c1f3 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -19,7 +19,7 @@ import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { handleError } from '$lib/utils/handle-error'; - import CastPlayer from '$lib/utils/cast-sender'; + import CastPlayer from '$lib/utils/cast-player'; import { get } from 'svelte/store'; export let asset: AssetResponseDto; @@ -41,7 +41,6 @@ let loader: HTMLImageElement; let castPlayer = CastPlayer.getInstance(); - let isCastInitialized = get(castPlayer.isInitialized); let castState = get(castPlayer.castState); @@ -54,7 +53,7 @@ $: preload(useOriginalImage, preloadAssets); $: imageLoaderUrl = getAssetUrl(asset.id, useOriginalImage, asset.checksum); - $: cast(imageLoaderUrl); + $: void cast(imageLoaderUrl); photoZoomState.set({ currentRotation: 0, @@ -65,22 +64,20 @@ }); $zoomed = false; - onMount(async () => { - castPlayer.isInitialized.subscribe((value) => { - isCastInitialized = value; - }); - + onMount(() => { castPlayer.castState.subscribe((value) => { - castState = value; - if (value === 'CONNECTED') { - cast(assetFileUrl); + if (castState !== value) { + void cast(assetFileUrl); } + castState = value; }); }); const cast = async (url: string) => { if (!url) { return; + } else if (castState !== 'CONNECTED') { + return; } const fullUrl = new URL(url, window.location.href); await castPlayer.loadMedia(fullUrl.href); 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 513f34317c..c6c771b4a3 100644 --- a/web/src/lib/components/asset-viewer/video-native-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-native-viewer.svelte @@ -9,7 +9,7 @@ import type { SwipeCustomEvent } from 'svelte-gestures'; import { fade } from 'svelte/transition'; import { t } from 'svelte-i18n'; - import CastPlayer from '$lib/utils/cast-sender'; + import CastPlayer from '$lib/utils/cast-player'; import { get } from 'svelte/store'; import VideoRemoteViewer from '$lib/components/asset-viewer/video-remote-viewer.svelte'; @@ -28,30 +28,23 @@ let assetFileUrl: string; let forceMuted = false; - let isCastInitialized = false; let castState = get(castPlayer.castState); $: if (element) { assetFileUrl = getAssetPlaybackUrl({ id: assetId, checksum }); forceMuted = false; element.load(); - cast(assetFileUrl); } $: if (assetFileUrl) { - console.log(assetFileUrl); + void cast(assetFileUrl); } - onMount(async () => { - isCastInitialized = get(castPlayer.isInitialized); - castPlayer.isInitialized.subscribe((value) => { - isCastInitialized = value; - }); - + onMount(() => { castPlayer.castState.subscribe((value) => { - castState = value; - if (value === 'CONNECTED') { - cast(assetFileUrl); + if (castState !== value && value === 'CONNECTED') { + void cast(assetFileUrl); } + castState = value; }); }); @@ -59,6 +52,8 @@ console.log('casting', url); if (!url) { return; + } else if (castState !== 'CONNECTED') { + return; } const fullUrl = new URL(url, window.location.href); await castPlayer.loadMedia(fullUrl.href); diff --git a/web/src/lib/components/asset-viewer/video-remote-viewer.svelte b/web/src/lib/components/asset-viewer/video-remote-viewer.svelte index 340b628871..4f9df0b4a9 100644 --- a/web/src/lib/components/asset-viewer/video-remote-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-remote-viewer.svelte @@ -1,5 +1,5 @@