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:
commit
4d5aaf6568
@ -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.
|
||||
|
@ -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} />
|
||||
)}
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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
42
web/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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 () => {
|
||||
|
@ -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;
|
@ -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,
|
||||
|
@ -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}'],
|
||||
|
Loading…
Reference in New Issue
Block a user