Spaces:
Runtime error
Runtime error
bump sveltkit and svelte
Browse files- frontend/.eslintignore +0 -13
- frontend/.eslintrc.cjs +0 -30
- frontend/.gitignore +17 -4
- frontend/.prettierignore +4 -11
- frontend/.prettierrc +1 -9
- frontend/README.md +5 -5
- frontend/package-lock.json +0 -0
- frontend/package.json +21 -22
- frontend/postcss.config.js +0 -6
- frontend/src/app.css +2 -3
- frontend/src/app.d.ts +8 -7
- frontend/src/app.html +9 -9
- frontend/src/lib/components/AspectRatioSelect.svelte +10 -9
- frontend/src/lib/components/Button.svelte +7 -8
- frontend/src/lib/components/Checkbox.svelte +12 -6
- frontend/src/lib/components/ImagePlayer.svelte +38 -31
- frontend/src/lib/components/InputRange.svelte +6 -5
- frontend/src/lib/components/MediaListSwitcher.svelte +15 -19
- frontend/src/lib/components/PipelineOptions.svelte +39 -24
- frontend/src/lib/components/SeedInput.svelte +9 -9
- frontend/src/lib/components/Selectlist.svelte +6 -5
- frontend/src/lib/components/TextArea.svelte +6 -5
- frontend/src/lib/components/VideoInput.svelte +73 -43
- frontend/src/lib/components/Warning.svelte +13 -11
- frontend/src/lib/icons/aspect.svelte +7 -2
- frontend/src/lib/icons/expand.svelte +7 -2
- frontend/src/lib/icons/floppy.svelte +7 -2
- frontend/src/lib/icons/screen.svelte +7 -2
- frontend/src/lib/icons/spinner.svelte +7 -2
- frontend/src/lib/lcmLive.ts +58 -50
- frontend/src/lib/mediaStream.ts +28 -23
- frontend/src/lib/store.ts +12 -4
- frontend/src/lib/types.ts +8 -8
- frontend/src/lib/utils.ts +22 -22
- frontend/src/piexifjs.d.ts +3 -3
- frontend/src/routes/+layout.svelte +5 -3
- frontend/src/routes/+page.svelte +61 -47
- frontend/svelte.config.js +8 -9
- frontend/tailwind.config.js +3 -3
- frontend/tsconfig.json +17 -15
- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
4 |
/.svelte-kit
|
5 |
-
/
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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 |
-
"
|
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 |
-
#
|
2 |
|
3 |
-
Everything you need to build a Svelte project, powered by [`
|
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 |
-
|
12 |
|
13 |
# create a new project in my-app
|
14 |
-
|
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://
|
|
|
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": "
|
12 |
"format": "prettier --write ."
|
13 |
},
|
14 |
"devDependencies": {
|
15 |
-
"@
|
|
|
16 |
"@sveltejs/adapter-static": "^3.0.8",
|
17 |
-
"@sveltejs/kit": "^2.
|
18 |
-
"@sveltejs/vite-plugin-svelte": "^
|
19 |
-
"@
|
20 |
-
"@
|
21 |
-
"
|
22 |
-
"
|
23 |
-
"eslint": "^
|
24 |
-
"
|
25 |
-
"
|
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
|
32 |
-
"svelte-check": "^
|
33 |
-
"tailwindcss": "^
|
34 |
-
"
|
35 |
-
"typescript": "^
|
36 |
-
"vite": "^
|
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 |
-
@
|
2 |
-
@
|
3 |
-
@tailwind utilities;
|
|
|
1 |
+
@import "tailwindcss";
|
2 |
+
@plugin '@tailwindcss/typography';
|
|
frontend/src/app.d.ts
CHANGED
@@ -1,12 +1,13 @@
|
|
1 |
-
// See https://
|
2 |
// for information about these interfaces
|
3 |
declare global {
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
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 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
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 |
-
|
|
|
|
|
|
|
3 |
|
4 |
-
let options: string[] = [
|
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(
|
12 |
aspectRatio = width / height;
|
13 |
-
|
14 |
}
|
15 |
</script>
|
16 |
|
17 |
<div class="relative">
|
18 |
<select
|
19 |
-
|
20 |
title="Aspect Ratio"
|
21 |
-
class="
|
22 |
>
|
23 |
-
{#each options as option
|
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 |
-
|
3 |
-
export let disabled: boolean = false;
|
4 |
-
export let title: string = '';
|
5 |
</script>
|
6 |
|
7 |
-
<button
|
8 |
-
|
9 |
</button>
|
10 |
|
11 |
-
<style lang="postcss"
|
12 |
-
|
13 |
-
|
|
|
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
|
3 |
-
import { onMount } from
|
4 |
-
|
5 |
-
|
|
|
6 |
onMount(() => {
|
7 |
-
value =
|
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
|
|
|
|
|
|
|
|
|
|
|
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
|
3 |
-
import { getPipelineValues } from
|
4 |
|
5 |
-
import Button from
|
6 |
-
import Floppy from
|
7 |
-
import Expand from
|
8 |
-
import { snapImage, expandWindow } from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(
|
28 |
-
expandedWindow.addEventListener(
|
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
|
|
|
|
|
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={
|
56 |
-
|
57 |
-
console.error(
|
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 |
-
|
68 |
-
title=
|
69 |
-
|
70 |
>
|
71 |
-
<Expand
|
72 |
</Button>
|
73 |
<Button
|
74 |
-
|
75 |
disabled={!isLCMRunning}
|
76 |
-
title=
|
77 |
-
|
78 |
>
|
79 |
-
<Floppy
|
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
|
3 |
-
import { onMount } from
|
4 |
-
|
5 |
-
|
|
|
6 |
onMount(() => {
|
7 |
-
value = Number(params?.default
|
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
|
3 |
-
import Screen from
|
4 |
-
import AspectRatioSelect from
|
5 |
-
import { onMount } from
|
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
|
|
|
|
|
22 |
<AspectRatioSelect
|
23 |
bind:aspectRatio
|
24 |
-
|
25 |
/>
|
26 |
<button
|
27 |
title="Share your screen"
|
28 |
-
class="
|
29 |
-
|
30 |
>
|
31 |
<span>Share</span>
|
32 |
|
33 |
-
<Screen
|
34 |
</button>
|
35 |
{#if $mediaDevices}
|
36 |
<select
|
37 |
bind:value={deviceId}
|
38 |
-
|
39 |
id="devices-list"
|
40 |
-
class="
|
41 |
>
|
42 |
-
{#each $mediaDevices as device
|
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
|
3 |
-
import { FieldType } from
|
4 |
-
import InputRange from
|
5 |
-
import SeedInput from
|
6 |
-
import TextArea from
|
7 |
-
import Checkbox from
|
8 |
-
import Selectlist from
|
9 |
-
import { pipelineValues } from
|
10 |
|
11 |
-
|
12 |
|
13 |
-
|
14 |
-
|
|
|
|
|
|
|
|
|
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]}
|
|
|
23 |
{:else if params.field === FieldType.SEED}
|
24 |
-
<SeedInput {params} bind:value={$pipelineValues[params.id]}
|
|
|
25 |
{:else if params.field === FieldType.TEXTAREA}
|
26 |
-
<TextArea {params} bind:value={$pipelineValues[params.id]}
|
|
|
27 |
{:else if params.field === FieldType.CHECKBOX}
|
28 |
-
<Checkbox {params} bind:value={$pipelineValues[params.id]}
|
|
|
29 |
{:else if params.field === FieldType.SELECT}
|
30 |
-
<Selectlist {params} bind:value={$pipelineValues[params.id]}
|
|
|
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)
|
|
|
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]}
|
|
|
47 |
{:else if params.field === FieldType.SEED}
|
48 |
-
<SeedInput {params} bind:value={$pipelineValues[params.id]}
|
|
|
49 |
{:else if params.field === FieldType.TEXTAREA}
|
50 |
-
<TextArea {params} bind:value={$pipelineValues[params.id]}
|
|
|
51 |
{:else if params.field === FieldType.CHECKBOX}
|
52 |
-
<Checkbox {params} bind:value={$pipelineValues[params.id]}
|
|
|
53 |
{:else if params.field === FieldType.SELECT}
|
54 |
-
<Selectlist {params} bind:value={$pipelineValues[params.id]}
|
|
|
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
|
3 |
-
import { onMount } from
|
4 |
-
import
|
5 |
-
|
6 |
-
|
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
|
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
|
3 |
-
import {
|
4 |
-
|
5 |
-
|
|
|
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
|
3 |
-
import {
|
4 |
-
|
5 |
-
|
|
|
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
|
3 |
|
4 |
-
import { onDestroy, onMount } from
|
5 |
import {
|
6 |
mediaStreamStatus,
|
7 |
MediaStreamStatusEnum,
|
8 |
onFrameChangeStore,
|
9 |
mediaStream,
|
10 |
-
mediaDevices
|
11 |
-
} from
|
12 |
-
import MediaListSwitcher from
|
13 |
-
import Button from
|
14 |
-
import Expand from
|
|
|
|
|
|
|
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 |
-
|
32 |
-
|
33 |
-
|
|
|
|
|
34 |
});
|
35 |
-
|
36 |
console.log(selectedDevice);
|
37 |
-
}
|
38 |
|
39 |
onDestroy(() => {
|
40 |
-
if (videoFrameCallbackId
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
});
|
42 |
|
43 |
-
$: if (videoEl) {
|
44 |
-
videoEl.srcObject = $mediaStream;
|
45 |
-
}
|
46 |
let lastMillis = 0;
|
47 |
-
async function onFrameChange(now: DOMHighResTimeStamp
|
|
|
|
|
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
|
65 |
(blob) => {
|
66 |
resolve(blob as Blob);
|
67 |
},
|
68 |
-
|
69 |
-
1
|
70 |
);
|
71 |
});
|
72 |
onFrameChangeStore.set({ blob });
|
73 |
videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
|
74 |
}
|
75 |
|
76 |
-
|
77 |
-
|
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
|
92 |
-
|
|
|
|
|
|
|
|
|
93 |
{#if $mediaDevices.length > 0}
|
94 |
-
<div class="absolute bottom-0 right-0 z-10 flex bg-slate-400
|
95 |
<MediaListSwitcher />
|
96 |
<Button
|
97 |
-
|
98 |
-
title=
|
99 |
-
|
100 |
>
|
101 |
-
<Expand
|
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 |
-
|
109 |
videoIsReady = true;
|
110 |
}}
|
111 |
playsinline
|
@@ -113,11 +135,19 @@
|
|
113 |
muted
|
114 |
loop
|
115 |
></video>
|
116 |
-
<canvas
|
|
|
|
|
117 |
></canvas>
|
118 |
</div>
|
119 |
-
<div
|
120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
3 |
|
4 |
-
let timeout = 0;
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
|
|
|
|
12 |
</script>
|
13 |
|
14 |
{#if message}
|
@@ -16,8 +18,8 @@
|
|
16 |
<button
|
17 |
type="button"
|
18 |
class="w-full"
|
19 |
-
|
20 |
-
|
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 |
-
|
3 |
</script>
|
4 |
|
5 |
-
<svg
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
3 |
</script>
|
4 |
|
5 |
-
<svg
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
3 |
</script>
|
4 |
|
5 |
-
<svg
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
3 |
</script>
|
4 |
|
5 |
-
<svg
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
3 |
</script>
|
4 |
|
5 |
-
<svg
|
|
|
|
|
|
|
|
|
|
|
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
|
2 |
|
3 |
export enum LCMLiveStatus {
|
4 |
-
CONNECTED =
|
5 |
-
DISCONNECTED =
|
6 |
-
CONNECTING =
|
7 |
-
WAIT =
|
8 |
-
SEND_FRAME =
|
9 |
-
TIMEOUT =
|
10 |
-
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
|
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 !==
|
25 |
-
window.addEventListener(
|
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,
|
31 |
}
|
32 |
});
|
33 |
}
|
34 |
export const lcmLiveActions = {
|
35 |
-
async start(
|
|
|
|
|
|
|
|
|
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 ===
|
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(
|
57 |
lcmLiveStatus.set(LCMLiveStatus.ERROR);
|
58 |
streamId.set(null);
|
59 |
-
reject(new Error(
|
60 |
websocket.close();
|
61 |
}
|
62 |
}, 10000); // 10 second timeout
|
63 |
|
64 |
websocket.onopen = () => {
|
65 |
clearTimeout(connectionTimeout);
|
66 |
-
console.log(
|
67 |
};
|
68 |
|
69 |
websocket.onclose = (event) => {
|
70 |
clearTimeout(connectionTimeout);
|
71 |
-
console.log(
|
|
|
|
|
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(
|
|
|
|
|
81 |
}
|
82 |
};
|
83 |
|
84 |
websocket.onerror = (err) => {
|
85 |
clearTimeout(connectionTimeout);
|
86 |
-
console.error(
|
87 |
lcmLiveStatus.set(LCMLiveStatus.ERROR);
|
88 |
streamId.set(null);
|
89 |
-
reject(new Error(
|
90 |
};
|
91 |
|
92 |
websocket.onmessage = (event) => {
|
93 |
try {
|
94 |
const data = JSON.parse(event.data);
|
95 |
switch (data.status) {
|
96 |
-
case
|
97 |
lcmLiveStatus.set(LCMLiveStatus.CONNECTED);
|
98 |
streamId.set(userId);
|
99 |
-
resolve({ status:
|
100 |
break;
|
101 |
-
case
|
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:
|
107 |
for (const d of streamData) {
|
108 |
this.send(d);
|
109 |
}
|
110 |
} catch (error) {
|
111 |
-
console.error(
|
112 |
}
|
113 |
break;
|
114 |
-
case
|
115 |
lcmLiveStatus.set(LCMLiveStatus.WAIT);
|
116 |
break;
|
117 |
-
case
|
118 |
-
console.log(
|
119 |
lcmLiveStatus.set(LCMLiveStatus.TIMEOUT);
|
120 |
streamId.set(null);
|
121 |
-
reject(new Error(
|
122 |
break;
|
123 |
-
case
|
124 |
-
console.error(
|
125 |
lcmLiveStatus.set(LCMLiveStatus.ERROR);
|
126 |
streamId.set(null);
|
127 |
-
reject(new Error(data.message ||
|
128 |
break;
|
129 |
default:
|
130 |
-
console.log(
|
131 |
}
|
132 |
} catch (error) {
|
133 |
-
console.error(
|
134 |
}
|
135 |
};
|
136 |
} catch (err) {
|
137 |
-
console.error(
|
138 |
lcmLiveStatus.set(LCMLiveStatus.ERROR);
|
139 |
streamId.set(null);
|
140 |
reject(err);
|
141 |
}
|
142 |
});
|
143 |
},
|
144 |
-
send(data: Blob |
|
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 |
-
? [
|
155 |
-
:
|
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(
|
166 |
// Handle WebSocket error by forcing disconnection
|
167 |
this.stop();
|
168 |
}
|
169 |
},
|
170 |
|
171 |
-
async reconnect(
|
|
|
|
|
|
|
|
|
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(
|
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(
|
192 |
};
|
193 |
|
194 |
// Set up onerror to be silent during intentional closure
|
195 |
websocket.onerror = () => {};
|
196 |
|
197 |
-
websocket.close(1000,
|
198 |
}
|
199 |
}
|
200 |
} catch (error) {
|
201 |
-
console.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
|
2 |
|
3 |
const BASE_HEIGHT = 720;
|
4 |
export enum MediaStreamStatusEnum {
|
5 |
-
INIT =
|
6 |
-
CONNECTED =
|
7 |
-
DISCONNECTED =
|
8 |
}
|
9 |
-
export const onFrameChangeStore: Writable<{ blob: Blob }> = writable({
|
|
|
|
|
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(
|
|
|
|
|
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:
|
58 |
},
|
59 |
audio: false,
|
60 |
-
surfaceSwitching:
|
61 |
};
|
62 |
|
63 |
let captureStream = null;
|
64 |
|
65 |
try {
|
66 |
-
captureStream =
|
|
|
67 |
const videoTrack = captureStream.getVideoTracks()[0];
|
68 |
|
69 |
-
console.log(
|
70 |
console.log(JSON.stringify(videoTrack.getSettings(), null, 2));
|
71 |
-
console.log(
|
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(
|
79 |
} catch (err) {
|
80 |
console.error(err);
|
81 |
}
|
82 |
},
|
83 |
async switchCamera(mediaDevicedID: string, aspectRatio: number) {
|
84 |
-
console.log(
|
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(
|
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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
-
export
|
4 |
-
|
|
|
|
|
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 =
|
3 |
-
SEED =
|
4 |
-
TEXTAREA =
|
5 |
-
CHECKBOX =
|
6 |
-
SELECT =
|
7 |
}
|
8 |
export const enum PipelineMode {
|
9 |
-
IMAGE =
|
10 |
-
VIDEO =
|
11 |
-
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
|
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 =
|
11 |
-
video = 'video'
|
12 |
}
|
13 |
|
14 |
export function snapImage(imageEl: HTMLImageElement, info: IImageInfo) {
|
15 |
try {
|
16 |
-
const zeroth: { [key: string]:
|
17 |
-
const exif: { [key: string]:
|
18 |
-
const gps: { [key: string]:
|
19 |
-
zeroth[piexif.ImageIFD.Make] =
|
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 |
-
|
24 |
exif[piexif.ExifIFD.DateTimeOriginal] = new Date().toISOString();
|
25 |
|
26 |
-
const exifObj = {
|
27 |
const exifBytes = piexif.dump(exifObj);
|
28 |
|
29 |
-
const canvas = document.createElement(
|
30 |
canvas.width = imageEl.naturalWidth;
|
31 |
canvas.height = imageEl.naturalHeight;
|
32 |
-
const ctx = canvas.getContext(
|
33 |
ctx.drawImage(imageEl, 0, 0);
|
34 |
-
const dataURL = canvas.toDataURL(
|
35 |
const withExif = piexif.insert(exifBytes, dataURL);
|
36 |
|
37 |
-
const a = document.createElement(
|
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
|
47 |
const newWindow = window.open(
|
48 |
-
|
49 |
-
|
50 |
-
|
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(
|
91 |
img.src = streamURL;
|
92 |
-
img.style.width =
|
93 |
-
img.style.height =
|
94 |
-
img.style.objectFit =
|
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
|
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:
|
11 |
-
export function insert(exifBytes:
|
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
|
|
|
|
|
3 |
</script>
|
4 |
|
5 |
-
|
|
|
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
|
3 |
-
import type { Fields, PipelineInfo } from
|
4 |
-
import { PipelineMode } from
|
5 |
-
import ImagePlayer from
|
6 |
-
import VideoInput from
|
7 |
-
import Button from
|
8 |
-
import PipelineOptions from
|
9 |
-
import Spinner from
|
10 |
-
import Warning from
|
11 |
-
import { lcmLiveStatus, lcmLiveActions, LCMLiveStatus } from
|
12 |
-
import { mediaStreamActions, onFrameChangeStore } from
|
13 |
-
import { getPipelineValues, deboucedPipelineValues } from
|
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
|
22 |
-
let
|
|
|
|
|
23 |
onMount(() => {
|
24 |
getSettings();
|
25 |
});
|
26 |
|
27 |
async function getSettings() {
|
28 |
-
const settings = await fetch(
|
29 |
pipelineParams = settings.input_params.properties;
|
30 |
pipelineInfo = settings.info.properties;
|
31 |
-
isImageMode = pipelineInfo
|
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(
|
48 |
currentQueueSize = data.queue_size;
|
49 |
setTimeout(getQueueSize, 10000);
|
50 |
}
|
51 |
-
|
52 |
-
|
|
|
53 |
if (isImageMode) {
|
54 |
return [getPipelineValues(), $onFrameChangeStore?.blob];
|
55 |
} else {
|
@@ -57,19 +60,20 @@
|
|
57 |
}
|
58 |
}
|
59 |
|
60 |
-
|
61 |
-
$lcmLiveStatus !== LCMLiveStatus.DISCONNECTED &&
|
62 |
-
|
|
|
|
|
63 |
|
64 |
-
|
65 |
// Set warning messages based on lcmLiveStatus
|
66 |
if ($lcmLiveStatus === LCMLiveStatus.TIMEOUT) {
|
67 |
-
warningMessage =
|
68 |
} else if ($lcmLiveStatus === LCMLiveStatus.ERROR) {
|
69 |
-
warningMessage =
|
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(
|
112 |
-
warningMessage =
|
|
|
113 |
disabled = false;
|
114 |
toggleQueueChecker(true);
|
115 |
}
|
@@ -119,7 +124,7 @@
|
|
119 |
async function reconnect() {
|
120 |
try {
|
121 |
disabled = true;
|
122 |
-
warningMessage =
|
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 :
|
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"
|
157 |
-
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
169 |
Try reconnecting
|
170 |
</button>
|
171 |
</p>
|
@@ -181,11 +194,11 @@
|
|
181 |
></VideoInput>
|
182 |
</div>
|
183 |
{/if}
|
184 |
-
<div class={isImageMode ?
|
185 |
<ImagePlayer />
|
186 |
</div>
|
187 |
<div class="sm:col-span-4 sm:row-start-2">
|
188 |
-
<Button
|
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
|
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
|
2 |
-
import { vitePreprocess } from
|
3 |
|
4 |
-
/** @type {import('@sveltejs/kit').Config} */
|
5 |
const config = {
|
6 |
-
preprocess: vitePreprocess(
|
7 |
kit: {
|
8 |
adapter: adapter({
|
9 |
-
pages:
|
10 |
-
assets:
|
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: [
|
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 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
|
|
|
|
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
|
2 |
-
import
|
|
|
3 |
|
4 |
export default defineConfig({
|
5 |
-
plugins: [sveltekit()],
|
6 |
server: {
|
7 |
proxy: {
|
8 |
-
|
9 |
-
|
10 |
-
target:
|
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 |
});
|