Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin' into feat/cast
Browse files Browse the repository at this point in the history
  • Loading branch information
etnoy committed Nov 8, 2024
2 parents e55cbeb + cff0b95 commit 5b33d27
Show file tree
Hide file tree
Showing 15 changed files with 106 additions and 51 deletions.
12 changes: 7 additions & 5 deletions docs/docs/features/casting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.

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
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
Expand All @@ -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.
2 changes: 1 addition & 1 deletion docs/src/components/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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} />
)}
Expand Down
7 changes: 7 additions & 0 deletions docs/src/pages/roadmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion mobile/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions mobile/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions mobile/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
42 changes: 42 additions & 0 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 8 additions & 11 deletions web/src/lib/components/asset-viewer/photo-viewer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,7 +41,6 @@
let loader: HTMLImageElement;
let castPlayer = CastPlayer.getInstance();
let isCastInitialized = get(castPlayer.isInitialized);
let castState = get(castPlayer.castState);
Expand All @@ -54,7 +53,7 @@
$: preload(useOriginalImage, preloadAssets);
$: imageLoaderUrl = getAssetUrl(asset.id, useOriginalImage, asset.checksum);
$: cast(imageLoaderUrl);
$: void cast(imageLoaderUrl);
photoZoomState.set({
currentRotation: 0,
Expand All @@ -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);
Expand Down
21 changes: 8 additions & 13 deletions web/src/lib/components/asset-viewer/video-native-viewer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,37 +28,32 @@
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;
});
});
const cast = async (url: string) => {
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
});
Expand Down
2 changes: 1 addition & 1 deletion web/src/lib/components/cast/cast-button.svelte
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
}
}
}

Expand Down Expand Up @@ -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() {
Expand All @@ -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) {
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 5b33d27

Please sign in to comment.