1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-27 09:21:05 +02:00

Merge remote-tracking branch 'origin' into feat/cast

This commit is contained in:
Jonathan Jogenfors 2024-11-07 00:01:18 +01:00
commit 4d5aaf6568
15 changed files with 107 additions and 51 deletions

View File

@ -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.
If you have issues playing video, check the video playback limitations above.

View File

@ -49,7 +49,7 @@ export function Timeline({ items }: Props): JSX.Element {
<div className="flex flex-col flex-grow justify-between gap-2">
<div className="flex gap-2 items-center">
{cardIcon === 'immich' ? (
<img src="img/immich-logo.svg" height="30" className="rounded-none" />
<img src="/img/immich-logo.svg" height="30" className="rounded-none" />
) : (
<Icon path={cardIcon} size={1} color={item.iconColor} />
)}

View File

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

View File

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

View File

@ -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;

View File

@ -58,11 +58,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.119.0</string>
<string>1.120.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>181</string>
<string>182</string>
<key>FLTEnableImpeller</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>

42
web/package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<script lang="ts">
import CastPlayer from '$lib/utils/cast-sender';
import CastPlayer from '$lib/utils/cast-player';
import Button from '$lib/components/elements/buttons/button.svelte';
import { get } from 'svelte/store';
import { onMount } from 'svelte';
@ -22,11 +22,11 @@
};
function handleSeek(event: Event) {
const newTime: number = parseFloat((event.target as HTMLInputElement).value);
const newTime: number = Number.parseFloat((event.target as HTMLInputElement).value);
castPlayer.seek(newTime);
}
onMount(async () => {
onMount(() => {
castPlayer.isConnected.subscribe((value) => {
remotePlayer.isConnected = value;
});

View File

@ -1,5 +1,5 @@
<script lang="ts">
import CastPlayer, { loadCastFramework } from '$lib/utils/cast-sender';
import CastPlayer, { loadCastFramework } from '$lib/utils/cast-player';
import Button from '$lib/components/elements/buttons/button.svelte';
import { onMount } from 'svelte';
onMount(async () => {

View File

@ -1,7 +1,7 @@
/// <reference types="chromecast-caf-sender" />
import { PUBLIC_IMMICH_CAST_APPLICATION_ID } from '$env/static/public';
import { createApiKey, deleteApiKey, getApiKeys, Permission, type ApiKeyCreateResponseDto } from '@immich/sdk';
import 'chromecast-caf-sender';
import { get, writable } from 'svelte/store';
const CAST_API_KEY_NAME = 'cast';
@ -103,24 +103,30 @@ class CastPlayer {
private onRemotePlayerChange(event: cast.framework.RemotePlayerChangedEvent) {
switch (event.field) {
case 'isConnected':
case 'isConnected': {
this.isConnected.set(event.value);
break;
case 'mediaInfo':
}
case 'mediaInfo': {
this.mediaInfo.set(event.value);
break;
case 'remotePlayer':
}
case 'remotePlayer': {
this.remotePlayer.set(event.value);
break;
case 'duration':
}
case 'duration': {
this.duration.set(event.value);
break;
case 'currentTime':
}
case 'currentTime': {
this.currentTime.set(event.value);
break;
case 'playerState':
}
case 'playerState': {
this.playerState.set(event.value);
break;
}
}
}
@ -237,7 +243,7 @@ class CastPlayer {
const playRequest = new chrome.cast.media.PlayRequest();
this.currentMedia.play(playRequest, (success: any) => console.log(success), this.onError.bind(this));
this.currentMedia.play(playRequest, () => {}, this.onError.bind(this));
}
pause() {
@ -248,7 +254,7 @@ class CastPlayer {
const pauseRequest = new chrome.cast.media.PauseRequest();
this.currentMedia.pause(pauseRequest, (success: any) => console.log(success), this.onError.bind(this));
this.currentMedia.pause(pauseRequest, () => {}, this.onError.bind(this));
}
seek(currentTime: number) {
@ -271,7 +277,7 @@ export const loadCastFramework = (() => {
script.src = FRAMEWORK_LINK;
document.body.append(script);
console.log('Loading cast framework');
console.debug('Cast framework loaded');
});
}
return promise;

View File

@ -6,6 +6,9 @@
"forceConsistentCasingInFileNames": true,
"module": "es2020",
"moduleResolution": "bundler",
"paths": {
"chromecast-caf-sender": ["./node_modules/@types/chromecast-caf-sender/index.d.ts"]
},
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,

View File

@ -4,6 +4,7 @@ import { svelteTesting } from '@testing-library/svelte/vite';
import path from 'node:path';
import { visualizer } from 'rollup-plugin-visualizer';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
const upstream = {
target: process.env.IMMICH_SERVER_URL || 'http://immich-server:2283/',
@ -19,6 +20,7 @@ export default defineConfig({
'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js',
// eslint-disable-next-line unicorn/prefer-module
'@test-data': path.resolve(__dirname, './src/test-data'),
'chromecast-caf-sender': path.resolve(__dirname, 'node_modules/@types/chromecast-caf-sender/index.d.ts'),
},
},
server: {
@ -39,6 +41,7 @@ export default defineConfig({
: undefined,
enhancedImages(),
svelteTesting(),
tsconfigPaths(),
],
optimizeDeps: {
entries: ['src/**/*.{svelte,ts,html}'],