poe-svelte-ui-lib 1.10.9 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,17 @@
7
7
  import { onMount } from "svelte"
8
8
  import { twMerge } from "tailwind-merge"
9
9
 
10
- let { id = crypto.randomUUID(), wrapperClass, label = { name: "", class: "" }, showSelect = true }: IVideoViewerProps = $props()
10
+ let {
11
+ id = crypto.randomUUID(),
12
+ wrapperClass,
13
+ label = { name: "", class: "" },
14
+ showSelect = true,
15
+ source = "camera",
16
+ frame = null,
17
+ status,
18
+ }: IVideoViewerProps = $props()
19
+
20
+ const isRemote = $derived(source === "remote")
11
21
 
12
22
  let videoElement = $state<HTMLVideoElement | null>(null)
13
23
  let stream = $state<MediaStream | null>(null)
@@ -92,11 +102,16 @@
92
102
  }
93
103
  }
94
104
 
95
- setInterval(async () => {
96
- sources = await getDevices()
97
- }, 1000)
105
+ $effect(() => {
106
+ if (isRemote) return
107
+ const interval = setInterval(async () => {
108
+ sources = await getDevices()
109
+ }, 1000)
110
+ return () => clearInterval(interval)
111
+ })
98
112
 
99
113
  onMount(() => {
114
+ if (isRemote) return
100
115
  getDevices()
101
116
  if (typeof window !== "undefined" && "mediaDevices" in navigator) {
102
117
  handlePermission()
@@ -105,6 +120,20 @@
105
120
  stopStream()
106
121
  }
107
122
  })
123
+
124
+ /* Remote-режим: кадр приходит извне через проп frame — компонент только рендерит, WS не открывает.
125
+ object URL создаётся заново на каждый кадр и отзывается перед следующим (и при размонтировании) */
126
+ let remoteImgSrc = $state<string | null>(null)
127
+ $effect(() => {
128
+ if (!isRemote || !frame) {
129
+ remoteImgSrc = null
130
+ return
131
+ }
132
+ const blob = frame instanceof Blob ? frame : new Blob([frame], { type: "image/jpeg" })
133
+ const url = URL.createObjectURL(blob)
134
+ remoteImgSrc = url
135
+ return () => URL.revokeObjectURL(url)
136
+ })
108
137
  </script>
109
138
 
110
139
  <div id={`${id}-${crypto.randomUUID().slice(0, 6)}`} class={twMerge("flex flex-col w-full h-full items-center", wrapperClass)}>
@@ -112,30 +141,57 @@
112
141
  <h5 class={twMerge(`w-full px-4 text-center`, label.class)}>{label.name}</h5>
113
142
  {/if}
114
143
  <div class="relative flex flex-1 w-full items-center justify-center">
115
- {#if loading || error}
116
- <div class="absolute h-full w-full rounded-2xl px-40 py-15 flex flex-col items-center justify-center bg-(--border-color)/50 gap-4 z-10">
117
- {#if loading}
118
- <h2>{$T("constructor.props.camera.loading")}</h2>
119
- {:else if error}
120
- <svg class="w-12 h-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
121
- <path
122
- stroke-linecap="round"
123
- stroke-linejoin="round"
124
- stroke-width="2"
125
- d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
126
- />
127
- </svg>
128
- <h5 class="text-red-600">{error}</h5>
129
- <Button wrapperClass="w-fit" content={{ name: $T("library.retry") }} componentClass="bg-blue px-5" onClick={handlePermission} />
144
+ {#if isRemote}
145
+ {#if status !== "live" || !remoteImgSrc}
146
+ <div class="absolute h-full w-full rounded-2xl px-40 py-15 flex flex-col items-center justify-center bg-(--border-color)/50 gap-4 z-10">
147
+ {#if status === "error"}
148
+ <svg class="w-12 h-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
149
+ <path
150
+ stroke-linecap="round"
151
+ stroke-linejoin="round"
152
+ stroke-width="2"
153
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
154
+ />
155
+ </svg>
156
+ <h5 class="text-red-600">{$T("constructor.props.video.viewer.error")}</h5>
157
+ {:else if status === "offline"}
158
+ <h2>{$T("constructor.props.video.viewer.offline")}</h2>
159
+ {:else}
160
+ <h2>{$T("constructor.props.video.viewer.connecting")}</h2>
161
+ {/if}
162
+ </div>
163
+ {/if}
164
+ <div class="absolute h-full w-full rounded-2xl bg-(--border-color)/50">
165
+ {#if remoteImgSrc}
166
+ <img src={remoteImgSrc} alt={label.name || ""} class="h-full w-full rounded-2xl object-contain block" />
130
167
  {/if}
131
168
  </div>
169
+ {:else}
170
+ {#if loading || error}
171
+ <div class="absolute h-full w-full rounded-2xl px-40 py-15 flex flex-col items-center justify-center bg-(--border-color)/50 gap-4 z-10">
172
+ {#if loading}
173
+ <h2>{$T("constructor.props.camera.loading")}</h2>
174
+ {:else if error}
175
+ <svg class="w-12 h-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
176
+ <path
177
+ stroke-linecap="round"
178
+ stroke-linejoin="round"
179
+ stroke-width="2"
180
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
181
+ />
182
+ </svg>
183
+ <h5 class="text-red-600">{error}</h5>
184
+ <Button wrapperClass="w-fit" content={{ name: $T("library.retry") }} componentClass="bg-blue px-5" onClick={handlePermission} />
185
+ {/if}
186
+ </div>
187
+ {/if}
188
+ <div class="absolute h-full w-full rounded-2xl bg-(--border-color)/50">
189
+ <video bind:this={videoElement} class="h-full w-full rounded-2xl object-contain block {!isStreaming || loading ? 'invisible' : 'visible'}"></video>
190
+ </div>
132
191
  {/if}
133
- <div class="absolute h-full w-full rounded-2xl bg-(--border-color)/50">
134
- <video bind:this={videoElement} class="h-full w-full rounded-2xl object-contain block {!isStreaming || loading ? 'invisible' : 'visible'}"></video>
135
- </div>
136
192
  </div>
137
193
 
138
- {#if sources.length > 1 && showSelect}
194
+ {#if !isRemote && sources.length > 1 && showSelect}
139
195
  {@const deviceOptions = sources.map((dev) => {
140
196
  return { id: dev.deviceId, name: dev.label, value: dev.deviceId }
141
197
  })}
@@ -24,6 +24,23 @@
24
24
  />
25
25
  {/snippet}
26
26
 
27
+ {#snippet VideoViewerSource()}
28
+ <UI.Switch
29
+ label={{ name: $T("constructor.props.video.viewer.source"), captionLeft: $T("constructor.props.video.viewer.source.camera"), captionRight: $T("constructor.props.video.viewer.source.remote") }}
30
+ value={component.properties?.source === "remote" ? 1 : 0}
31
+ options={[{ id: crypto.randomUUID(), value: 0, class: "" }]}
32
+ onChange={(value) => updateProperty("source", value === 1 ? "remote" : "camera", component, onPropertyChange)}
33
+ />
34
+ {/snippet}
35
+
36
+ {#snippet VideoViewerStreamKey()}
37
+ <UI.Input
38
+ label={{ name: $T("constructor.props.video.viewer.stream.key") }}
39
+ value={component.properties?.streamKey ?? ""}
40
+ onUpdate={(value) => updateProperty("streamKey", value, component, onPropertyChange)}
41
+ />
42
+ {/snippet}
43
+
27
44
  {#if forConstructor}
28
45
  <div class="flex mb-4 justify-center gap-8">
29
46
  <div class="flex w-1/3 flex-col px-2">
@@ -31,9 +48,13 @@
31
48
  </div>
32
49
  <div class="flex w-1/3 flex-col px-2">
33
50
  <CommonSnippets snippet="Label" {component} {onPropertyChange} />
51
+ {@render VideoViewerSource()}
34
52
  </div>
35
53
  <div class="flex w-1/3 flex-col px-2">
36
54
  {@render VideoViewershowSelect()}
55
+ {#if component.properties?.source === "remote"}
56
+ {@render VideoViewerStreamKey()}
57
+ {/if}
37
58
  </div>
38
59
  </div>
39
60
  {:else}
@@ -45,9 +66,13 @@
45
66
  <div class="flex w-1/3 flex-col px-2">
46
67
  <CommonSnippets snippet="WrapperClass" {component} {onPropertyChange} />
47
68
  <CommonSnippets snippet="Label" {component} {onPropertyChange} />
69
+ {@render VideoViewerSource()}
48
70
  </div>
49
71
  <div class="flex w-1/3 flex-col px-2">
50
72
  {@render VideoViewershowSelect()}
73
+ {#if component.properties?.source === "remote"}
74
+ {@render VideoViewerStreamKey()}
75
+ {/if}
51
76
  </div>
52
77
  </div>
53
78
  {/if}
@@ -181,6 +181,13 @@ const translations = {
181
181
  "constructor.props.camera.loading": "Загрузка камеры...",
182
182
  "constructor.props.video.viewer.no.permission": "Нет разрешения для доступа к камере",
183
183
  "constructor.props.video.viewer.no.access": "Камера недоступна",
184
+ "constructor.props.video.viewer.connecting": "Подключение...",
185
+ "constructor.props.video.viewer.offline": "Устройство offline",
186
+ "constructor.props.video.viewer.error": "Нет сигнала",
187
+ "constructor.props.video.viewer.source": "Режим",
188
+ "constructor.props.video.viewer.source.camera": "Камера",
189
+ "constructor.props.video.viewer.source.remote": "Удалённо",
190
+ "constructor.props.video.viewer.stream.key": "Ключ потока",
184
191
  "constructor.props.source": "Источник",
185
192
  "constructor.props.showSelect": "Выбор источника",
186
193
  "constructor.props.widget.mode": "Зацикленный",
package/dist/types.d.ts CHANGED
@@ -428,6 +428,10 @@ export interface IVideoViewerProps {
428
428
  class?: string;
429
429
  };
430
430
  showSelect?: boolean;
431
+ source?: "camera" | "remote";
432
+ streamKey?: string;
433
+ frame?: Uint8Array | Blob | null;
434
+ status?: "connecting" | "live" | "offline" | "error";
431
435
  }
432
436
  export interface IWidgetProps {
433
437
  id?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.10.9",
3
+ "version": "1.11.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {