@gradio/image 0.14.0 → 0.16.0-beta.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.
- package/CHANGELOG.md +56 -0
- package/Image.stories.svelte +43 -15
- package/Image.test.ts +2 -3
- package/Index.svelte +54 -4
- package/dist/Example.svelte +46 -0
- package/dist/Example.svelte.d.ts +19 -0
- package/dist/Index.svelte +200 -0
- package/dist/Index.svelte.d.ts +176 -0
- package/dist/shared/ClearImage.svelte +28 -0
- package/dist/shared/ClearImage.svelte.d.ts +18 -0
- package/dist/shared/Image.svelte +26 -0
- package/dist/shared/Image.svelte.d.ts +247 -0
- package/dist/shared/ImagePreview.svelte +155 -0
- package/dist/shared/ImagePreview.svelte.d.ts +32 -0
- package/dist/shared/ImageUploader.svelte +198 -0
- package/dist/shared/ImageUploader.svelte.d.ts +47 -0
- package/dist/shared/Webcam.svelte +431 -0
- package/dist/shared/Webcam.svelte.d.ts +40 -0
- package/dist/shared/WebcamPermissions.svelte +42 -0
- package/dist/shared/WebcamPermissions.svelte.d.ts +18 -0
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +2 -0
- package/dist/shared/stream_utils.d.ts +5 -0
- package/dist/shared/stream_utils.js +31 -0
- package/dist/shared/utils.d.ts +1 -0
- package/dist/shared/utils.js +28 -0
- package/package.json +38 -14
- package/shared/ImagePreview.svelte +17 -6
- package/shared/ImageUploader.svelte +49 -20
- package/shared/Webcam.svelte +80 -15
@@ -0,0 +1,40 @@
|
|
1
|
+
import { SvelteComponent } from "svelte";
|
2
|
+
import type { I18nFormatter } from "@gradio/utils";
|
3
|
+
import { type FileData, type Client } from "@gradio/client";
|
4
|
+
declare const __propDef: {
|
5
|
+
props: {
|
6
|
+
modify_stream?: ((state: "open" | "closed" | "waiting") => void) | undefined;
|
7
|
+
set_time_limit?: ((time: number) => void) | undefined;
|
8
|
+
streaming?: boolean | undefined;
|
9
|
+
pending?: boolean | undefined;
|
10
|
+
root?: string | undefined;
|
11
|
+
stream_every?: number | undefined;
|
12
|
+
mode?: ("image" | "video") | undefined;
|
13
|
+
mirror_webcam: boolean;
|
14
|
+
include_audio: boolean;
|
15
|
+
i18n: I18nFormatter;
|
16
|
+
upload: Client["upload"];
|
17
|
+
value?: (FileData | null) | undefined;
|
18
|
+
click_outside?: ((node: Node, cb: any) => any) | undefined;
|
19
|
+
};
|
20
|
+
events: {
|
21
|
+
stream: CustomEvent<undefined>;
|
22
|
+
capture: CustomEvent<Blob | FileData | null>;
|
23
|
+
error: CustomEvent<string>;
|
24
|
+
start_recording: CustomEvent<undefined>;
|
25
|
+
stop_recording: CustomEvent<undefined>;
|
26
|
+
close_stream: CustomEvent<undefined>;
|
27
|
+
} & {
|
28
|
+
[evt: string]: CustomEvent<any>;
|
29
|
+
};
|
30
|
+
slots: {};
|
31
|
+
};
|
32
|
+
export type WebcamProps = typeof __propDef.props;
|
33
|
+
export type WebcamEvents = typeof __propDef.events;
|
34
|
+
export type WebcamSlots = typeof __propDef.slots;
|
35
|
+
export default class Webcam extends SvelteComponent<WebcamProps, WebcamEvents, WebcamSlots> {
|
36
|
+
get modify_stream(): (state: "open" | "closed" | "waiting") => void;
|
37
|
+
get set_time_limit(): (time: number) => void;
|
38
|
+
get click_outside(): (node: Node, cb: any) => any;
|
39
|
+
}
|
40
|
+
export {};
|
@@ -0,0 +1,42 @@
|
|
1
|
+
<script>import { Webcam } from "@gradio/icons";
|
2
|
+
import { createEventDispatcher } from "svelte";
|
3
|
+
const dispatch = createEventDispatcher();
|
4
|
+
</script>
|
5
|
+
|
6
|
+
<button style:height="100%" on:click={() => dispatch("click")}>
|
7
|
+
<div class="wrap">
|
8
|
+
<span class="icon-wrap">
|
9
|
+
<Webcam />
|
10
|
+
</span>
|
11
|
+
{"Click to Access Webcam"}
|
12
|
+
</div>
|
13
|
+
</button>
|
14
|
+
|
15
|
+
<style>
|
16
|
+
button {
|
17
|
+
cursor: pointer;
|
18
|
+
width: var(--size-full);
|
19
|
+
}
|
20
|
+
|
21
|
+
.wrap {
|
22
|
+
display: flex;
|
23
|
+
flex-direction: column;
|
24
|
+
justify-content: center;
|
25
|
+
align-items: center;
|
26
|
+
min-height: var(--size-60);
|
27
|
+
color: var(--block-label-text-color);
|
28
|
+
height: 100%;
|
29
|
+
padding-top: var(--size-3);
|
30
|
+
}
|
31
|
+
|
32
|
+
.icon-wrap {
|
33
|
+
width: 30px;
|
34
|
+
margin-bottom: var(--spacing-lg);
|
35
|
+
}
|
36
|
+
|
37
|
+
@media (--screen-md) {
|
38
|
+
.wrap {
|
39
|
+
font-size: var(--text-lg);
|
40
|
+
}
|
41
|
+
}
|
42
|
+
</style>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { SvelteComponent } from "svelte";
|
2
|
+
declare const __propDef: {
|
3
|
+
props: {
|
4
|
+
[x: string]: never;
|
5
|
+
};
|
6
|
+
events: {
|
7
|
+
click: CustomEvent<undefined>;
|
8
|
+
} & {
|
9
|
+
[evt: string]: CustomEvent<any>;
|
10
|
+
};
|
11
|
+
slots: {};
|
12
|
+
};
|
13
|
+
export type WebcamPermissionsProps = typeof __propDef.props;
|
14
|
+
export type WebcamPermissionsEvents = typeof __propDef.events;
|
15
|
+
export type WebcamPermissionsSlots = typeof __propDef.slots;
|
16
|
+
export default class WebcamPermissions extends SvelteComponent<WebcamPermissionsProps, WebcamPermissionsEvents, WebcamPermissionsSlots> {
|
17
|
+
}
|
18
|
+
export {};
|
@@ -0,0 +1,5 @@
|
|
1
|
+
export declare function get_devices(): Promise<MediaDeviceInfo[]>;
|
2
|
+
export declare function handle_error(error: string): void;
|
3
|
+
export declare function set_local_stream(local_stream: MediaStream | null, video_source: HTMLVideoElement): void;
|
4
|
+
export declare function get_video_stream(include_audio: boolean, video_source: HTMLVideoElement, device_id?: string): Promise<MediaStream>;
|
5
|
+
export declare function set_available_devices(devices: MediaDeviceInfo[]): MediaDeviceInfo[];
|
@@ -0,0 +1,31 @@
|
|
1
|
+
export function get_devices() {
|
2
|
+
return navigator.mediaDevices.enumerateDevices();
|
3
|
+
}
|
4
|
+
export function handle_error(error) {
|
5
|
+
throw new Error(error);
|
6
|
+
}
|
7
|
+
export function set_local_stream(local_stream, video_source) {
|
8
|
+
video_source.srcObject = local_stream;
|
9
|
+
video_source.muted = true;
|
10
|
+
video_source.play();
|
11
|
+
}
|
12
|
+
export async function get_video_stream(include_audio, video_source, device_id) {
|
13
|
+
const size = {
|
14
|
+
width: { ideal: 1920 },
|
15
|
+
height: { ideal: 1440 }
|
16
|
+
};
|
17
|
+
const constraints = {
|
18
|
+
video: device_id ? { deviceId: { exact: device_id }, ...size } : size,
|
19
|
+
audio: include_audio
|
20
|
+
};
|
21
|
+
return navigator.mediaDevices
|
22
|
+
.getUserMedia(constraints)
|
23
|
+
.then((local_stream) => {
|
24
|
+
set_local_stream(local_stream, video_source);
|
25
|
+
return local_stream;
|
26
|
+
});
|
27
|
+
}
|
28
|
+
export function set_available_devices(devices) {
|
29
|
+
const cameras = devices.filter((device) => device.kind === "videoinput");
|
30
|
+
return cameras;
|
31
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const get_coordinates_of_clicked_image: (evt: MouseEvent) => [number, number] | null;
|
@@ -0,0 +1,28 @@
|
|
1
|
+
export const get_coordinates_of_clicked_image = (evt) => {
|
2
|
+
let image;
|
3
|
+
if (evt.currentTarget instanceof Element) {
|
4
|
+
image = evt.currentTarget.querySelector("img");
|
5
|
+
}
|
6
|
+
else {
|
7
|
+
return [NaN, NaN];
|
8
|
+
}
|
9
|
+
const imageRect = image.getBoundingClientRect();
|
10
|
+
const xScale = image.naturalWidth / imageRect.width;
|
11
|
+
const yScale = image.naturalHeight / imageRect.height;
|
12
|
+
if (xScale > yScale) {
|
13
|
+
const displayed_height = image.naturalHeight / xScale;
|
14
|
+
const y_offset = (imageRect.height - displayed_height) / 2;
|
15
|
+
var x = Math.round((evt.clientX - imageRect.left) * xScale);
|
16
|
+
var y = Math.round((evt.clientY - imageRect.top - y_offset) * xScale);
|
17
|
+
}
|
18
|
+
else {
|
19
|
+
const displayed_width = image.naturalWidth / yScale;
|
20
|
+
const x_offset = (imageRect.width - displayed_width) / 2;
|
21
|
+
var x = Math.round((evt.clientX - imageRect.left - x_offset) * yScale);
|
22
|
+
var y = Math.round((evt.clientY - imageRect.top) * yScale);
|
23
|
+
}
|
24
|
+
if (x < 0 || x >= image.naturalWidth || y < 0 || y >= image.naturalHeight) {
|
25
|
+
return null;
|
26
|
+
}
|
27
|
+
return [x, y];
|
28
|
+
};
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@gradio/image",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.16.0-beta.1",
|
4
4
|
"description": "Gradio UI packages",
|
5
5
|
"type": "module",
|
6
6
|
"author": "",
|
@@ -10,24 +10,48 @@
|
|
10
10
|
"cropperjs": "^1.5.12",
|
11
11
|
"lazy-brush": "^1.0.1",
|
12
12
|
"resize-observer-polyfill": "^1.5.1",
|
13
|
-
"@gradio/atoms": "^0.
|
14
|
-
"@gradio/icons": "^0.
|
15
|
-
"@gradio/
|
16
|
-
"@gradio/
|
17
|
-
"@gradio/
|
18
|
-
"@gradio/
|
19
|
-
"@gradio/
|
13
|
+
"@gradio/atoms": "^0.8.1-beta.1",
|
14
|
+
"@gradio/icons": "^0.8.0-beta.1",
|
15
|
+
"@gradio/client": "^1.6.0-beta.1",
|
16
|
+
"@gradio/utils": "^0.7.0-beta.1",
|
17
|
+
"@gradio/wasm": "^0.13.1-beta.1",
|
18
|
+
"@gradio/upload": "^0.12.4-beta.1",
|
19
|
+
"@gradio/statustracker": "^0.8.0-beta.1"
|
20
20
|
},
|
21
21
|
"devDependencies": {
|
22
|
-
"@gradio/preview": "^0.
|
22
|
+
"@gradio/preview": "^0.11.1-beta.0"
|
23
23
|
},
|
24
24
|
"main_changeset": true,
|
25
25
|
"main": "./Index.svelte",
|
26
26
|
"exports": {
|
27
|
-
".": "./
|
28
|
-
"
|
29
|
-
|
30
|
-
|
31
|
-
|
27
|
+
"./package.json": "./package.json",
|
28
|
+
".": {
|
29
|
+
"gradio": "./Index.svelte",
|
30
|
+
"svelte": "./dist/Index.svelte",
|
31
|
+
"types": "./dist/Index.svelte.d.ts"
|
32
|
+
},
|
33
|
+
"./example": {
|
34
|
+
"gradio": "./Example.svelte",
|
35
|
+
"svelte": "./dist/Example.svelte",
|
36
|
+
"types": "./dist/Example.svelte.d.ts"
|
37
|
+
},
|
38
|
+
"./base": {
|
39
|
+
"gradio": "./shared/ImagePreview.svelte",
|
40
|
+
"svelte": "./dist/shared/ImagePreview.svelte",
|
41
|
+
"types": "./dist/shared/ImagePreview.svelte.d.ts"
|
42
|
+
},
|
43
|
+
"./shared": {
|
44
|
+
"gradio": "./shared/index.ts",
|
45
|
+
"svelte": "./dist/shared/index.js",
|
46
|
+
"types": "./dist/shared/index.d.ts"
|
47
|
+
}
|
48
|
+
},
|
49
|
+
"peerDependencies": {
|
50
|
+
"svelte": "^4.0.0"
|
51
|
+
},
|
52
|
+
"repository": {
|
53
|
+
"type": "git",
|
54
|
+
"url": "git+https://github.com/gradio-app/gradio.git",
|
55
|
+
"directory": "js/image"
|
32
56
|
}
|
33
57
|
}
|
@@ -99,7 +99,7 @@
|
|
99
99
|
{/if}
|
100
100
|
</div>
|
101
101
|
<button on:click={handle_click}>
|
102
|
-
<div class:selectable>
|
102
|
+
<div class:selectable class="image-frame">
|
103
103
|
<Image src={value.url} alt="" loading="lazy" on:load />
|
104
104
|
</div>
|
105
105
|
</button>
|
@@ -111,12 +111,10 @@
|
|
111
111
|
height: 100%;
|
112
112
|
position: relative;
|
113
113
|
}
|
114
|
-
|
115
|
-
button {
|
114
|
+
|
115
|
+
.image-container button {
|
116
116
|
width: var(--size-full);
|
117
117
|
height: var(--size-full);
|
118
|
-
object-fit: contain;
|
119
|
-
display: block;
|
120
118
|
border-radius: var(--radius-lg);
|
121
119
|
|
122
120
|
display: flex;
|
@@ -124,6 +122,19 @@
|
|
124
122
|
justify-content: center;
|
125
123
|
}
|
126
124
|
|
125
|
+
.image-frame {
|
126
|
+
width: auto;
|
127
|
+
height: 100%;
|
128
|
+
display: flex;
|
129
|
+
align-items: center;
|
130
|
+
justify-content: center;
|
131
|
+
}
|
132
|
+
.image-frame :global(img) {
|
133
|
+
width: var(--size-full);
|
134
|
+
height: var(--size-full);
|
135
|
+
object-fit: scale-down;
|
136
|
+
}
|
137
|
+
|
127
138
|
.selectable {
|
128
139
|
cursor: crosshair;
|
129
140
|
}
|
@@ -152,6 +163,6 @@
|
|
152
163
|
:global(.image-container:fullscreen img) {
|
153
164
|
max-width: 90vw;
|
154
165
|
max-height: 90vh;
|
155
|
-
object-fit:
|
166
|
+
object-fit: scale-down;
|
156
167
|
}
|
157
168
|
</style>
|
@@ -2,12 +2,16 @@
|
|
2
2
|
import { createEventDispatcher, tick } from "svelte";
|
3
3
|
import { BlockLabel } from "@gradio/atoms";
|
4
4
|
import { Image as ImageIcon } from "@gradio/icons";
|
5
|
-
import
|
5
|
+
import {
|
6
|
+
type SelectData,
|
7
|
+
type I18nFormatter,
|
8
|
+
type ValueData
|
9
|
+
} from "@gradio/utils";
|
6
10
|
import { get_coordinates_of_clicked_image } from "./utils";
|
7
11
|
import Webcam from "./Webcam.svelte";
|
8
12
|
|
9
13
|
import { Upload } from "@gradio/upload";
|
10
|
-
import
|
14
|
+
import { FileData, type Client } from "@gradio/client";
|
11
15
|
import ClearImage from "./ClearImage.svelte";
|
12
16
|
import { SelectSource } from "@gradio/atoms";
|
13
17
|
import Image from "./Image.svelte";
|
@@ -28,14 +32,21 @@
|
|
28
32
|
export let max_file_size: number | null = null;
|
29
33
|
export let upload: Client["upload"];
|
30
34
|
export let stream_handler: Client["stream"];
|
35
|
+
export let stream_every: number;
|
36
|
+
|
37
|
+
export let modify_stream: (state: "open" | "closed" | "waiting") => void;
|
38
|
+
export let set_time_limit: (arg0: number) => void;
|
31
39
|
|
32
40
|
let upload_input: Upload;
|
33
41
|
let uploading = false;
|
34
42
|
export let active_source: source_type = null;
|
35
43
|
|
36
44
|
function handle_upload({ detail }: CustomEvent<FileData>): void {
|
37
|
-
|
38
|
-
|
45
|
+
// only trigger streaming event if streaming
|
46
|
+
if (!streaming) {
|
47
|
+
value = detail;
|
48
|
+
dispatch("upload");
|
49
|
+
}
|
39
50
|
}
|
40
51
|
|
41
52
|
function handle_clear(): void {
|
@@ -44,17 +55,22 @@
|
|
44
55
|
dispatch("change", null);
|
45
56
|
}
|
46
57
|
|
47
|
-
async function handle_save(
|
58
|
+
async function handle_save(
|
59
|
+
img_blob: Blob | any,
|
60
|
+
event: "change" | "stream" | "upload"
|
61
|
+
): Promise<void> {
|
48
62
|
pending = true;
|
49
63
|
const f = await upload_input.load_files([
|
50
|
-
new File([img_blob], `
|
64
|
+
new File([img_blob], `image/${streaming ? "jpeg" : "png"}`)
|
51
65
|
]);
|
52
66
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
67
|
+
if (event === "change" || event === "upload") {
|
68
|
+
value = f?.[0] || null;
|
69
|
+
await tick();
|
70
|
+
dispatch("change");
|
71
|
+
} else {
|
72
|
+
dispatch("stream", { value: f?.[0] || null, is_value_data: true });
|
73
|
+
}
|
58
74
|
pending = false;
|
59
75
|
}
|
60
76
|
|
@@ -63,14 +79,15 @@
|
|
63
79
|
|
64
80
|
const dispatch = createEventDispatcher<{
|
65
81
|
change?: never;
|
66
|
-
stream
|
82
|
+
stream: ValueData;
|
67
83
|
clear?: never;
|
68
84
|
drag: boolean;
|
69
85
|
upload?: never;
|
70
86
|
select: SelectData;
|
87
|
+
end_stream: never;
|
71
88
|
}>();
|
72
89
|
|
73
|
-
let dragging = false;
|
90
|
+
export let dragging = false;
|
74
91
|
|
75
92
|
$: dispatch("drag", dragging);
|
76
93
|
|
@@ -109,7 +126,11 @@
|
|
109
126
|
}}
|
110
127
|
/>
|
111
128
|
{/if}
|
112
|
-
<div
|
129
|
+
<div
|
130
|
+
class="upload-container"
|
131
|
+
class:reduced-height={sources.length > 1}
|
132
|
+
style:width={value ? "auto" : "100%"}
|
133
|
+
>
|
113
134
|
<Upload
|
114
135
|
hidden={value !== null || active_source === "webcam"}
|
115
136
|
bind:this={upload_input}
|
@@ -120,7 +141,7 @@
|
|
120
141
|
on:error
|
121
142
|
{root}
|
122
143
|
{max_file_size}
|
123
|
-
disable_click={!sources.includes("upload")}
|
144
|
+
disable_click={!sources.includes("upload") || value !== null}
|
124
145
|
{upload}
|
125
146
|
{stream_handler}
|
126
147
|
>
|
@@ -131,17 +152,22 @@
|
|
131
152
|
{#if active_source === "webcam" && (streaming || (!streaming && !value))}
|
132
153
|
<Webcam
|
133
154
|
{root}
|
134
|
-
|
135
|
-
on:
|
155
|
+
{value}
|
156
|
+
on:capture={(e) => handle_save(e.detail, "change")}
|
157
|
+
on:stream={(e) => handle_save(e.detail, "stream")}
|
136
158
|
on:error
|
137
159
|
on:drag
|
138
|
-
on:upload={(e) => handle_save(e.detail)}
|
160
|
+
on:upload={(e) => handle_save(e.detail, "upload")}
|
161
|
+
on:close_stream
|
139
162
|
{mirror_webcam}
|
163
|
+
{stream_every}
|
140
164
|
{streaming}
|
141
165
|
mode="image"
|
142
166
|
include_audio={false}
|
143
167
|
{i18n}
|
144
168
|
{upload}
|
169
|
+
bind:modify_stream
|
170
|
+
bind:set_time_limit
|
145
171
|
/>
|
146
172
|
{:else if value !== null && !streaming}
|
147
173
|
<!-- svelte-ignore a11y-click-events-have-key-events-->
|
@@ -165,7 +191,7 @@
|
|
165
191
|
.image-frame :global(img) {
|
166
192
|
width: var(--size-full);
|
167
193
|
height: var(--size-full);
|
168
|
-
object-fit:
|
194
|
+
object-fit: scale-down;
|
169
195
|
}
|
170
196
|
|
171
197
|
.image-frame {
|
@@ -175,10 +201,13 @@
|
|
175
201
|
}
|
176
202
|
|
177
203
|
.upload-container {
|
204
|
+
display: flex;
|
205
|
+
align-items: center;
|
206
|
+
justify-content: center;
|
207
|
+
|
178
208
|
height: 100%;
|
179
209
|
flex-shrink: 1;
|
180
210
|
max-height: 100%;
|
181
|
-
width: 100%;
|
182
211
|
}
|
183
212
|
|
184
213
|
.reduced-height {
|
package/shared/Webcam.svelte
CHANGED
@@ -1,7 +1,14 @@
|
|
1
1
|
<script lang="ts">
|
2
2
|
import { createEventDispatcher, onMount } from "svelte";
|
3
|
-
import {
|
3
|
+
import {
|
4
|
+
Camera,
|
5
|
+
Circle,
|
6
|
+
Square,
|
7
|
+
DropdownArrow,
|
8
|
+
Spinner
|
9
|
+
} from "@gradio/icons";
|
4
10
|
import type { I18nFormatter } from "@gradio/utils";
|
11
|
+
import { StreamingBar } from "@gradio/statustracker";
|
5
12
|
import { type FileData, type Client, prepare_files } from "@gradio/client";
|
6
13
|
import WebcamPermissions from "./WebcamPermissions.svelte";
|
7
14
|
import { fade } from "svelte/transition";
|
@@ -14,17 +21,39 @@
|
|
14
21
|
let video_source: HTMLVideoElement;
|
15
22
|
let available_video_devices: MediaDeviceInfo[] = [];
|
16
23
|
let selected_device: MediaDeviceInfo | null = null;
|
24
|
+
let time_limit: number | null = null;
|
25
|
+
let stream_state: "open" | "waiting" | "closed" = "closed";
|
26
|
+
|
27
|
+
export const modify_stream: (state: "open" | "closed" | "waiting") => void = (
|
28
|
+
state: "open" | "closed" | "waiting"
|
29
|
+
) => {
|
30
|
+
if (state === "closed") {
|
31
|
+
time_limit = null;
|
32
|
+
stream_state = "closed";
|
33
|
+
value = null;
|
34
|
+
} else if (state === "waiting") {
|
35
|
+
stream_state = "waiting";
|
36
|
+
} else {
|
37
|
+
stream_state = "open";
|
38
|
+
}
|
39
|
+
};
|
40
|
+
|
41
|
+
export const set_time_limit = (time: number): void => {
|
42
|
+
if (recording) time_limit = time;
|
43
|
+
};
|
17
44
|
|
18
45
|
let canvas: HTMLCanvasElement;
|
19
46
|
export let streaming = false;
|
20
47
|
export let pending = false;
|
21
48
|
export let root = "";
|
49
|
+
export let stream_every = 1;
|
22
50
|
|
23
51
|
export let mode: "image" | "video" = "image";
|
24
52
|
export let mirror_webcam: boolean;
|
25
53
|
export let include_audio: boolean;
|
26
54
|
export let i18n: I18nFormatter;
|
27
55
|
export let upload: Client["upload"];
|
56
|
+
export let value: FileData | null = null;
|
28
57
|
|
29
58
|
const dispatch = createEventDispatcher<{
|
30
59
|
stream: undefined;
|
@@ -32,6 +61,7 @@
|
|
32
61
|
error: string;
|
33
62
|
start_recording: undefined;
|
34
63
|
stop_recording: undefined;
|
64
|
+
close_stream: undefined;
|
35
65
|
}>();
|
36
66
|
|
37
67
|
onMount(() => (canvas = document.createElement("canvas")));
|
@@ -108,11 +138,15 @@
|
|
108
138
|
context.drawImage(video_source, -video_source.videoWidth, 0);
|
109
139
|
}
|
110
140
|
|
141
|
+
if (streaming && (!recording || stream_state === "waiting")) {
|
142
|
+
return;
|
143
|
+
}
|
144
|
+
|
111
145
|
canvas.toBlob(
|
112
146
|
(blob) => {
|
113
147
|
dispatch(streaming ? "stream" : "capture", blob);
|
114
148
|
},
|
115
|
-
"
|
149
|
+
`image/${streaming ? "jpeg" : "png"}`,
|
116
150
|
0.8
|
117
151
|
);
|
118
152
|
}
|
@@ -136,10 +170,10 @@
|
|
136
170
|
"sample." + mimeType.substring(6)
|
137
171
|
);
|
138
172
|
const val = await prepare_files([_video_blob]);
|
139
|
-
let
|
173
|
+
let val_ = (
|
140
174
|
(await upload(val, root))?.filter(Boolean) as FileData[]
|
141
175
|
)[0];
|
142
|
-
dispatch("capture",
|
176
|
+
dispatch("capture", val_);
|
143
177
|
dispatch("stop_recording");
|
144
178
|
}
|
145
179
|
};
|
@@ -181,9 +215,14 @@
|
|
181
215
|
take_recording();
|
182
216
|
}
|
183
217
|
if (!recording && stream) {
|
218
|
+
dispatch("close_stream");
|
184
219
|
stream.getTracks().forEach((track) => track.stop());
|
185
220
|
video_source.srcObject = null;
|
186
221
|
webcam_accessed = false;
|
222
|
+
window.setTimeout(() => {
|
223
|
+
value = null;
|
224
|
+
}, 500);
|
225
|
+
value = null;
|
187
226
|
}
|
188
227
|
}
|
189
228
|
|
@@ -192,7 +231,7 @@
|
|
192
231
|
if (video_source && !pending) {
|
193
232
|
take_picture();
|
194
233
|
}
|
195
|
-
},
|
234
|
+
}, stream_every * 1000);
|
196
235
|
}
|
197
236
|
|
198
237
|
let options_open = false;
|
@@ -225,12 +264,18 @@
|
|
225
264
|
</script>
|
226
265
|
|
227
266
|
<div class="wrap">
|
267
|
+
<StreamingBar {time_limit} />
|
228
268
|
<!-- svelte-ignore a11y-media-has-caption -->
|
229
269
|
<!-- need to suppress for video streaming https://github.com/sveltejs/svelte/issues/5967 -->
|
230
270
|
<video
|
231
271
|
bind:this={video_source}
|
232
272
|
class:flip={mirror_webcam}
|
233
|
-
class:hide={!webcam_accessed}
|
273
|
+
class:hide={!webcam_accessed || (webcam_accessed && !!value)}
|
274
|
+
/>
|
275
|
+
<!-- svelte-ignore a11y-missing-attribute -->
|
276
|
+
<img
|
277
|
+
src={value?.url}
|
278
|
+
class:hide={!webcam_accessed || (webcam_accessed && !value)}
|
234
279
|
/>
|
235
280
|
{#if !webcam_accessed}
|
236
281
|
<div
|
@@ -247,13 +292,26 @@
|
|
247
292
|
aria-label={mode === "image" ? "capture photo" : "start recording"}
|
248
293
|
>
|
249
294
|
{#if mode === "video" || streaming}
|
250
|
-
{#if
|
251
|
-
<div class="icon
|
252
|
-
<
|
295
|
+
{#if streaming && stream_state === "waiting"}
|
296
|
+
<div class="icon-with-text" style="width:var(--size-24);">
|
297
|
+
<div class="icon color-primary" title="spinner">
|
298
|
+
<Spinner />
|
299
|
+
</div>
|
300
|
+
{i18n("audio.waiting")}
|
301
|
+
</div>
|
302
|
+
{:else if (streaming && stream_state === "open") || (!streaming && recording)}
|
303
|
+
<div class="icon-with-text">
|
304
|
+
<div class="icon color-primary" title="stop recording">
|
305
|
+
<Square />
|
306
|
+
</div>
|
307
|
+
{i18n("audio.stop")}
|
253
308
|
</div>
|
254
309
|
{:else}
|
255
|
-
<div class="icon
|
256
|
-
<
|
310
|
+
<div class="icon-with-text">
|
311
|
+
<div class="icon color-primary" title="start recording">
|
312
|
+
<Circle />
|
313
|
+
</div>
|
314
|
+
{i18n("audio.record")}
|
257
315
|
</div>
|
258
316
|
{/if}
|
259
317
|
{:else}
|
@@ -335,6 +393,14 @@
|
|
335
393
|
color: var(--button-secondary-text-color);
|
336
394
|
}
|
337
395
|
|
396
|
+
.icon-with-text {
|
397
|
+
width: var(--size-20);
|
398
|
+
align-items: center;
|
399
|
+
margin: 0 var(--spacing-xl);
|
400
|
+
display: flex;
|
401
|
+
justify-content: space-evenly;
|
402
|
+
}
|
403
|
+
|
338
404
|
@media (--screen-md) {
|
339
405
|
button {
|
340
406
|
bottom: var(--size-4);
|
@@ -348,7 +414,6 @@
|
|
348
414
|
}
|
349
415
|
|
350
416
|
.icon {
|
351
|
-
opacity: 0.8;
|
352
417
|
width: 18px;
|
353
418
|
height: 18px;
|
354
419
|
display: flex;
|
@@ -356,9 +421,9 @@
|
|
356
421
|
align-items: center;
|
357
422
|
}
|
358
423
|
|
359
|
-
.
|
360
|
-
fill:
|
361
|
-
stroke:
|
424
|
+
.color-primary {
|
425
|
+
fill: var(--primary-600);
|
426
|
+
stroke: var(--primary-600);
|
362
427
|
}
|
363
428
|
|
364
429
|
.flip {
|