radames commited on
Commit
246efdb
·
1 Parent(s): 409869d

bump sveltkit and svelte

Browse files
Files changed (41) hide show
  1. frontend/.eslintignore +0 -13
  2. frontend/.eslintrc.cjs +0 -30
  3. frontend/.gitignore +17 -4
  4. frontend/.prettierignore +4 -11
  5. frontend/.prettierrc +1 -9
  6. frontend/README.md +5 -5
  7. frontend/package-lock.json +0 -0
  8. frontend/package.json +21 -22
  9. frontend/postcss.config.js +0 -6
  10. frontend/src/app.css +2 -3
  11. frontend/src/app.d.ts +8 -7
  12. frontend/src/app.html +9 -9
  13. frontend/src/lib/components/AspectRatioSelect.svelte +10 -9
  14. frontend/src/lib/components/Button.svelte +7 -8
  15. frontend/src/lib/components/Checkbox.svelte +12 -6
  16. frontend/src/lib/components/ImagePlayer.svelte +38 -31
  17. frontend/src/lib/components/InputRange.svelte +6 -5
  18. frontend/src/lib/components/MediaListSwitcher.svelte +15 -19
  19. frontend/src/lib/components/PipelineOptions.svelte +39 -24
  20. frontend/src/lib/components/SeedInput.svelte +9 -9
  21. frontend/src/lib/components/Selectlist.svelte +6 -5
  22. frontend/src/lib/components/TextArea.svelte +6 -5
  23. frontend/src/lib/components/VideoInput.svelte +73 -43
  24. frontend/src/lib/components/Warning.svelte +13 -11
  25. frontend/src/lib/icons/aspect.svelte +7 -2
  26. frontend/src/lib/icons/expand.svelte +7 -2
  27. frontend/src/lib/icons/floppy.svelte +7 -2
  28. frontend/src/lib/icons/screen.svelte +7 -2
  29. frontend/src/lib/icons/spinner.svelte +7 -2
  30. frontend/src/lib/lcmLive.ts +58 -50
  31. frontend/src/lib/mediaStream.ts +28 -23
  32. frontend/src/lib/store.ts +12 -4
  33. frontend/src/lib/types.ts +8 -8
  34. frontend/src/lib/utils.ts +22 -22
  35. frontend/src/piexifjs.d.ts +3 -3
  36. frontend/src/routes/+layout.svelte +5 -3
  37. frontend/src/routes/+page.svelte +61 -47
  38. frontend/svelte.config.js +8 -9
  39. frontend/tailwind.config.js +3 -3
  40. frontend/tsconfig.json +17 -15
  41. frontend/vite.config.ts +11 -10
frontend/.eslintignore DELETED
@@ -1,13 +0,0 @@
1
- .DS_Store
2
- node_modules
3
- /build
4
- /.svelte-kit
5
- /package
6
- .env
7
- .env.*
8
- !.env.example
9
-
10
- # Ignore files for PNPM, NPM and YARN
11
- pnpm-lock.yaml
12
- package-lock.json
13
- yarn.lock
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/.eslintrc.cjs DELETED
@@ -1,30 +0,0 @@
1
- module.exports = {
2
- root: true,
3
- extends: [
4
- 'eslint:recommended',
5
- 'plugin:@typescript-eslint/recommended',
6
- 'plugin:svelte/recommended',
7
- 'prettier'
8
- ],
9
- parser: '@typescript-eslint/parser',
10
- plugins: ['@typescript-eslint'],
11
- parserOptions: {
12
- sourceType: 'module',
13
- ecmaVersion: 2020,
14
- extraFileExtensions: ['.svelte']
15
- },
16
- env: {
17
- browser: true,
18
- es2017: true,
19
- node: true
20
- },
21
- overrides: [
22
- {
23
- files: ['*.svelte'],
24
- parser: 'svelte-eslint-parser',
25
- parserOptions: {
26
- parser: '@typescript-eslint/parser'
27
- }
28
- }
29
- ]
30
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/.gitignore CHANGED
@@ -1,11 +1,24 @@
1
- .DS_Store
2
  node_modules
3
- /build
 
 
 
 
 
4
  /.svelte-kit
5
- /package
 
 
 
 
 
 
6
  .env
7
  .env.*
8
  !.env.example
 
 
 
9
  vite.config.js.timestamp-*
10
  vite.config.ts.timestamp-*
11
- public
 
 
1
  node_modules
2
+
3
+ # Output
4
+ .output
5
+ .vercel
6
+ .netlify
7
+ .wrangler
8
  /.svelte-kit
9
+ /build
10
+
11
+ # OS
12
+ .DS_Store
13
+ Thumbs.db
14
+
15
+ # Env
16
  .env
17
  .env.*
18
  !.env.example
19
+ !.env.test
20
+
21
+ # Vite
22
  vite.config.js.timestamp-*
23
  vite.config.ts.timestamp-*
24
+ public/
frontend/.prettierignore CHANGED
@@ -1,13 +1,6 @@
1
- .DS_Store
2
- node_modules
3
- /build
4
- /.svelte-kit
5
- /package
6
- .env
7
- .env.*
8
- !.env.example
9
-
10
- # Ignore files for PNPM, NPM and YARN
11
- pnpm-lock.yaml
12
  package-lock.json
 
13
  yarn.lock
 
 
 
1
+ # Package Managers
 
 
 
 
 
 
 
 
 
 
2
  package-lock.json
3
+ pnpm-lock.yaml
4
  yarn.lock
5
+ bun.lock
6
+ bun.lockb
frontend/.prettierrc CHANGED
@@ -1,13 +1,5 @@
1
  {
2
- "useTabs": false,
3
- "singleQuote": true,
4
- "trailingComma": "none",
5
- "printWidth": 100,
6
- "plugins": [
7
- "prettier-plugin-svelte",
8
- "prettier-plugin-organize-imports",
9
- "prettier-plugin-tailwindcss"
10
- ],
11
  "overrides": [
12
  {
13
  "files": "*.svelte",
 
1
  {
2
+ "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
 
 
 
 
 
 
 
 
3
  "overrides": [
4
  {
5
  "files": "*.svelte",
frontend/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # create-svelte
2
 
3
- Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
4
 
5
  ## Creating a project
6
 
@@ -8,10 +8,10 @@ If you're seeing this, you've probably already done this step. Congrats!
8
 
9
  ```bash
10
  # create a new project in the current directory
11
- npm create svelte@latest
12
 
13
  # create a new project in my-app
14
- npm create svelte@latest my-app
15
  ```
16
 
17
  ## Developing
@@ -35,4 +35,4 @@ npm run build
35
 
36
  You can preview the production build with `npm run preview`.
37
 
38
- > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
 
1
+ # sv
2
 
3
+ Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
4
 
5
  ## Creating a project
6
 
 
8
 
9
  ```bash
10
  # create a new project in the current directory
11
+ npx sv create
12
 
13
  # create a new project in my-app
14
+ npx sv create my-app
15
  ```
16
 
17
  ## Developing
 
35
 
36
  You can preview the production build with `npm run preview`.
37
 
38
+ > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
frontend/package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
frontend/package.json CHANGED
@@ -1,41 +1,40 @@
1
  {
2
  "name": "frontend",
3
- "version": "0.0.1",
4
  "private": true,
 
 
5
  "scripts": {
6
  "dev": "vite dev",
7
  "build": "vite build",
8
  "preview": "vite preview",
 
9
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
10
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
11
- "lint": "prettier --check . && eslint .",
12
  "format": "prettier --write ."
13
  },
14
  "devDependencies": {
15
- "@sveltejs/adapter-auto": "^3.3.1",
 
16
  "@sveltejs/adapter-static": "^3.0.8",
17
- "@sveltejs/kit": "^2.20.2",
18
- "@sveltejs/vite-plugin-svelte": "^3.1.2",
19
- "@types/eslint": "^8.56.12",
20
- "@typescript-eslint/eslint-plugin": "^7.18.0",
21
- "@typescript-eslint/parser": "^7.18.0",
22
- "autoprefixer": "^10.4.21",
23
- "eslint": "^8.57.1",
24
- "eslint-config-prettier": "^9.1.0",
25
- "eslint-plugin-svelte": "^2.46.1",
26
- "postcss": "^8.5.3",
27
- "prettier": "^3.5.3",
28
- "prettier-plugin-organize-imports": "^3.2.4",
29
  "prettier-plugin-svelte": "^3.3.3",
30
  "prettier-plugin-tailwindcss": "^0.6.11",
31
- "svelte": "^5.0.0-next.65",
32
- "svelte-check": "^3.8.6",
33
- "tailwindcss": "^3.4.17",
34
- "tslib": "^2.8.1",
35
- "typescript": "^5.8.2",
36
- "vite": "^5.4.14"
37
  },
38
- "type": "module",
39
  "dependencies": {
40
  "piexifjs": "^1.0.6",
41
  "rvfc-polyfill": "^1.0.7"
 
1
  {
2
  "name": "frontend",
 
3
  "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
  "scripts": {
7
  "dev": "vite dev",
8
  "build": "vite build",
9
  "preview": "vite preview",
10
+ "prepare": "svelte-kit sync || echo ''",
11
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
12
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
13
+ "lint": "eslint . && prettier --check .",
14
  "format": "prettier --write ."
15
  },
16
  "devDependencies": {
17
+ "@eslint/compat": "^1.2.5",
18
+ "@eslint/js": "^9.26.0",
19
  "@sveltejs/adapter-static": "^3.0.8",
20
+ "@sveltejs/kit": "^2.16.0",
21
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
22
+ "@tailwindcss/typography": "^0.5.15",
23
+ "@tailwindcss/vite": "^4.1.5",
24
+ "eslint": "^9.26.0",
25
+ "eslint-config-prettier": "^10.0.1",
26
+ "eslint-plugin-svelte": "^3.0.0",
27
+ "globals": "^16.0.0",
28
+ "prettier": "^3.4.2",
 
 
 
29
  "prettier-plugin-svelte": "^3.3.3",
30
  "prettier-plugin-tailwindcss": "^0.6.11",
31
+ "svelte": "^5.0.0",
32
+ "svelte-check": "^4.0.0",
33
+ "tailwindcss": "^4.1.5",
34
+ "typescript": "^5.0.0",
35
+ "typescript-eslint": "^8.20.0",
36
+ "vite": "^6.2.6"
37
  },
 
38
  "dependencies": {
39
  "piexifjs": "^1.0.6",
40
  "rvfc-polyfill": "^1.0.7"
frontend/postcss.config.js DELETED
@@ -1,6 +0,0 @@
1
- export default {
2
- plugins: {
3
- tailwindcss: {},
4
- autoprefixer: {}
5
- }
6
- };
 
 
 
 
 
 
 
frontend/src/app.css CHANGED
@@ -1,3 +1,2 @@
1
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
 
1
+ @import "tailwindcss";
2
+ @plugin '@tailwindcss/typography';
 
frontend/src/app.d.ts CHANGED
@@ -1,12 +1,13 @@
1
- // See https://kit.svelte.dev/docs/types#app
2
  // for information about these interfaces
3
  declare global {
4
- namespace App {
5
- // interface Error {}
6
- // interface Locals {}
7
- // interface PageData {}
8
- // interface Platform {}
9
- }
 
10
  }
11
 
12
  export {};
 
1
+ // See https://svelte.dev/docs/kit/types#app.d.ts
2
  // for information about these interfaces
3
  declare global {
4
+ namespace App {
5
+ // interface Error {}
6
+ // interface Locals {}
7
+ // interface PageData {}
8
+ // interface PageState {}
9
+ // interface Platform {}
10
+ }
11
  }
12
 
13
  export {};
frontend/src/app.html CHANGED
@@ -1,12 +1,12 @@
1
  <!doctype html>
2
  <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <link rel="icon" href="%sveltekit.assets%/favicon.png" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1" />
7
- %sveltekit.head%
8
- </head>
9
- <body data-sveltekit-preload-data="hover">
10
- <div style="display: contents">%sveltekit.body%</div>
11
- </body>
12
  </html>
 
1
  <!doctype html>
2
  <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="%sveltekit.assets%/favicon.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ %sveltekit.head%
8
+ </head>
9
+ <body data-sveltekit-preload-data="hover">
10
+ <div style="display: contents">%sveltekit.body%</div>
11
+ </body>
12
  </html>
frontend/src/lib/components/AspectRatioSelect.svelte CHANGED
@@ -1,26 +1,27 @@
1
  <script lang="ts">
2
- import { createEventDispatcher } from 'svelte';
 
 
 
3
 
4
- let options: string[] = ['1:1', '16:9', '4:3', '3:2', '3:4', '9:16'];
5
- export let aspectRatio: number = 1;
6
- const dispatchEvent = createEventDispatcher();
7
 
8
  function onChange(e: Event) {
9
  const target = e.target as HTMLSelectElement;
10
  const value = target.value;
11
- const [width, height] = value.split(':').map((v) => parseInt(v));
12
  aspectRatio = width / height;
13
- dispatchEvent('change', aspectRatio);
14
  }
15
  </script>
16
 
17
  <div class="relative">
18
  <select
19
- on:change={onChange}
20
  title="Aspect Ratio"
21
- class="border-1 block cursor-pointer rounded-md border-gray-800 border-opacity-50 bg-slate-100 bg-opacity-30 p-1 font-medium text-white"
22
  >
23
- {#each options as option, i}
24
  <option value={option}>{option}</option>
25
  {/each}
26
  </select>
 
1
  <script lang="ts">
2
+ let {
3
+ change,
4
+ aspectRatio = $bindable(),
5
+ }: { change: (aspectRatio: number) => void; aspectRatio: number } = $props();
6
 
7
+ let options: string[] = ["1:1", "16:9", "4:3", "3:2", "3:4", "9:16"];
 
 
8
 
9
  function onChange(e: Event) {
10
  const target = e.target as HTMLSelectElement;
11
  const value = target.value;
12
+ const [width, height] = value.split(":").map((v) => parseInt(v));
13
  aspectRatio = width / height;
14
+ change(aspectRatio);
15
  }
16
  </script>
17
 
18
  <div class="relative">
19
  <select
20
+ onchange={onChange}
21
  title="Aspect Ratio"
22
+ class="block cursor-pointer rounded-md border border-gray-800/50 bg-slate-100/30 p-1 font-medium text-white"
23
  >
24
+ {#each options as option (option)}
25
  <option value={option}>{option}</option>
26
  {/each}
27
  </select>
frontend/src/lib/components/Button.svelte CHANGED
@@ -1,15 +1,14 @@
1
  <script lang="ts">
2
- export let classList: string = 'p-2';
3
- export let disabled: boolean = false;
4
- export let title: string = '';
5
  </script>
6
 
7
- <button class="button {classList}" on:click {disabled} {title}>
8
- <slot />
9
  </button>
10
 
11
- <style lang="postcss" scoped>
12
- .button {
13
- @apply rounded bg-gray-700 font-normal text-white hover:bg-gray-800 disabled:cursor-not-allowed disabled:bg-gray-300 dark:disabled:bg-gray-700 dark:disabled:text-black;
 
14
  }
15
  </style>
 
1
  <script lang="ts">
2
+ let props = $props();
 
 
3
  </script>
4
 
5
+ <button {...props}>
6
+ {@render props.children()}
7
  </button>
8
 
9
+ <style lang="postcss">
10
+ @reference "tailwindcss";
11
+ button {
12
+ @apply cursor-pointer rounded bg-gray-700 font-normal text-white hover:bg-gray-800 disabled:cursor-not-allowed disabled:bg-gray-300 dark:disabled:bg-gray-700 dark:disabled:text-black;
13
  }
14
  </style>
frontend/src/lib/components/Checkbox.svelte CHANGED
@@ -1,14 +1,20 @@
1
  <script lang="ts">
2
- import type { FieldProps } from '$lib/types';
3
- import { onMount } from 'svelte';
4
- export let value = false;
5
- export let params: FieldProps;
 
6
  onMount(() => {
7
- value = Boolean(params?.default) ?? 8.0;
8
  });
9
  </script>
10
 
11
  <div class="grid max-w-md grid-cols-4 items-center justify-items-start gap-3">
12
  <label class="text-sm font-medium" for={params.id}>{params?.title}</label>
13
- <input bind:checked={value} type="checkbox" id={params.id} class="cursor-pointer" />
 
 
 
 
 
14
  </div>
 
1
  <script lang="ts">
2
+ import type { FieldProps } from "$lib/types";
3
+ import { onMount } from "svelte";
4
+ let { value = $bindable(), params }: { value: boolean; params: FieldProps } =
5
+ $props();
6
+
7
  onMount(() => {
8
+ value = params?.default ? true : false;
9
  });
10
  </script>
11
 
12
  <div class="grid max-w-md grid-cols-4 items-center justify-items-start gap-3">
13
  <label class="text-sm font-medium" for={params.id}>{params?.title}</label>
14
+ <input
15
+ bind:checked={value}
16
+ type="checkbox"
17
+ id={params.id}
18
+ class="cursor-pointer"
19
+ />
20
  </div>
frontend/src/lib/components/ImagePlayer.svelte CHANGED
@@ -1,31 +1,35 @@
1
  <script lang="ts">
2
- import { lcmLiveStatus, LCMLiveStatus, streamId } from '$lib/lcmLive';
3
- import { getPipelineValues } from '$lib/store';
4
 
5
- import Button from '$lib/components/Button.svelte';
6
- import Floppy from '$lib/icons/floppy.svelte';
7
- import Expand from '$lib/icons/expand.svelte';
8
- import { snapImage, expandWindow } from '$lib/utils';
 
 
 
 
 
 
 
 
 
9
 
10
- $: isLCMRunning =
11
- $lcmLiveStatus !== LCMLiveStatus.DISCONNECTED && $lcmLiveStatus !== LCMLiveStatus.ERROR;
12
- let imageEl: HTMLImageElement;
13
- let expandedWindow: Window;
14
- let isExpanded = false;
15
  async function takeSnapshot() {
16
- if (isLCMRunning) {
17
  await snapImage(imageEl, {
18
- prompt: getPipelineValues()?.prompt,
19
- negative_prompt: getPipelineValues()?.negative_prompt,
20
- seed: getPipelineValues()?.seed,
21
- guidance_scale: getPipelineValues()?.guidance_scale
22
  });
23
  }
24
  }
25
  async function toggleFullscreen() {
26
  if (isLCMRunning && !isExpanded) {
27
- expandedWindow = expandWindow('/api/stream/' + $streamId);
28
- expandedWindow.addEventListener('beforeunload', () => {
29
  isExpanded = false;
30
  });
31
  isExpanded = true;
@@ -39,22 +43,24 @@
39
  <div
40
  class="relative mx-auto aspect-square max-w-lg self-center overflow-hidden rounded-lg border border-slate-300"
41
  >
42
- <!-- svelte-ignore a11y-missing-attribute -->
43
  {#if $lcmLiveStatus === LCMLiveStatus.CONNECTING}
44
  <!-- Show connecting spinner -->
45
  <div class="flex h-full w-full items-center justify-center">
46
- <div class="h-16 w-16 animate-spin rounded-full border-b-2 border-white"></div>
 
 
47
  <p class="ml-2 text-white">Connecting...</p>
48
  </div>
49
  {:else if isLCMRunning}
50
  {#if !isExpanded}
51
  <!-- Handle image error by adding onerror event -->
 
52
  <img
53
  bind:this={imageEl}
54
  class="aspect-square w-full rounded-lg"
55
- src={'/api/stream/' + $streamId}
56
- on:error={(e) => {
57
- console.error('Image stream error:', e);
58
  // If stream fails to load, set status to error
59
  if ($lcmLiveStatus !== LCMLiveStatus.ERROR) {
60
  lcmLiveStatus.set(LCMLiveStatus.ERROR);
@@ -64,19 +70,19 @@
64
  {/if}
65
  <div class="absolute bottom-1 right-1">
66
  <Button
67
- on:click={toggleFullscreen}
68
- title={'Expand Fullscreen'}
69
- classList={'text-sm ml-auto text-white p-1 shadow-lg rounded-lg opacity-50'}
70
  >
71
- <Expand classList={''} />
72
  </Button>
73
  <Button
74
- on:click={takeSnapshot}
75
  disabled={!isLCMRunning}
76
- title={'Take Snapshot'}
77
- classList={'text-sm ml-auto text-white p-1 shadow-lg rounded-lg opacity-50'}
78
  >
79
- <Floppy classList={''} />
80
  </Button>
81
  </div>
82
  {:else if $lcmLiveStatus === LCMLiveStatus.ERROR}
@@ -87,6 +93,7 @@
87
  <p class="p-4 text-center text-white">Connection error</p>
88
  </div>
89
  {:else}
 
90
  <img
91
  class="aspect-square w-full rounded-lg"
92
  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
 
1
  <script lang="ts">
2
+ import { lcmLiveStatus, LCMLiveStatus, streamId } from "$lib/lcmLive";
3
+ import { getPipelineValues } from "$lib/store";
4
 
5
+ import Button from "$lib/components/Button.svelte";
6
+ import Floppy from "$lib/icons/floppy.svelte";
7
+ import Expand from "$lib/icons/expand.svelte";
8
+ import { snapImage, expandWindow } from "$lib/utils";
9
+
10
+ let isLCMRunning = $derived(
11
+ $lcmLiveStatus !== LCMLiveStatus.DISCONNECTED &&
12
+ $lcmLiveStatus !== LCMLiveStatus.ERROR,
13
+ );
14
+
15
+ let imageEl: HTMLImageElement | undefined = $state();
16
+ let expandedWindow: Window | undefined = $state();
17
+ let isExpanded = $state(false);
18
 
 
 
 
 
 
19
  async function takeSnapshot() {
20
+ if (isLCMRunning && imageEl) {
21
  await snapImage(imageEl, {
22
+ prompt: getPipelineValues()?.prompt as string,
23
+ negative_prompt: getPipelineValues()?.negative_prompt as string,
24
+ seed: getPipelineValues()?.seed as number,
25
+ guidance_scale: getPipelineValues()?.guidance_scale as number,
26
  });
27
  }
28
  }
29
  async function toggleFullscreen() {
30
  if (isLCMRunning && !isExpanded) {
31
+ expandedWindow = expandWindow("/api/stream/" + $streamId);
32
+ expandedWindow.addEventListener("beforeunload", () => {
33
  isExpanded = false;
34
  });
35
  isExpanded = true;
 
43
  <div
44
  class="relative mx-auto aspect-square max-w-lg self-center overflow-hidden rounded-lg border border-slate-300"
45
  >
 
46
  {#if $lcmLiveStatus === LCMLiveStatus.CONNECTING}
47
  <!-- Show connecting spinner -->
48
  <div class="flex h-full w-full items-center justify-center">
49
+ <div
50
+ class="h-16 w-16 animate-spin rounded-full border-b-2 border-white"
51
+ ></div>
52
  <p class="ml-2 text-white">Connecting...</p>
53
  </div>
54
  {:else if isLCMRunning}
55
  {#if !isExpanded}
56
  <!-- Handle image error by adding onerror event -->
57
+ <!-- svelte-ignore a11y_missing_attribute -->
58
  <img
59
  bind:this={imageEl}
60
  class="aspect-square w-full rounded-lg"
61
+ src={"/api/stream/" + $streamId}
62
+ onerror={(e) => {
63
+ console.error("Image stream error:", e);
64
  // If stream fails to load, set status to error
65
  if ($lcmLiveStatus !== LCMLiveStatus.ERROR) {
66
  lcmLiveStatus.set(LCMLiveStatus.ERROR);
 
70
  {/if}
71
  <div class="absolute bottom-1 right-1">
72
  <Button
73
+ onclick={toggleFullscreen}
74
+ title="Expand Fullscreen"
75
+ class="ml-auto rounded-lg p-1 text-sm text-white opacity-50 shadow-lg"
76
  >
77
+ <Expand />
78
  </Button>
79
  <Button
80
+ onclick={takeSnapshot}
81
  disabled={!isLCMRunning}
82
+ title="Take Snapshot"
83
+ class="ml-auto rounded-lg p-1 text-sm text-white opacity-50 shadow-lg"
84
  >
85
+ <Floppy />
86
  </Button>
87
  </div>
88
  {:else if $lcmLiveStatus === LCMLiveStatus.ERROR}
 
93
  <p class="p-4 text-center text-white">Connection error</p>
94
  </div>
95
  {:else}
96
+ <!-- svelte-ignore a11y_missing_attribute -->
97
  <img
98
  class="aspect-square w-full rounded-lg"
99
  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
frontend/src/lib/components/InputRange.svelte CHANGED
@@ -1,10 +1,11 @@
1
  <script lang="ts">
2
- import type { FieldProps } from '$lib/types';
3
- import { onMount } from 'svelte';
4
- export let value = 8.0;
5
- export let params: FieldProps;
 
6
  onMount(() => {
7
- value = Number(params?.default) ?? 8.0;
8
  });
9
  </script>
10
 
 
1
  <script lang="ts">
2
+ import type { FieldProps } from "$lib/types";
3
+ import { onMount } from "svelte";
4
+ let { value = $bindable(), params }: { value: number; params: FieldProps } =
5
+ $props();
6
+
7
  onMount(() => {
8
+ value = Number(params?.default ?? "");
9
  });
10
  </script>
11
 
frontend/src/lib/components/MediaListSwitcher.svelte CHANGED
@@ -1,45 +1,41 @@
1
  <script lang="ts">
2
- import { mediaDevices, mediaStreamActions } from '$lib/mediaStream';
3
- import Screen from '$lib/icons/screen.svelte';
4
- import AspectRatioSelect from './AspectRatioSelect.svelte';
5
- import { onMount } from 'svelte';
6
 
7
- let deviceId: string = '';
8
  let aspectRatio: number = 1;
9
 
10
  onMount(() => {
11
  deviceId = $mediaDevices[0].deviceId;
12
  });
13
- $: {
14
- console.log(deviceId);
15
- }
16
- $: {
17
- console.log(aspectRatio);
18
- }
19
  </script>
20
 
21
- <div class="flex items-center justify-center text-xs backdrop-blur-sm backdrop-grayscale">
 
 
22
  <AspectRatioSelect
23
  bind:aspectRatio
24
- on:change={() => mediaStreamActions.switchCamera(deviceId, aspectRatio)}
25
  />
26
  <button
27
  title="Share your screen"
28
- class="border-1 my-1 flex cursor-pointer gap-1 rounded-md border-gray-500 border-opacity-50 bg-slate-100 bg-opacity-30 p-1 font-medium text-white"
29
- on:click={() => mediaStreamActions.startScreenCapture()}
30
  >
31
  <span>Share</span>
32
 
33
- <Screen classList={''} />
34
  </button>
35
  {#if $mediaDevices}
36
  <select
37
  bind:value={deviceId}
38
- on:change={() => mediaStreamActions.switchCamera(deviceId, aspectRatio)}
39
  id="devices-list"
40
- class="border-1 block cursor-pointer rounded-md border-gray-800 border-opacity-50 bg-slate-100 bg-opacity-30 p-1 font-medium text-white"
41
  >
42
- {#each $mediaDevices as device, i}
43
  <option value={device.deviceId}>{device.label}</option>
44
  {/each}
45
  </select>
 
1
  <script lang="ts">
2
+ import { mediaDevices, mediaStreamActions } from "$lib/mediaStream";
3
+ import Screen from "$lib/icons/screen.svelte";
4
+ import AspectRatioSelect from "./AspectRatioSelect.svelte";
5
+ import { onMount } from "svelte";
6
 
7
+ let deviceId: string = "";
8
  let aspectRatio: number = 1;
9
 
10
  onMount(() => {
11
  deviceId = $mediaDevices[0].deviceId;
12
  });
 
 
 
 
 
 
13
  </script>
14
 
15
+ <div
16
+ class="flex items-center justify-center text-xs backdrop-blur-sm backdrop-grayscale"
17
+ >
18
  <AspectRatioSelect
19
  bind:aspectRatio
20
+ change={(value) => mediaStreamActions.switchCamera(deviceId, value)}
21
  />
22
  <button
23
  title="Share your screen"
24
+ class="my-1 flex cursor-pointer gap-1 rounded-md border border-gray-500/50 bg-slate-100/30 p-1 font-medium text-white"
25
+ onclick={() => mediaStreamActions.startScreenCapture()}
26
  >
27
  <span>Share</span>
28
 
29
+ <Screen />
30
  </button>
31
  {#if $mediaDevices}
32
  <select
33
  bind:value={deviceId}
34
+ onchange={() => mediaStreamActions.switchCamera(deviceId, aspectRatio)}
35
  id="devices-list"
36
+ class="block cursor-pointer rounded-md border border-gray-800/50 bg-slate-100/30 p-1 font-medium text-white"
37
  >
38
+ {#each $mediaDevices as device (device.deviceId)}
39
  <option value={device.deviceId}>{device.label}</option>
40
  {/each}
41
  </select>
frontend/src/lib/components/PipelineOptions.svelte CHANGED
@@ -1,33 +1,42 @@
1
  <script lang="ts">
2
- import type { Fields } from '$lib/types';
3
- import { FieldType } from '$lib/types';
4
- import InputRange from './InputRange.svelte';
5
- import SeedInput from './SeedInput.svelte';
6
- import TextArea from './TextArea.svelte';
7
- import Checkbox from './Checkbox.svelte';
8
- import Selectlist from './Selectlist.svelte';
9
- import { pipelineValues } from '$lib/store';
10
 
11
- export let pipelineParams: Fields;
12
 
13
- $: advanceOptions = Object.values(pipelineParams)?.filter((e) => e?.hide == true);
14
- $: featuredOptions = Object.values(pipelineParams)?.filter((e) => e?.hide !== true);
 
 
 
 
15
  </script>
16
 
17
  <div class="flex flex-col gap-3">
18
  <div class="grid grid-cols-1 items-center gap-3">
19
  {#if featuredOptions}
20
- {#each featuredOptions as params}
21
  {#if params.field === FieldType.RANGE}
22
- <InputRange {params} bind:value={$pipelineValues[params.id]}></InputRange>
 
23
  {:else if params.field === FieldType.SEED}
24
- <SeedInput {params} bind:value={$pipelineValues[params.id]}></SeedInput>
 
25
  {:else if params.field === FieldType.TEXTAREA}
26
- <TextArea {params} bind:value={$pipelineValues[params.id]}></TextArea>
 
27
  {:else if params.field === FieldType.CHECKBOX}
28
- <Checkbox {params} bind:value={$pipelineValues[params.id]}></Checkbox>
 
29
  {:else if params.field === FieldType.SELECT}
30
- <Selectlist {params} bind:value={$pipelineValues[params.id]}></Selectlist>
 
31
  {/if}
32
  {/each}
33
  {/if}
@@ -36,22 +45,28 @@
36
  <details>
37
  <summary class="cursor-pointer font-medium">Advanced Options</summary>
38
  <div
39
- class="grid grid-cols-1 items-center gap-3 {Object.values(pipelineParams).length > 5
 
40
  ? 'sm:grid-cols-2'
41
  : ''}"
42
  >
43
  {#if advanceOptions}
44
- {#each advanceOptions as params}
45
  {#if params.field === FieldType.RANGE}
46
- <InputRange {params} bind:value={$pipelineValues[params.id]}></InputRange>
 
47
  {:else if params.field === FieldType.SEED}
48
- <SeedInput {params} bind:value={$pipelineValues[params.id]}></SeedInput>
 
49
  {:else if params.field === FieldType.TEXTAREA}
50
- <TextArea {params} bind:value={$pipelineValues[params.id]}></TextArea>
 
51
  {:else if params.field === FieldType.CHECKBOX}
52
- <Checkbox {params} bind:value={$pipelineValues[params.id]}></Checkbox>
 
53
  {:else if params.field === FieldType.SELECT}
54
- <Selectlist {params} bind:value={$pipelineValues[params.id]}></Selectlist>
 
55
  {/if}
56
  {/each}
57
  {/if}
 
1
  <script lang="ts">
2
+ import type { Fields } from "$lib/types";
3
+ import { FieldType } from "$lib/types";
4
+ import InputRange from "./InputRange.svelte";
5
+ import SeedInput from "./SeedInput.svelte";
6
+ import TextArea from "./TextArea.svelte";
7
+ import Checkbox from "./Checkbox.svelte";
8
+ import Selectlist from "./Selectlist.svelte";
9
+ import { pipelineValues } from "$lib/store";
10
 
11
+ let { pipelineParams = $bindable() }: { pipelineParams: Fields } = $props();
12
 
13
+ let advanceOptions = $derived(
14
+ Object.values(pipelineParams)?.filter((e) => e?.hide == true),
15
+ );
16
+ let featuredOptions = $derived(
17
+ Object.values(pipelineParams)?.filter((e) => e?.hide !== true),
18
+ );
19
  </script>
20
 
21
  <div class="flex flex-col gap-3">
22
  <div class="grid grid-cols-1 items-center gap-3">
23
  {#if featuredOptions}
24
+ {#each featuredOptions as params (params.id)}
25
  {#if params.field === FieldType.RANGE}
26
+ <InputRange {params} bind:value={$pipelineValues[params.id] as number}
27
+ ></InputRange>
28
  {:else if params.field === FieldType.SEED}
29
+ <SeedInput {params} bind:value={$pipelineValues[params.id] as number}
30
+ ></SeedInput>
31
  {:else if params.field === FieldType.TEXTAREA}
32
+ <TextArea {params} bind:value={$pipelineValues[params.id] as string}
33
+ ></TextArea>
34
  {:else if params.field === FieldType.CHECKBOX}
35
+ <Checkbox {params} bind:value={$pipelineValues[params.id] as boolean}
36
+ ></Checkbox>
37
  {:else if params.field === FieldType.SELECT}
38
+ <Selectlist {params} bind:value={$pipelineValues[params.id] as string}
39
+ ></Selectlist>
40
  {/if}
41
  {/each}
42
  {/if}
 
45
  <details>
46
  <summary class="cursor-pointer font-medium">Advanced Options</summary>
47
  <div
48
+ class="grid grid-cols-1 items-center gap-3 {Object.values(pipelineParams)
49
+ .length > 5
50
  ? 'sm:grid-cols-2'
51
  : ''}"
52
  >
53
  {#if advanceOptions}
54
+ {#each advanceOptions as params (params.id)}
55
  {#if params.field === FieldType.RANGE}
56
+ <InputRange {params} bind:value={$pipelineValues[params.id]}
57
+ ></InputRange>
58
  {:else if params.field === FieldType.SEED}
59
+ <SeedInput {params} bind:value={$pipelineValues[params.id]}
60
+ ></SeedInput>
61
  {:else if params.field === FieldType.TEXTAREA}
62
+ <TextArea {params} bind:value={$pipelineValues[params.id]}
63
+ ></TextArea>
64
  {:else if params.field === FieldType.CHECKBOX}
65
+ <Checkbox {params} bind:value={$pipelineValues[params.id]}
66
+ ></Checkbox>
67
  {:else if params.field === FieldType.SELECT}
68
+ <Selectlist {params} bind:value={$pipelineValues[params.id]}
69
+ ></Selectlist>
70
  {/if}
71
  {/each}
72
  {/if}
frontend/src/lib/components/SeedInput.svelte CHANGED
@@ -1,16 +1,16 @@
1
  <script lang="ts">
2
- import type { FieldProps } from '$lib/types';
3
- import { onMount } from 'svelte';
4
- import Button from './Button.svelte';
5
- export let value = 299792458;
6
- export let params: FieldProps;
7
 
8
- onMount(() => {
9
- value = Number(params?.default ?? '');
10
- });
11
  function randomize() {
12
  value = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
13
  }
 
 
 
14
  </script>
15
 
16
  <div class="grid max-w-md grid-cols-4 items-center gap-3">
@@ -22,5 +22,5 @@
22
  name="seed"
23
  class="col-span-2 rounded-md border border-gray-700 p-2 text-right font-light dark:text-black"
24
  />
25
- <Button on:click={randomize}>Rand</Button>
26
  </div>
 
1
  <script lang="ts">
2
+ import Button from "./Button.svelte";
3
+ import { onMount } from "svelte";
4
+ import type { FieldProps } from "$lib/types";
5
+ let { value = $bindable(), params }: { value: number; params: FieldProps } =
6
+ $props();
7
 
 
 
 
8
  function randomize() {
9
  value = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
10
  }
11
+ onMount(() => {
12
+ value = Number(params?.default ?? "");
13
+ });
14
  </script>
15
 
16
  <div class="grid max-w-md grid-cols-4 items-center gap-3">
 
22
  name="seed"
23
  class="col-span-2 rounded-md border border-gray-700 p-2 text-right font-light dark:text-black"
24
  />
25
+ <Button onclick={randomize}>Rand</Button>
26
  </div>
frontend/src/lib/components/Selectlist.svelte CHANGED
@@ -1,8 +1,9 @@
1
  <script lang="ts">
2
- import type { FieldProps } from '$lib/types';
3
- import { onMount } from 'svelte';
4
- export let value = '';
5
- export let params: FieldProps;
 
6
  onMount(() => {
7
  value = String(params?.default);
8
  });
@@ -16,7 +17,7 @@
16
  id="model-list"
17
  class="cursor-pointer rounded-md border-2 border-gray-500 p-2 font-light dark:text-black"
18
  >
19
- {#each params.values as model, i}
20
  <option value={model} selected={i === 0}>{model}</option>
21
  {/each}
22
  </select>
 
1
  <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import type { FieldProps } from "$lib/types";
4
+ let { value = $bindable(""), params }: { value: string; params: FieldProps } =
5
+ $props();
6
+
7
  onMount(() => {
8
  value = String(params?.default);
9
  });
 
17
  id="model-list"
18
  class="cursor-pointer rounded-md border-2 border-gray-500 p-2 font-light dark:text-black"
19
  >
20
+ {#each params.values as model, i (model)}
21
  <option value={model} selected={i === 0}>{model}</option>
22
  {/each}
23
  </select>
frontend/src/lib/components/TextArea.svelte CHANGED
@@ -1,10 +1,11 @@
1
  <script lang="ts">
2
- import type { FieldProps } from '$lib/types';
3
- import { onMount } from 'svelte';
4
- export let value: string;
5
- export let params: FieldProps;
 
6
  onMount(() => {
7
- value = String(params?.default ?? '');
8
  });
9
  </script>
10
 
 
1
  <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import type { FieldProps } from "$lib/types";
4
+ let { value = $bindable(), params }: { value: string; params: FieldProps } =
5
+ $props();
6
+
7
  onMount(() => {
8
+ value = String(params?.default ?? "");
9
  });
10
  </script>
11
 
frontend/src/lib/components/VideoInput.svelte CHANGED
@@ -1,54 +1,66 @@
1
  <script lang="ts">
2
- import 'rvfc-polyfill';
3
 
4
- import { onDestroy, onMount } from 'svelte';
5
  import {
6
  mediaStreamStatus,
7
  MediaStreamStatusEnum,
8
  onFrameChangeStore,
9
  mediaStream,
10
- mediaDevices
11
- } from '$lib/mediaStream';
12
- import MediaListSwitcher from './MediaListSwitcher.svelte';
13
- import Button from '$lib/components/Button.svelte';
14
- import Expand from '$lib/icons/expand.svelte';
 
 
 
15
 
16
- export let width = 512;
17
- export let height = 512;
18
  const size = { width, height };
19
 
20
- let videoEl: HTMLVideoElement;
21
- let canvasEl: HTMLCanvasElement;
22
- let ctx: CanvasRenderingContext2D;
23
- let videoFrameCallbackId: number;
24
 
25
  // ajust the throttle time to your needs
26
  const THROTTLE = 1000 / 120;
27
- let selectedDevice: string = '';
28
- let videoIsReady = false;
29
 
30
  onMount(() => {
31
- ctx = canvasEl.getContext('2d') as CanvasRenderingContext2D;
32
- canvasEl.width = size.width;
33
- canvasEl.height = size.height;
 
 
34
  });
35
- $: {
36
  console.log(selectedDevice);
37
- }
38
 
39
  onDestroy(() => {
40
- if (videoFrameCallbackId) videoEl.cancelVideoFrameCallback(videoFrameCallbackId);
 
 
 
 
 
 
 
 
41
  });
42
 
43
- $: if (videoEl) {
44
- videoEl.srcObject = $mediaStream;
45
- }
46
  let lastMillis = 0;
47
- async function onFrameChange(now: DOMHighResTimeStamp, metadata: VideoFrameCallbackMetadata) {
 
 
48
  if (now - lastMillis < THROTTLE) {
49
  videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
50
  return;
51
  }
 
 
52
  const videoWidth = videoEl.videoWidth;
53
  const videoHeight = videoEl.videoHeight;
54
  // scale down video to fit canvas, size.width, size.height
@@ -61,24 +73,30 @@
61
  ctx.drawImage(videoEl, x0, y0, width0, height0);
62
 
63
  const blob = await new Promise<Blob>((resolve) => {
64
- canvasEl.toBlob(
65
  (blob) => {
66
  resolve(blob as Blob);
67
  },
68
- 'image/jpeg',
69
- 1
70
  );
71
  });
72
  onFrameChangeStore.set({ blob });
73
  videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
74
  }
75
 
76
- $: if ($mediaStreamStatus == MediaStreamStatusEnum.CONNECTED && videoIsReady) {
77
- videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
78
- }
 
 
 
 
 
 
79
 
80
  function toggleFullscreen() {
81
- if (videoIsReady) {
82
  if (document.fullscreenElement) {
83
  document.exitFullscreen();
84
  } else {
@@ -88,24 +106,28 @@
88
  }
89
  </script>
90
 
91
- <div class="relative mx-auto max-w-lg overflow-hidden rounded-lg border border-slate-300">
92
- <div class="relative z-10 flex aspect-square w-full items-center justify-center object-cover">
 
 
 
 
93
  {#if $mediaDevices.length > 0}
94
- <div class="absolute bottom-0 right-0 z-10 flex bg-slate-400 bg-opacity-40">
95
  <MediaListSwitcher />
96
  <Button
97
- on:click={toggleFullscreen}
98
- title={'Expand Fullscreen'}
99
- classList={'text-sm ml-auto text-white p-1 shadow-lg rounded-lg opacity-50'}
100
  >
101
- <Expand classList={''} />
102
  </Button>
103
  </div>
104
  {/if}
105
  <video
106
  class="pointer-events-none aspect-square w-full justify-center object-contain"
107
  bind:this={videoEl}
108
- on:loadeddata={() => {
109
  videoIsReady = true;
110
  }}
111
  playsinline
@@ -113,11 +135,19 @@
113
  muted
114
  loop
115
  ></video>
116
- <canvas bind:this={canvasEl} class="absolute left-0 top-0 aspect-square w-full object-cover"
 
 
117
  ></canvas>
118
  </div>
119
- <div class="absolute left-0 top-0 flex aspect-square w-full items-center justify-center">
120
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 448" class="w-40 p-5 opacity-20">
 
 
 
 
 
 
121
  <path
122
  fill="currentColor"
123
  d="M224 256a128 128 0 1 0 0-256 128 128 0 1 0 0 256zm-45.7 48A178.3 178.3 0 0 0 0 482.3 29.7 29.7 0 0 0 29.7 512h388.6a29.7 29.7 0 0 0 29.7-29.7c0-98.5-79.8-178.3-178.3-178.3h-91.4z"
 
1
  <script lang="ts">
2
+ import "rvfc-polyfill";
3
 
4
+ import { onDestroy, onMount } from "svelte";
5
  import {
6
  mediaStreamStatus,
7
  MediaStreamStatusEnum,
8
  onFrameChangeStore,
9
  mediaStream,
10
+ mediaDevices,
11
+ } from "$lib/mediaStream";
12
+ import MediaListSwitcher from "./MediaListSwitcher.svelte";
13
+ import Button from "$lib/components/Button.svelte";
14
+ import Expand from "$lib/icons/expand.svelte";
15
+
16
+ let { width = 512, height = 512 }: { width: number; height: number } =
17
+ $props();
18
 
 
 
19
  const size = { width, height };
20
 
21
+ let videoEl: HTMLVideoElement | undefined = $state();
22
+ let canvasEl: HTMLCanvasElement | undefined = $state();
23
+ let ctx: CanvasRenderingContext2D | undefined = $state();
24
+ let videoFrameCallbackId: number | undefined = $state();
25
 
26
  // ajust the throttle time to your needs
27
  const THROTTLE = 1000 / 120;
28
+ let selectedDevice: string = $state("");
29
+ let videoIsReady = $state(false);
30
 
31
  onMount(() => {
32
+ if (canvasEl) {
33
+ ctx = canvasEl.getContext("2d") as CanvasRenderingContext2D;
34
+ canvasEl.width = size.width;
35
+ canvasEl.height = size.height;
36
+ }
37
  });
38
+ $effect(() => {
39
  console.log(selectedDevice);
40
+ });
41
 
42
  onDestroy(() => {
43
+ if (videoFrameCallbackId && videoEl) {
44
+ videoEl.cancelVideoFrameCallback(videoFrameCallbackId);
45
+ }
46
+ });
47
+
48
+ $effect(() => {
49
+ if (videoEl && $mediaStream) {
50
+ videoEl.srcObject = $mediaStream;
51
+ }
52
  });
53
 
 
 
 
54
  let lastMillis = 0;
55
+ async function onFrameChange(now: DOMHighResTimeStamp) {
56
+ if (!videoEl || !ctx || !canvasEl) return;
57
+
58
  if (now - lastMillis < THROTTLE) {
59
  videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
60
  return;
61
  }
62
+
63
+ lastMillis = now;
64
  const videoWidth = videoEl.videoWidth;
65
  const videoHeight = videoEl.videoHeight;
66
  // scale down video to fit canvas, size.width, size.height
 
73
  ctx.drawImage(videoEl, x0, y0, width0, height0);
74
 
75
  const blob = await new Promise<Blob>((resolve) => {
76
+ canvasEl?.toBlob(
77
  (blob) => {
78
  resolve(blob as Blob);
79
  },
80
+ "image/jpeg",
81
+ 1,
82
  );
83
  });
84
  onFrameChangeStore.set({ blob });
85
  videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
86
  }
87
 
88
+ $effect(() => {
89
+ if (
90
+ $mediaStreamStatus == MediaStreamStatusEnum.CONNECTED &&
91
+ videoIsReady &&
92
+ videoEl
93
+ ) {
94
+ videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
95
+ }
96
+ });
97
 
98
  function toggleFullscreen() {
99
+ if (videoIsReady && videoEl) {
100
  if (document.fullscreenElement) {
101
  document.exitFullscreen();
102
  } else {
 
106
  }
107
  </script>
108
 
109
+ <div
110
+ class="relative mx-auto max-w-lg overflow-hidden rounded-lg border border-slate-300"
111
+ >
112
+ <div
113
+ class="relative z-10 flex aspect-square w-full items-center justify-center object-cover"
114
+ >
115
  {#if $mediaDevices.length > 0}
116
+ <div class="absolute bottom-0 right-0 z-10 flex bg-slate-400/40">
117
  <MediaListSwitcher />
118
  <Button
119
+ onclick={toggleFullscreen}
120
+ title="Expand Fullscreen"
121
+ class="ml-auto rounded-lg p-1 text-sm text-white opacity-50 shadow-lg"
122
  >
123
+ <Expand />
124
  </Button>
125
  </div>
126
  {/if}
127
  <video
128
  class="pointer-events-none aspect-square w-full justify-center object-contain"
129
  bind:this={videoEl}
130
+ onloadeddata={() => {
131
  videoIsReady = true;
132
  }}
133
  playsinline
 
135
  muted
136
  loop
137
  ></video>
138
+ <canvas
139
+ bind:this={canvasEl}
140
+ class="absolute left-0 top-0 aspect-square w-full object-cover"
141
  ></canvas>
142
  </div>
143
+ <div
144
+ class="absolute left-0 top-0 flex aspect-square w-full items-center justify-center"
145
+ >
146
+ <svg
147
+ xmlns="http://www.w3.org/2000/svg"
148
+ viewBox="0 0 448 448"
149
+ class="w-40 p-5 opacity-20"
150
+ >
151
  <path
152
  fill="currentColor"
153
  d="M224 256a128 128 0 1 0 0-256 128 128 0 1 0 0 256zm-45.7 48A178.3 178.3 0 0 0 0 482.3 29.7 29.7 0 0 0 29.7 512h388.6a29.7 29.7 0 0 0 29.7-29.7c0-98.5-79.8-178.3-178.3-178.3h-91.4z"
frontend/src/lib/components/Warning.svelte CHANGED
@@ -1,14 +1,16 @@
1
  <script lang="ts">
2
- export let message: string = '';
3
 
4
- let timeout = 0;
5
- $: if (message !== '') {
6
- console.log('message', message);
7
- clearTimeout(timeout);
8
- timeout = setTimeout(() => {
9
- message = '';
10
- }, 5000);
11
- }
 
 
12
  </script>
13
 
14
  {#if message}
@@ -16,8 +18,8 @@
16
  <button
17
  type="button"
18
  class="w-full"
19
- on:click={() => (message = '')}
20
- on:keydown={(e) => e.key === 'Enter' && (message = '')}
21
  >
22
  <div class="rounded bg-red-800 p-4 text-white">
23
  {message}
 
1
  <script lang="ts">
2
+ let { message = $bindable() }: { message: string } = $props();
3
 
4
+ let timeout = $state(0);
5
+ $effect(() => {
6
+ if (message !== "") {
7
+ console.log("message", message);
8
+ clearTimeout(timeout);
9
+ timeout = setTimeout(() => {
10
+ message = "";
11
+ }, 5000);
12
+ }
13
+ });
14
  </script>
15
 
16
  {#if message}
 
18
  <button
19
  type="button"
20
  class="w-full"
21
+ onclick={() => (message = "")}
22
+ onkeydown={(e) => e.key === "Enter" && (message = "")}
23
  >
24
  <div class="rounded bg-red-800 p-4 text-white">
25
  {message}
frontend/src/lib/icons/aspect.svelte CHANGED
@@ -1,8 +1,13 @@
1
  <script lang="ts">
2
- export let classList: string = '';
3
  </script>
4
 
5
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" height="16px" class={classList}>
 
 
 
 
 
6
  <path
7
  fill="currentColor"
8
  d="M32 32C14.3 32 0 46.3 0 64v96c0 17.7 14.3 32 32 32s32-14.3 32-32V96h64c17.7 0 32-14.3 32-32s-14.3-32-32-32H32zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7 14.3 32 32 32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H64V352zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32h64v64c0 17.7 14.3 32 32 32s32-14.3 32-32V64c0-17.7-14.3-32-32-32H320zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64H320c-17.7 0-32 14.3-32 32s14.3 32 32 32h96c17.7 0 32-14.3 32-32V352z"
 
1
  <script lang="ts">
2
+ let props = $props();
3
  </script>
4
 
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ viewBox="0 0 448 512"
8
+ height="16px"
9
+ {...props}
10
+ >
11
  <path
12
  fill="currentColor"
13
  d="M32 32C14.3 32 0 46.3 0 64v96c0 17.7 14.3 32 32 32s32-14.3 32-32V96h64c17.7 0 32-14.3 32-32s-14.3-32-32-32H32zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7 14.3 32 32 32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H64V352zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32h64v64c0 17.7 14.3 32 32 32s32-14.3 32-32V64c0-17.7-14.3-32-32-32H320zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64H320c-17.7 0-32 14.3-32 32s14.3 32 32 32h96c17.7 0 32-14.3 32-32V352z"
frontend/src/lib/icons/expand.svelte CHANGED
@@ -1,8 +1,13 @@
1
  <script lang="ts">
2
- export let classList: string = '';
3
  </script>
4
 
5
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="1em" class={classList}>
 
 
 
 
 
6
  <path
7
  fill="currentColor"
8
  d="M.3 89.5C.1 91.6 0 93.8 0 96V224 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64V224 96c0-35.3-28.7-64-64-64H64c-2.2 0-4.4 .1-6.5 .3c-9.2 .9-17.8 3.8-25.5 8.2C21.8 46.5 13.4 55.1 7.7 65.5c-3.9 7.3-6.5 15.4-7.4 24zM48 224H464l0 192c0 8.8-7.2 16-16 16L64 432c-8.8 0-16-7.2-16-16l0-192z"
 
1
  <script lang="ts">
2
+ let props = $props();
3
  </script>
4
 
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ viewBox="0 0 512 512"
8
+ height="1em"
9
+ {...props}
10
+ >
11
  <path
12
  fill="currentColor"
13
  d="M.3 89.5C.1 91.6 0 93.8 0 96V224 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64V224 96c0-35.3-28.7-64-64-64H64c-2.2 0-4.4 .1-6.5 .3c-9.2 .9-17.8 3.8-25.5 8.2C21.8 46.5 13.4 55.1 7.7 65.5c-3.9 7.3-6.5 15.4-7.4 24zM48 224H464l0 192c0 8.8-7.2 16-16 16L64 432c-8.8 0-16-7.2-16-16l0-192z"
frontend/src/lib/icons/floppy.svelte CHANGED
@@ -1,8 +1,13 @@
1
  <script lang="ts">
2
- export let classList: string;
3
  </script>
4
 
5
- <svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512" class={classList}>
 
 
 
 
 
6
  <path
7
  fill="currentColor"
8
  d="M48 96v320a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V170.5a16 16 0 0 0-4.7-11.3l33.9-33.9a64 64 0 0 1 18.7 45.3V416a64 64 0 0 1-64 64H64a64 64 0 0 1-64-64V96a64 64 0 0 1 64-64h245.5a64 64 0 0 1 45.3 18.7l74.5 74.5-33.9 33.9-74.6-74.4-.8-.8V184a24 24 0 0 1-24 24H104a24 24 0 0 1-24-24V80H64a16 16 0 0 0-16 16zm80-16v80h144V80H128zm32 240a64 64 0 1 1 128 0 64 64 0 1 1-128 0z"
 
1
  <script lang="ts">
2
+ let props = $props();
3
  </script>
4
 
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ height="1em"
8
+ viewBox="0 0 448 512"
9
+ {...props}
10
+ >
11
  <path
12
  fill="currentColor"
13
  d="M48 96v320a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V170.5a16 16 0 0 0-4.7-11.3l33.9-33.9a64 64 0 0 1 18.7 45.3V416a64 64 0 0 1-64 64H64a64 64 0 0 1-64-64V96a64 64 0 0 1 64-64h245.5a64 64 0 0 1 45.3 18.7l74.5 74.5-33.9 33.9-74.6-74.4-.8-.8V184a24 24 0 0 1-24 24H104a24 24 0 0 1-24-24V80H64a16 16 0 0 0-16 16zm80-16v80h144V80H128zm32 240a64 64 0 1 1 128 0 64 64 0 1 1-128 0z"
frontend/src/lib/icons/screen.svelte CHANGED
@@ -1,8 +1,13 @@
1
  <script lang="ts">
2
- export let classList: string = '';
3
  </script>
4
 
5
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -32 576 576" height="16px" class={classList}>
 
 
 
 
 
6
  <path
7
  fill="currentColor"
8
  d="M64 0A64 64 0 0 0 0 64v288a64 64 0 0 0 64 64h176l-10.7 32H160a32 32 0 1 0 0 64h256a32 32 0 1 0 0-64h-69.3L336 416h176a64 64 0 0 0 64-64V64a64 64 0 0 0-64-64H64zm448 64v288H64V64h448z"
 
1
  <script lang="ts">
2
+ let props = $props();
3
  </script>
4
 
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ viewBox="0 -32 576 576"
8
+ height="16px"
9
+ {...props}
10
+ >
11
  <path
12
  fill="currentColor"
13
  d="M64 0A64 64 0 0 0 0 64v288a64 64 0 0 0 64 64h176l-10.7 32H160a32 32 0 1 0 0 64h256a32 32 0 1 0 0-64h-69.3L336 416h176a64 64 0 0 0 64-64V64a64 64 0 0 0-64-64H64zm448 64v288H64V64h448z"
frontend/src/lib/icons/spinner.svelte CHANGED
@@ -1,8 +1,13 @@
1
  <script lang="ts">
2
- export let classList: string;
3
  </script>
4
 
5
- <svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512" class={classList}>
 
 
 
 
 
6
  <path
7
  fill="currentColor"
8
  d="M304 48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zm0 416a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM48 304a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm464-48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM142.9 437A48 48 0 1 0 75 369.1 48 48 0 1 0 142.9 437zm0-294.2A48 48 0 1 0 75 75a48 48 0 1 0 67.9 67.9zM369.1 437A48 48 0 1 0 437 369.1 48 48 0 1 0 369.1 437z"
 
1
  <script lang="ts">
2
+ let props = $props();
3
  </script>
4
 
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ height="1em"
8
+ viewBox="0 0 512 512"
9
+ {...props}
10
+ >
11
  <path
12
  fill="currentColor"
13
  d="M304 48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zm0 416a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM48 304a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm464-48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM142.9 437A48 48 0 1 0 75 369.1 48 48 0 1 0 142.9 437zm0-294.2A48 48 0 1 0 75 75a48 48 0 1 0 67.9 67.9zM369.1 437A48 48 0 1 0 437 369.1 48 48 0 1 0 369.1 437z"
frontend/src/lib/lcmLive.ts CHANGED
@@ -1,13 +1,13 @@
1
- import { get, writable } from 'svelte/store';
2
 
3
  export enum LCMLiveStatus {
4
- CONNECTED = 'connected',
5
- DISCONNECTED = 'disconnected',
6
- CONNECTING = 'connecting',
7
- WAIT = 'wait',
8
- SEND_FRAME = 'send_frame',
9
- TIMEOUT = 'timeout',
10
- ERROR = 'error'
11
  }
12
 
13
  const initStatus: LCMLiveStatus = LCMLiveStatus.DISCONNECTED;
@@ -16,23 +16,23 @@ export const lcmLiveStatus = writable<LCMLiveStatus>(initStatus);
16
  export const streamId = writable<string | null>(null);
17
 
18
  // WebSocket connection
19
- let websocket: WebSocket | null = null;
20
- // Flag to track intentional connection closure
21
- let intentionalClosure = false;
22
 
23
  // Register browser unload event listener to properly close WebSockets
24
- if (typeof window !== 'undefined') {
25
- window.addEventListener('beforeunload', () => {
26
- // Mark any closure during page unload as intentional
27
- intentionalClosure = true;
28
  // Close the WebSocket properly if it exists
29
  if (websocket && websocket.readyState === WebSocket.OPEN) {
30
- websocket.close(1000, 'Page unload');
31
  }
32
  });
33
  }
34
  export const lcmLiveActions = {
35
- async start(getSreamdata: () => any[]) {
 
 
 
 
36
  return new Promise((resolve, reject) => {
37
  try {
38
  // Set connecting status immediately
@@ -40,7 +40,7 @@ export const lcmLiveActions = {
40
 
41
  const userId = crypto.randomUUID();
42
  const websocketURL = `${
43
- window.location.protocol === 'https:' ? 'wss' : 'ws'
44
  }:${window.location.host}/api/ws/${userId}`;
45
 
46
  // Close any existing connection first
@@ -53,22 +53,24 @@ export const lcmLiveActions = {
53
  // Set a connection timeout
54
  const connectionTimeout = setTimeout(() => {
55
  if (websocket && websocket.readyState !== WebSocket.OPEN) {
56
- console.error('WebSocket connection timeout');
57
  lcmLiveStatus.set(LCMLiveStatus.ERROR);
58
  streamId.set(null);
59
- reject(new Error('Connection timeout. Please try again.'));
60
  websocket.close();
61
  }
62
  }, 10000); // 10 second timeout
63
 
64
  websocket.onopen = () => {
65
  clearTimeout(connectionTimeout);
66
- console.log('Connected to websocket');
67
  };
68
 
69
  websocket.onclose = (event) => {
70
  clearTimeout(connectionTimeout);
71
- console.log(`Disconnected from websocket: ${event.code} ${event.reason}`);
 
 
72
 
73
  // Only change status if we're not in ERROR state (which would mean we already handled the error)
74
  if (get(lcmLiveStatus) !== LCMLiveStatus.ERROR) {
@@ -77,71 +79,73 @@ export const lcmLiveActions = {
77
 
78
  // If connection was never established (close without open)
79
  if (event.code === 1006 && get(streamId) === null) {
80
- reject(new Error('Cannot connect to server. Please try again later.'));
 
 
81
  }
82
  };
83
 
84
  websocket.onerror = (err) => {
85
  clearTimeout(connectionTimeout);
86
- console.error('WebSocket error:', err);
87
  lcmLiveStatus.set(LCMLiveStatus.ERROR);
88
  streamId.set(null);
89
- reject(new Error('Connection error. Please try again.'));
90
  };
91
 
92
  websocket.onmessage = (event) => {
93
  try {
94
  const data = JSON.parse(event.data);
95
  switch (data.status) {
96
- case 'connected':
97
  lcmLiveStatus.set(LCMLiveStatus.CONNECTED);
98
  streamId.set(userId);
99
- resolve({ status: 'connected', userId });
100
  break;
101
- case 'send_frame':
102
  lcmLiveStatus.set(LCMLiveStatus.SEND_FRAME);
103
  try {
104
  const streamData = getSreamdata();
105
  // Send as an object, not a string, to use the proper handling in the send method
106
- this.send({ status: 'next_frame' });
107
  for (const d of streamData) {
108
  this.send(d);
109
  }
110
  } catch (error) {
111
- console.error('Error sending frame data:', error);
112
  }
113
  break;
114
- case 'wait':
115
  lcmLiveStatus.set(LCMLiveStatus.WAIT);
116
  break;
117
- case 'timeout':
118
- console.log('Session timeout');
119
  lcmLiveStatus.set(LCMLiveStatus.TIMEOUT);
120
  streamId.set(null);
121
- reject(new Error('Session timeout. Please restart.'));
122
  break;
123
- case 'error':
124
- console.error('Server error:', data.message);
125
  lcmLiveStatus.set(LCMLiveStatus.ERROR);
126
  streamId.set(null);
127
- reject(new Error(data.message || 'Server error occurred'));
128
  break;
129
  default:
130
- console.log('Unknown message status:', data.status);
131
  }
132
  } catch (error) {
133
- console.error('Error handling websocket message:', error);
134
  }
135
  };
136
  } catch (err) {
137
- console.error('Error initializing websocket:', err);
138
  lcmLiveStatus.set(LCMLiveStatus.ERROR);
139
  streamId.set(null);
140
  reject(err);
141
  }
142
  });
143
  },
144
- send(data: Blob | { [key: string]: any }) {
145
  try {
146
  if (websocket && websocket.readyState === WebSocket.OPEN) {
147
  if (data instanceof Blob) {
@@ -151,8 +155,8 @@ export const lcmLiveActions = {
151
  }
152
  } else {
153
  const readyStateText = websocket
154
- ? ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'][websocket.readyState]
155
- : 'null';
156
  console.warn(`WebSocket not ready for sending: ${readyStateText}`);
157
 
158
  // If WebSocket is closed unexpectedly, set status to disconnected
@@ -162,20 +166,24 @@ export const lcmLiveActions = {
162
  }
163
  }
164
  } catch (error) {
165
- console.error('Error sending data through WebSocket:', error);
166
  // Handle WebSocket error by forcing disconnection
167
  this.stop();
168
  }
169
  },
170
 
171
- async reconnect(getSreamdata: () => any[]) {
 
 
 
 
172
  try {
173
  await this.stop();
174
  // Small delay to ensure clean disconnection before reconnecting
175
  await new Promise((resolve) => setTimeout(resolve, 500));
176
  return await this.start(getSreamdata);
177
  } catch (error) {
178
- console.error('Reconnection failed:', error);
179
  throw error;
180
  }
181
  },
@@ -188,21 +196,21 @@ export const lcmLiveActions = {
188
  if (websocket.readyState !== WebSocket.CLOSED) {
189
  // Set up onclose handler to clean up only
190
  websocket.onclose = () => {
191
- console.log('WebSocket closed cleanly during stop()');
192
  };
193
 
194
  // Set up onerror to be silent during intentional closure
195
  websocket.onerror = () => {};
196
 
197
- websocket.close(1000, 'Client initiated disconnect');
198
  }
199
  }
200
  } catch (error) {
201
- console.error('Error during WebSocket closure:', error);
202
  } finally {
203
  // Always clean up references
204
  websocket = null;
205
  streamId.set(null);
206
  }
207
- }
208
  };
 
1
+ import { get, writable } from "svelte/store";
2
 
3
  export enum LCMLiveStatus {
4
+ CONNECTED = "connected",
5
+ DISCONNECTED = "disconnected",
6
+ CONNECTING = "connecting",
7
+ WAIT = "wait",
8
+ SEND_FRAME = "send_frame",
9
+ TIMEOUT = "timeout",
10
+ ERROR = "error",
11
  }
12
 
13
  const initStatus: LCMLiveStatus = LCMLiveStatus.DISCONNECTED;
 
16
  export const streamId = writable<string | null>(null);
17
 
18
  // WebSocket connection
19
+ let websocket: WebSocket;
 
 
20
 
21
  // Register browser unload event listener to properly close WebSockets
22
+ if (typeof window !== "undefined") {
23
+ window.addEventListener("beforeunload", () => {
 
 
24
  // Close the WebSocket properly if it exists
25
  if (websocket && websocket.readyState === WebSocket.OPEN) {
26
+ websocket.close(1000, "Page unload");
27
  }
28
  });
29
  }
30
  export const lcmLiveActions = {
31
+ async start(
32
+ getSreamdata: () =>
33
+ | [Record<string, unknown>]
34
+ | [Record<string, unknown>, Blob],
35
+ ) {
36
  return new Promise((resolve, reject) => {
37
  try {
38
  // Set connecting status immediately
 
40
 
41
  const userId = crypto.randomUUID();
42
  const websocketURL = `${
43
+ window.location.protocol === "https:" ? "wss" : "ws"
44
  }:${window.location.host}/api/ws/${userId}`;
45
 
46
  // Close any existing connection first
 
53
  // Set a connection timeout
54
  const connectionTimeout = setTimeout(() => {
55
  if (websocket && websocket.readyState !== WebSocket.OPEN) {
56
+ console.error("WebSocket connection timeout");
57
  lcmLiveStatus.set(LCMLiveStatus.ERROR);
58
  streamId.set(null);
59
+ reject(new Error("Connection timeout. Please try again."));
60
  websocket.close();
61
  }
62
  }, 10000); // 10 second timeout
63
 
64
  websocket.onopen = () => {
65
  clearTimeout(connectionTimeout);
66
+ console.log("Connected to websocket");
67
  };
68
 
69
  websocket.onclose = (event) => {
70
  clearTimeout(connectionTimeout);
71
+ console.log(
72
+ `Disconnected from websocket: ${event.code} ${event.reason}`,
73
+ );
74
 
75
  // Only change status if we're not in ERROR state (which would mean we already handled the error)
76
  if (get(lcmLiveStatus) !== LCMLiveStatus.ERROR) {
 
79
 
80
  // If connection was never established (close without open)
81
  if (event.code === 1006 && get(streamId) === null) {
82
+ reject(
83
+ new Error("Cannot connect to server. Please try again later."),
84
+ );
85
  }
86
  };
87
 
88
  websocket.onerror = (err) => {
89
  clearTimeout(connectionTimeout);
90
+ console.error("WebSocket error:", err);
91
  lcmLiveStatus.set(LCMLiveStatus.ERROR);
92
  streamId.set(null);
93
+ reject(new Error("Connection error. Please try again."));
94
  };
95
 
96
  websocket.onmessage = (event) => {
97
  try {
98
  const data = JSON.parse(event.data);
99
  switch (data.status) {
100
+ case "connected":
101
  lcmLiveStatus.set(LCMLiveStatus.CONNECTED);
102
  streamId.set(userId);
103
+ resolve({ status: "connected", userId });
104
  break;
105
+ case "send_frame":
106
  lcmLiveStatus.set(LCMLiveStatus.SEND_FRAME);
107
  try {
108
  const streamData = getSreamdata();
109
  // Send as an object, not a string, to use the proper handling in the send method
110
+ this.send({ status: "next_frame" });
111
  for (const d of streamData) {
112
  this.send(d);
113
  }
114
  } catch (error) {
115
+ console.error("Error sending frame data:", error);
116
  }
117
  break;
118
+ case "wait":
119
  lcmLiveStatus.set(LCMLiveStatus.WAIT);
120
  break;
121
+ case "timeout":
122
+ console.log("Session timeout");
123
  lcmLiveStatus.set(LCMLiveStatus.TIMEOUT);
124
  streamId.set(null);
125
+ reject(new Error("Session timeout. Please restart."));
126
  break;
127
+ case "error":
128
+ console.error("Server error:", data.message);
129
  lcmLiveStatus.set(LCMLiveStatus.ERROR);
130
  streamId.set(null);
131
+ reject(new Error(data.message || "Server error occurred"));
132
  break;
133
  default:
134
+ console.log("Unknown message status:", data.status);
135
  }
136
  } catch (error) {
137
+ console.error("Error handling websocket message:", error);
138
  }
139
  };
140
  } catch (err) {
141
+ console.error("Error initializing websocket:", err);
142
  lcmLiveStatus.set(LCMLiveStatus.ERROR);
143
  streamId.set(null);
144
  reject(err);
145
  }
146
  });
147
  },
148
+ send(data: Blob | Record<string, unknown>) {
149
  try {
150
  if (websocket && websocket.readyState === WebSocket.OPEN) {
151
  if (data instanceof Blob) {
 
155
  }
156
  } else {
157
  const readyStateText = websocket
158
+ ? ["CONNECTING", "OPEN", "CLOSING", "CLOSED"][websocket.readyState]
159
+ : "null";
160
  console.warn(`WebSocket not ready for sending: ${readyStateText}`);
161
 
162
  // If WebSocket is closed unexpectedly, set status to disconnected
 
166
  }
167
  }
168
  } catch (error) {
169
+ console.error("Error sending data through WebSocket:", error);
170
  // Handle WebSocket error by forcing disconnection
171
  this.stop();
172
  }
173
  },
174
 
175
+ async reconnect(
176
+ getSreamdata: () =>
177
+ | [Record<string, unknown>]
178
+ | [Record<string, unknown>, Blob],
179
+ ) {
180
  try {
181
  await this.stop();
182
  // Small delay to ensure clean disconnection before reconnecting
183
  await new Promise((resolve) => setTimeout(resolve, 500));
184
  return await this.start(getSreamdata);
185
  } catch (error) {
186
+ console.error("Reconnection failed:", error);
187
  throw error;
188
  }
189
  },
 
196
  if (websocket.readyState !== WebSocket.CLOSED) {
197
  // Set up onclose handler to clean up only
198
  websocket.onclose = () => {
199
+ console.log("WebSocket closed cleanly during stop()");
200
  };
201
 
202
  // Set up onerror to be silent during intentional closure
203
  websocket.onerror = () => {};
204
 
205
+ websocket.close(1000, "Client initiated disconnect");
206
  }
207
  }
208
  } catch (error) {
209
+ console.error("Error during WebSocket closure:", error);
210
  } finally {
211
  // Always clean up references
212
  websocket = null;
213
  streamId.set(null);
214
  }
215
+ },
216
  };
frontend/src/lib/mediaStream.ts CHANGED
@@ -1,12 +1,14 @@
1
- import { get, writable, type Writable } from 'svelte/store';
2
 
3
  const BASE_HEIGHT = 720;
4
  export enum MediaStreamStatusEnum {
5
- INIT = 'init',
6
- CONNECTED = 'connected',
7
- DISCONNECTED = 'disconnected'
8
  }
9
- export const onFrameChangeStore: Writable<{ blob: Blob }> = writable({ blob: new Blob() });
 
 
10
 
11
  export const mediaDevices = writable<MediaDeviceInfo[]>([]);
12
  export const mediaStreamStatus = writable(MediaStreamStatusEnum.INIT);
@@ -18,7 +20,9 @@ export const mediaStreamActions = {
18
  await navigator.mediaDevices
19
  .enumerateDevices()
20
  .then((devices) => {
21
- const cameras = devices.filter((device) => device.kind === 'videoinput');
 
 
22
  mediaDevices.set(cameras);
23
  })
24
  .catch((err) => {
@@ -30,13 +34,13 @@ export const mediaStreamActions = {
30
  audio: false,
31
  video: {
32
  width: {
33
- ideal: BASE_HEIGHT * aspectRatio
34
  },
35
  height: {
36
- ideal: BASE_HEIGHT
37
  },
38
- deviceId: mediaDevicedID
39
- }
40
  };
41
 
42
  await navigator.mediaDevices
@@ -54,34 +58,35 @@ export const mediaStreamActions = {
54
  async startScreenCapture() {
55
  const displayMediaOptions = {
56
  video: {
57
- displaySurface: 'window'
58
  },
59
  audio: false,
60
- surfaceSwitching: 'include'
61
  };
62
 
63
  let captureStream = null;
64
 
65
  try {
66
- captureStream = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
 
67
  const videoTrack = captureStream.getVideoTracks()[0];
68
 
69
- console.log('Track settings:');
70
  console.log(JSON.stringify(videoTrack.getSettings(), null, 2));
71
- console.log('Track constraints:');
72
  console.log(JSON.stringify(videoTrack.getConstraints(), null, 2));
73
  mediaStreamStatus.set(MediaStreamStatusEnum.CONNECTED);
74
  mediaStream.set(captureStream);
75
 
76
  const capabilities = videoTrack.getCapabilities();
77
  const aspectRatio = capabilities.aspectRatio;
78
- console.log('Aspect Ratio Constraints:', aspectRatio);
79
  } catch (err) {
80
  console.error(err);
81
  }
82
  },
83
  async switchCamera(mediaDevicedID: string, aspectRatio: number) {
84
- console.log('Switching camera');
85
  if (get(mediaStreamStatus) !== MediaStreamStatusEnum.CONNECTED) {
86
  return;
87
  }
@@ -89,15 +94,15 @@ export const mediaStreamActions = {
89
  audio: false,
90
  video: {
91
  width: {
92
- ideal: BASE_HEIGHT * aspectRatio
93
  },
94
  height: {
95
- ideal: BASE_HEIGHT
96
  },
97
- deviceId: mediaDevicedID
98
- }
99
  };
100
- console.log('Switching camera', constraints);
101
  await navigator.mediaDevices
102
  .getUserMedia(constraints)
103
  .then((stream) => {
@@ -114,5 +119,5 @@ export const mediaStreamActions = {
114
  });
115
  mediaStreamStatus.set(MediaStreamStatusEnum.DISCONNECTED);
116
  mediaStream.set(null);
117
- }
118
  };
 
1
+ import { get, writable, type Writable } from "svelte/store";
2
 
3
  const BASE_HEIGHT = 720;
4
  export enum MediaStreamStatusEnum {
5
+ INIT = "init",
6
+ CONNECTED = "connected",
7
+ DISCONNECTED = "disconnected",
8
  }
9
+ export const onFrameChangeStore: Writable<{ blob: Blob }> = writable({
10
+ blob: new Blob(),
11
+ });
12
 
13
  export const mediaDevices = writable<MediaDeviceInfo[]>([]);
14
  export const mediaStreamStatus = writable(MediaStreamStatusEnum.INIT);
 
20
  await navigator.mediaDevices
21
  .enumerateDevices()
22
  .then((devices) => {
23
+ const cameras = devices.filter(
24
+ (device) => device.kind === "videoinput",
25
+ );
26
  mediaDevices.set(cameras);
27
  })
28
  .catch((err) => {
 
34
  audio: false,
35
  video: {
36
  width: {
37
+ ideal: BASE_HEIGHT * aspectRatio,
38
  },
39
  height: {
40
+ ideal: BASE_HEIGHT,
41
  },
42
+ deviceId: mediaDevicedID,
43
+ },
44
  };
45
 
46
  await navigator.mediaDevices
 
58
  async startScreenCapture() {
59
  const displayMediaOptions = {
60
  video: {
61
+ displaySurface: "window",
62
  },
63
  audio: false,
64
+ surfaceSwitching: "include",
65
  };
66
 
67
  let captureStream = null;
68
 
69
  try {
70
+ captureStream =
71
+ await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
72
  const videoTrack = captureStream.getVideoTracks()[0];
73
 
74
+ console.log("Track settings:");
75
  console.log(JSON.stringify(videoTrack.getSettings(), null, 2));
76
+ console.log("Track constraints:");
77
  console.log(JSON.stringify(videoTrack.getConstraints(), null, 2));
78
  mediaStreamStatus.set(MediaStreamStatusEnum.CONNECTED);
79
  mediaStream.set(captureStream);
80
 
81
  const capabilities = videoTrack.getCapabilities();
82
  const aspectRatio = capabilities.aspectRatio;
83
+ console.log("Aspect Ratio Constraints:", aspectRatio);
84
  } catch (err) {
85
  console.error(err);
86
  }
87
  },
88
  async switchCamera(mediaDevicedID: string, aspectRatio: number) {
89
+ console.log("Switching camera");
90
  if (get(mediaStreamStatus) !== MediaStreamStatusEnum.CONNECTED) {
91
  return;
92
  }
 
94
  audio: false,
95
  video: {
96
  width: {
97
+ ideal: BASE_HEIGHT * aspectRatio,
98
  },
99
  height: {
100
+ ideal: BASE_HEIGHT,
101
  },
102
+ deviceId: mediaDevicedID,
103
+ },
104
  };
105
+ console.log("Switching camera", constraints);
106
  await navigator.mediaDevices
107
  .getUserMedia(constraints)
108
  .then((stream) => {
 
119
  });
120
  mediaStreamStatus.set(MediaStreamStatusEnum.DISCONNECTED);
121
  mediaStream.set(null);
122
+ },
123
  };
frontend/src/lib/store.ts CHANGED
@@ -1,14 +1,22 @@
1
- import { derived, get, writable, type Readable, type Writable } from 'svelte/store';
 
 
 
 
 
 
2
 
3
- export const pipelineValues: Writable<Record<string, any>> = writable({});
4
- export const deboucedPipelineValues: Readable<Record<string, any>> = derived(
 
 
5
  pipelineValues,
6
  ($pipelineValues, set) => {
7
  const debounced = setTimeout(() => {
8
  set($pipelineValues);
9
  }, 100);
10
  return () => clearTimeout(debounced);
11
- }
12
  );
13
 
14
  export const getPipelineValues = () => get(pipelineValues);
 
1
+ import {
2
+ derived,
3
+ get,
4
+ writable,
5
+ type Readable,
6
+ type Writable,
7
+ } from "svelte/store";
8
 
9
+ export type PipelineValues = Record<string, string | boolean | number>;
10
+
11
+ export const pipelineValues: Writable<PipelineValues> = writable({});
12
+ export const deboucedPipelineValues: Readable<PipelineValues> = derived(
13
  pipelineValues,
14
  ($pipelineValues, set) => {
15
  const debounced = setTimeout(() => {
16
  set($pipelineValues);
17
  }, 100);
18
  return () => clearTimeout(debounced);
19
+ },
20
  );
21
 
22
  export const getPipelineValues = () => get(pipelineValues);
frontend/src/lib/types.ts CHANGED
@@ -1,14 +1,14 @@
1
  export const enum FieldType {
2
- RANGE = 'range',
3
- SEED = 'seed',
4
- TEXTAREA = 'textarea',
5
- CHECKBOX = 'checkbox',
6
- SELECT = 'select'
7
  }
8
  export const enum PipelineMode {
9
- IMAGE = 'image',
10
- VIDEO = 'video',
11
- TEXT = 'text'
12
  }
13
 
14
  export interface Fields {
 
1
  export const enum FieldType {
2
+ RANGE = "range",
3
+ SEED = "seed",
4
+ TEXTAREA = "textarea",
5
+ CHECKBOX = "checkbox",
6
+ SELECT = "select",
7
  }
8
  export const enum PipelineMode {
9
+ IMAGE = "image",
10
+ VIDEO = "video",
11
+ TEXT = "text",
12
  }
13
 
14
  export interface Fields {
frontend/src/lib/utils.ts CHANGED
@@ -1,40 +1,40 @@
1
- import * as piexif from 'piexifjs';
2
 
3
- interface IImageInfo {
4
  prompt?: string;
5
  negative_prompt?: string;
6
  seed?: number;
7
  guidance_scale?: number;
8
  }
 
9
  export enum windowType {
10
- image = 'image',
11
- video = 'video'
12
  }
13
 
14
  export function snapImage(imageEl: HTMLImageElement, info: IImageInfo) {
15
  try {
16
- const zeroth: { [key: string]: any } = {};
17
- const exif: { [key: string]: any } = {};
18
- const gps: { [key: string]: any } = {};
19
- zeroth[piexif.ImageIFD.Make] = 'LCM Image-to-Image ControNet';
20
  zeroth[piexif.ImageIFD.ImageDescription] =
21
  `prompt: ${info?.prompt} | negative_prompt: ${info?.negative_prompt} | seed: ${info?.seed} | guidance_scale: ${info?.guidance_scale}`;
22
  zeroth[piexif.ImageIFD.Software] =
23
- 'https://github.com/radames/Real-Time-Latent-Consistency-Model';
24
  exif[piexif.ExifIFD.DateTimeOriginal] = new Date().toISOString();
25
 
26
- const exifObj = { '0th': zeroth, Exif: exif, GPS: gps };
27
  const exifBytes = piexif.dump(exifObj);
28
 
29
- const canvas = document.createElement('canvas');
30
  canvas.width = imageEl.naturalWidth;
31
  canvas.height = imageEl.naturalHeight;
32
- const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
33
  ctx.drawImage(imageEl, 0, 0);
34
- const dataURL = canvas.toDataURL('image/jpeg');
35
  const withExif = piexif.insert(exifBytes, dataURL);
36
 
37
- const a = document.createElement('a');
38
  a.href = withExif;
39
  a.download = `lcm_txt_2_img${Date.now()}.png`;
40
  a.click();
@@ -43,11 +43,11 @@ export function snapImage(imageEl: HTMLImageElement, info: IImageInfo) {
43
  }
44
  }
45
 
46
- export function expandWindow(streamURL: string, type: windowType = windowType.image) {
47
  const newWindow = window.open(
48
- '',
49
- '_blank',
50
- 'width=1024,height=1024,scrollbars=0,resizable=1,toolbar=0,menubar=0,location=0,directories=0,status=0'
51
  ) as Window;
52
 
53
  const html = `
@@ -87,11 +87,11 @@ export function expandWindow(streamURL: string, type: windowType = windowType.im
87
  `;
88
  newWindow.document.write(html);
89
 
90
- const img = newWindow.document.createElement('img');
91
  img.src = streamURL;
92
- img.style.width = '100%';
93
- img.style.height = '100%';
94
- img.style.objectFit = 'contain';
95
  newWindow.document.body.appendChild(img);
96
 
97
  return newWindow;
 
1
+ import * as piexif from "piexifjs";
2
 
3
+ export interface IImageInfo {
4
  prompt?: string;
5
  negative_prompt?: string;
6
  seed?: number;
7
  guidance_scale?: number;
8
  }
9
+
10
  export enum windowType {
11
+ image = "image",
 
12
  }
13
 
14
  export function snapImage(imageEl: HTMLImageElement, info: IImageInfo) {
15
  try {
16
+ const zeroth: { [key: string]: string | number } = {};
17
+ const exif: { [key: string]: string | number } = {};
18
+ const gps: { [key: string]: string | number } = {};
19
+ zeroth[piexif.ImageIFD.Make] = "LCM Image-to-Image ControNet";
20
  zeroth[piexif.ImageIFD.ImageDescription] =
21
  `prompt: ${info?.prompt} | negative_prompt: ${info?.negative_prompt} | seed: ${info?.seed} | guidance_scale: ${info?.guidance_scale}`;
22
  zeroth[piexif.ImageIFD.Software] =
23
+ "https://github.com/radames/Real-Time-Latent-Consistency-Model";
24
  exif[piexif.ExifIFD.DateTimeOriginal] = new Date().toISOString();
25
 
26
+ const exifObj = { "0th": zeroth, Exif: exif, GPS: gps };
27
  const exifBytes = piexif.dump(exifObj);
28
 
29
+ const canvas = document.createElement("canvas");
30
  canvas.width = imageEl.naturalWidth;
31
  canvas.height = imageEl.naturalHeight;
32
+ const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
33
  ctx.drawImage(imageEl, 0, 0);
34
+ const dataURL = canvas.toDataURL("image/jpeg");
35
  const withExif = piexif.insert(exifBytes, dataURL);
36
 
37
+ const a = document.createElement("a");
38
  a.href = withExif;
39
  a.download = `lcm_txt_2_img${Date.now()}.png`;
40
  a.click();
 
43
  }
44
  }
45
 
46
+ export function expandWindow(streamURL: string) {
47
  const newWindow = window.open(
48
+ "",
49
+ "_blank",
50
+ "width=1024,height=1024,scrollbars=0,resizable=1,toolbar=0,menubar=0,location=0,directories=0,status=0",
51
  ) as Window;
52
 
53
  const html = `
 
87
  `;
88
  newWindow.document.write(html);
89
 
90
+ const img = newWindow.document.createElement("img");
91
  img.src = streamURL;
92
+ img.style.width = "100%";
93
+ img.style.height = "100%";
94
+ img.style.objectFit = "contain";
95
  newWindow.document.body.appendChild(img);
96
 
97
  return newWindow;
frontend/src/piexifjs.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- declare module 'piexifjs' {
2
  export const ImageIFD: {
3
  Make: number;
4
  ImageDescription: number;
@@ -7,6 +7,6 @@ declare module 'piexifjs' {
7
  export const ExifIFD: {
8
  DateTimeOriginal: number;
9
  };
10
- export function dump(exifObj: any): any;
11
- export function insert(exifBytes: any, dataURL: string): string;
12
  }
 
1
+ declare module "piexifjs" {
2
  export const ImageIFD: {
3
  Make: number;
4
  ImageDescription: number;
 
7
  export const ExifIFD: {
8
  DateTimeOriginal: number;
9
  };
10
+ export function dump(exifObj: Record<string, unknown>): string;
11
+ export function insert(exifBytes: string, dataURL: string): string;
12
  }
frontend/src/routes/+layout.svelte CHANGED
@@ -1,5 +1,7 @@
1
- <script>
2
- import '../app.css';
 
 
3
  </script>
4
 
5
- <slot />
 
1
+ <script lang="ts">
2
+ import "../app.css";
3
+
4
+ let { children } = $props();
5
  </script>
6
 
7
+ {@render children()}
frontend/src/routes/+page.svelte CHANGED
@@ -1,38 +1,40 @@
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
3
- import type { Fields, PipelineInfo } from '$lib/types';
4
- import { PipelineMode } from '$lib/types';
5
- import ImagePlayer from '$lib/components/ImagePlayer.svelte';
6
- import VideoInput from '$lib/components/VideoInput.svelte';
7
- import Button from '$lib/components/Button.svelte';
8
- import PipelineOptions from '$lib/components/PipelineOptions.svelte';
9
- import Spinner from '$lib/icons/spinner.svelte';
10
- import Warning from '$lib/components/Warning.svelte';
11
- import { lcmLiveStatus, lcmLiveActions, LCMLiveStatus } from '$lib/lcmLive';
12
- import { mediaStreamActions, onFrameChangeStore } from '$lib/mediaStream';
13
- import { getPipelineValues, deboucedPipelineValues } from '$lib/store';
14
-
15
- let pipelineParams: Fields;
16
- let pipelineInfo: PipelineInfo;
17
- let pageContent: string;
18
- let isImageMode: boolean = false;
19
- let maxQueueSize: number = 0;
20
- let currentQueueSize: number = 0;
21
- let queueCheckerRunning: boolean = false;
22
- let warningMessage: string = '';
 
 
23
  onMount(() => {
24
  getSettings();
25
  });
26
 
27
  async function getSettings() {
28
- const settings = await fetch('/api/settings').then((r) => r.json());
29
  pipelineParams = settings.input_params.properties;
30
  pipelineInfo = settings.info.properties;
31
- isImageMode = pipelineInfo.input_mode.default === PipelineMode.IMAGE;
32
  maxQueueSize = settings.max_queue_size;
33
  pageContent = settings.page_content;
34
- console.log(pipelineParams);
35
  toggleQueueChecker(true);
 
36
  }
37
  function toggleQueueChecker(start: boolean) {
38
  queueCheckerRunning = start && maxQueueSize > 0;
@@ -44,12 +46,13 @@
44
  if (!queueCheckerRunning) {
45
  return;
46
  }
47
- const data = await fetch('/api/queue').then((r) => r.json());
48
  currentQueueSize = data.queue_size;
49
  setTimeout(getQueueSize, 10000);
50
  }
51
-
52
- function getSreamdata() {
 
53
  if (isImageMode) {
54
  return [getPipelineValues(), $onFrameChangeStore?.blob];
55
  } else {
@@ -57,19 +60,20 @@
57
  }
58
  }
59
 
60
- $: isLCMRunning =
61
- $lcmLiveStatus !== LCMLiveStatus.DISCONNECTED && $lcmLiveStatus !== LCMLiveStatus.ERROR;
62
- $: isConnecting = $lcmLiveStatus === LCMLiveStatus.CONNECTING;
 
 
63
 
64
- $: {
65
  // Set warning messages based on lcmLiveStatus
66
  if ($lcmLiveStatus === LCMLiveStatus.TIMEOUT) {
67
- warningMessage = 'Session timed out. Please try again.';
68
  } else if ($lcmLiveStatus === LCMLiveStatus.ERROR) {
69
- warningMessage = 'Connection error occurred. Please try again.';
70
  }
71
- }
72
- let disabled = false;
73
  async function toggleLcmLive() {
74
  try {
75
  if (!isLCMRunning) {
@@ -78,7 +82,7 @@
78
  }
79
 
80
  // Clear any previous warning messages
81
- warningMessage = '';
82
  disabled = true;
83
 
84
  try {
@@ -108,8 +112,9 @@
108
  }
109
  }
110
  } catch (e) {
111
- console.error('Error in toggleLcmLive:', e);
112
- warningMessage = e instanceof Error ? e.message : 'An unknown error occurred';
 
113
  disabled = false;
114
  toggleQueueChecker(true);
115
  }
@@ -119,7 +124,7 @@
119
  async function reconnect() {
120
  try {
121
  disabled = true;
122
- warningMessage = 'Reconnecting...';
123
 
124
  if (isImageMode) {
125
  await mediaStreamActions.stop();
@@ -128,10 +133,10 @@
128
  }
129
 
130
  await lcmLiveActions.reconnect(getSreamdata);
131
- warningMessage = '';
132
  toggleQueueChecker(false);
133
  } catch (e) {
134
- warningMessage = e instanceof Error ? e.message : 'Reconnection failed';
135
  toggleQueueChecker(true);
136
  } finally {
137
  disabled = false;
@@ -149,12 +154,16 @@
149
  <Warning bind:message={warningMessage}></Warning>
150
  <article class="text-center">
151
  {#if pageContent}
 
152
  {@html pageContent}
153
  {/if}
154
  {#if maxQueueSize > 0}
155
  <p class="text-sm">
156
- There are <span id="queue_size" class="font-bold">{currentQueueSize}</span>
157
- user(s) sharing the same GPU, affecting real-time performance. Maximum queue size is {maxQueueSize}.
 
 
 
158
  <a
159
  href="https://huggingface.co/spaces/radames/Real-Time-Latent-Consistency-Model?duplicate=true"
160
  target="_blank"
@@ -165,7 +174,11 @@
165
 
166
  {#if $lcmLiveStatus === LCMLiveStatus.ERROR}
167
  <p class="mt-2 text-sm">
168
- <button class="text-blue-500 underline hover:no-underline" on:click={reconnect} {disabled}>
 
 
 
 
169
  Try reconnecting
170
  </button>
171
  </p>
@@ -181,11 +194,11 @@
181
  ></VideoInput>
182
  </div>
183
  {/if}
184
- <div class={isImageMode ? 'col-span-2 sm:col-start-3' : 'col-span-4'}>
185
  <ImagePlayer />
186
  </div>
187
  <div class="sm:col-span-4 sm:row-start-2">
188
- <Button on:click={toggleLcmLive} {disabled} classList={'text-lg my-1 p-2'}>
189
  {#if isConnecting}
190
  Connecting...
191
  {:else if isLCMRunning}
@@ -200,13 +213,14 @@
200
  {:else}
201
  <!-- loading -->
202
  <div class="flex items-center justify-center gap-3 py-48 text-2xl">
203
- <Spinner classList={'animate-spin opacity-50'}></Spinner>
204
  <p>Loading...</p>
205
  </div>
206
  {/if}
207
  </main>
208
 
209
  <style lang="postcss">
 
210
  :global(html) {
211
  @apply text-black dark:bg-gray-900 dark:text-white;
212
  }
 
1
  <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import type { Fields, PipelineInfo } from "$lib/types";
4
+ import { PipelineMode } from "$lib/types";
5
+ import ImagePlayer from "$lib/components/ImagePlayer.svelte";
6
+ import VideoInput from "$lib/components/VideoInput.svelte";
7
+ import Button from "$lib/components/Button.svelte";
8
+ import PipelineOptions from "$lib/components/PipelineOptions.svelte";
9
+ import Spinner from "$lib/icons/spinner.svelte";
10
+ import Warning from "$lib/components/Warning.svelte";
11
+ import { lcmLiveStatus, lcmLiveActions, LCMLiveStatus } from "$lib/lcmLive";
12
+ import { mediaStreamActions, onFrameChangeStore } from "$lib/mediaStream";
13
+ import { getPipelineValues, deboucedPipelineValues } from "$lib/store";
14
+
15
+ let pipelineParams: Fields | undefined = $state();
16
+ let pipelineInfo: PipelineInfo | undefined = $state();
17
+ let pageContent: string | undefined = $state();
18
+ let isImageMode: boolean = $state(false);
19
+ let maxQueueSize: number = $state(0);
20
+ let currentQueueSize: number = $state(0);
21
+ let disabled: boolean = $state(false);
22
+ let queueCheckerRunning: boolean = $state(false);
23
+ let warningMessage: string = $state("");
24
+
25
  onMount(() => {
26
  getSettings();
27
  });
28
 
29
  async function getSettings() {
30
+ const settings = await fetch("/api/settings").then((r) => r.json());
31
  pipelineParams = settings.input_params.properties;
32
  pipelineInfo = settings.info.properties;
33
+ isImageMode = pipelineInfo?.input_mode?.default === PipelineMode.IMAGE;
34
  maxQueueSize = settings.max_queue_size;
35
  pageContent = settings.page_content;
 
36
  toggleQueueChecker(true);
37
+ console.log(pipelineParams);
38
  }
39
  function toggleQueueChecker(start: boolean) {
40
  queueCheckerRunning = start && maxQueueSize > 0;
 
46
  if (!queueCheckerRunning) {
47
  return;
48
  }
49
+ const data = await fetch("/api/queue").then((r) => r.json());
50
  currentQueueSize = data.queue_size;
51
  setTimeout(getQueueSize, 10000);
52
  }
53
+ function getSreamdata():
54
+ | [Record<string, unknown>]
55
+ | [Record<string, unknown>, Blob] {
56
  if (isImageMode) {
57
  return [getPipelineValues(), $onFrameChangeStore?.blob];
58
  } else {
 
60
  }
61
  }
62
 
63
+ const isLCMRunning = $derived(
64
+ $lcmLiveStatus !== LCMLiveStatus.DISCONNECTED &&
65
+ $lcmLiveStatus !== LCMLiveStatus.ERROR,
66
+ );
67
+ const isConnecting = $derived($lcmLiveStatus === LCMLiveStatus.CONNECTING);
68
 
69
+ $effect(() => {
70
  // Set warning messages based on lcmLiveStatus
71
  if ($lcmLiveStatus === LCMLiveStatus.TIMEOUT) {
72
+ warningMessage = "Session timed out. Please try again.";
73
  } else if ($lcmLiveStatus === LCMLiveStatus.ERROR) {
74
+ warningMessage = "Connection error occurred. Please try again.";
75
  }
76
+ });
 
77
  async function toggleLcmLive() {
78
  try {
79
  if (!isLCMRunning) {
 
82
  }
83
 
84
  // Clear any previous warning messages
85
+ warningMessage = "";
86
  disabled = true;
87
 
88
  try {
 
112
  }
113
  }
114
  } catch (e) {
115
+ console.error("Error in toggleLcmLive:", e);
116
+ warningMessage =
117
+ e instanceof Error ? e.message : "An unknown error occurred";
118
  disabled = false;
119
  toggleQueueChecker(true);
120
  }
 
124
  async function reconnect() {
125
  try {
126
  disabled = true;
127
+ warningMessage = "Reconnecting...";
128
 
129
  if (isImageMode) {
130
  await mediaStreamActions.stop();
 
133
  }
134
 
135
  await lcmLiveActions.reconnect(getSreamdata);
136
+ warningMessage = "";
137
  toggleQueueChecker(false);
138
  } catch (e) {
139
+ warningMessage = e instanceof Error ? e.message : "Reconnection failed";
140
  toggleQueueChecker(true);
141
  } finally {
142
  disabled = false;
 
154
  <Warning bind:message={warningMessage}></Warning>
155
  <article class="text-center">
156
  {#if pageContent}
157
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
158
  {@html pageContent}
159
  {/if}
160
  {#if maxQueueSize > 0}
161
  <p class="text-sm">
162
+ There are <span id="queue_size" class="font-bold"
163
+ >{currentQueueSize}</span
164
+ >
165
+ user(s) sharing the same GPU, affecting real-time performance. Maximum queue
166
+ size is {maxQueueSize}.
167
  <a
168
  href="https://huggingface.co/spaces/radames/Real-Time-Latent-Consistency-Model?duplicate=true"
169
  target="_blank"
 
174
 
175
  {#if $lcmLiveStatus === LCMLiveStatus.ERROR}
176
  <p class="mt-2 text-sm">
177
+ <button
178
+ class="text-blue-500 underline hover:no-underline"
179
+ onclick={reconnect}
180
+ {disabled}
181
+ >
182
  Try reconnecting
183
  </button>
184
  </p>
 
194
  ></VideoInput>
195
  </div>
196
  {/if}
197
+ <div class={isImageMode ? "col-span-2 sm:col-start-3" : "col-span-4"}>
198
  <ImagePlayer />
199
  </div>
200
  <div class="sm:col-span-4 sm:row-start-2">
201
+ <Button onclick={toggleLcmLive} {disabled} class="my-1 p-2 text-lg">
202
  {#if isConnecting}
203
  Connecting...
204
  {:else if isLCMRunning}
 
213
  {:else}
214
  <!-- loading -->
215
  <div class="flex items-center justify-center gap-3 py-48 text-2xl">
216
+ <Spinner class="animate-spin opacity-50"></Spinner>
217
  <p>Loading...</p>
218
  </div>
219
  {/if}
220
  </main>
221
 
222
  <style lang="postcss">
223
+ @reference "tailwindcss";
224
  :global(html) {
225
  @apply text-black dark:bg-gray-900 dark:text-white;
226
  }
frontend/svelte.config.js CHANGED
@@ -1,18 +1,17 @@
1
- import adapter from '@sveltejs/adapter-static';
2
- import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3
 
4
- /** @type {import('@sveltejs/kit').Config} */
5
  const config = {
6
- preprocess: vitePreprocess({ postcss: true }),
7
  kit: {
8
  adapter: adapter({
9
- pages: 'public',
10
- assets: 'public',
11
  fallback: undefined,
12
  precompress: false,
13
- strict: true
14
- })
15
- }
16
  };
17
 
18
  export default config;
 
1
+ import adapter from "@sveltejs/adapter-static";
2
+ import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
3
 
 
4
  const config = {
5
+ preprocess: vitePreprocess(),
6
  kit: {
7
  adapter: adapter({
8
+ pages: "public",
9
+ assets: "public",
10
  fallback: undefined,
11
  precompress: false,
12
+ strict: true,
13
+ }),
14
+ },
15
  };
16
 
17
  export default config;
frontend/tailwind.config.js CHANGED
@@ -1,8 +1,8 @@
1
  /** @type {import('tailwindcss').Config} */
2
  export default {
3
- content: ['./src/**/*.{html,js,svelte,ts}', '../pipelines/**/*.py'],
4
  theme: {
5
- extend: {}
6
  },
7
- plugins: []
8
  };
 
1
  /** @type {import('tailwindcss').Config} */
2
  export default {
3
+ content: ["./src/**/*.{html,js,svelte,ts}"],
4
  theme: {
5
+ extend: {},
6
  },
7
+ plugins: [import("@tailwindcss/typography")],
8
  };
frontend/tsconfig.json CHANGED
@@ -1,17 +1,19 @@
1
  {
2
- "extends": "./.svelte-kit/tsconfig.json",
3
- "compilerOptions": {
4
- "allowJs": true,
5
- "checkJs": true,
6
- "esModuleInterop": true,
7
- "forceConsistentCasingInFileNames": true,
8
- "resolveJsonModule": true,
9
- "skipLibCheck": true,
10
- "sourceMap": true,
11
- "strict": true
12
- }
13
- // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
14
- //
15
- // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
16
- // from the referenced tsconfig.json - TypeScript does not merge them in
 
 
17
  }
 
1
  {
2
+ "extends": "./.svelte-kit/tsconfig.json",
3
+ "compilerOptions": {
4
+ "allowJs": true,
5
+ "checkJs": true,
6
+ "esModuleInterop": true,
7
+ "forceConsistentCasingInFileNames": true,
8
+ "resolveJsonModule": true,
9
+ "skipLibCheck": true,
10
+ "sourceMap": true,
11
+ "strict": true,
12
+ "moduleResolution": "bundler"
13
+ }
14
+ // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
15
+ // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
16
+ //
17
+ // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
18
+ // from the referenced tsconfig.json - TypeScript does not merge them in
19
  }
frontend/vite.config.ts CHANGED
@@ -1,15 +1,16 @@
1
- import { sveltekit } from '@sveltejs/kit/vite';
2
- import { defineConfig } from 'vite';
 
3
 
4
  export default defineConfig({
5
- plugins: [sveltekit()],
6
  server: {
7
  proxy: {
8
- '/api': 'http://localhost:7860',
9
- '/api/ws': {
10
- target: 'ws://localhost:7860',
11
- ws: true
12
- }
13
- }
14
- }
15
  });
 
1
+ import { sveltekit } from "@sveltejs/kit/vite";
2
+ import tailwindcss from "@tailwindcss/vite";
3
+ import { defineConfig } from "vite";
4
 
5
  export default defineConfig({
6
+ plugins: [tailwindcss(), sveltekit()],
7
  server: {
8
  proxy: {
9
+ "/api": "http://localhost:7860",
10
+ "/api/ws": {
11
+ target: "ws://localhost:7860",
12
+ ws: true,
13
+ },
14
+ },
15
+ },
16
  });