@gradio/image 0.9.10 → 0.9.12
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 +25 -0
- package/package.json +7 -7
- package/shared/Webcam.svelte +59 -41
- package/shared/stream_utils.test.ts +134 -0
- package/shared/stream_utils.ts +49 -0
package/CHANGELOG.md
CHANGED
@@ -1,5 +1,30 @@
|
|
1
1
|
# @gradio/image
|
2
2
|
|
3
|
+
## 0.9.12
|
4
|
+
|
5
|
+
### Dependency updates
|
6
|
+
|
7
|
+
- @gradio/utils@0.3.2
|
8
|
+
- @gradio/statustracker@0.4.12
|
9
|
+
- @gradio/client@0.16.0
|
10
|
+
- @gradio/upload@0.8.5
|
11
|
+
- @gradio/atoms@0.7.0
|
12
|
+
- @gradio/icons@0.4.0
|
13
|
+
|
14
|
+
## 0.9.11
|
15
|
+
|
16
|
+
### Fixes
|
17
|
+
|
18
|
+
- [#7754](https://github.com/gradio-app/gradio/pull/7754) [`057d171`](https://github.com/gradio-app/gradio/commit/057d171c717737a522b55b0d66962f9c62dd87c3) - Correctly handle device selection in `Image` and `ImageEditor`. Thanks @hannahblair!
|
19
|
+
|
20
|
+
### Dependency updates
|
21
|
+
|
22
|
+
- @gradio/utils@0.3.1
|
23
|
+
- @gradio/atoms@0.6.2
|
24
|
+
- @gradio/statustracker@0.4.11
|
25
|
+
- @gradio/upload@0.8.4
|
26
|
+
- @gradio/client@0.15.1
|
27
|
+
|
3
28
|
## 0.9.10
|
4
29
|
|
5
30
|
### Dependency updates
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@gradio/image",
|
3
|
-
"version": "0.9.
|
3
|
+
"version": "0.9.12",
|
4
4
|
"description": "Gradio UI packages",
|
5
5
|
"type": "module",
|
6
6
|
"author": "",
|
@@ -10,12 +10,12 @@
|
|
10
10
|
"cropperjs": "^1.5.12",
|
11
11
|
"lazy-brush": "^1.0.1",
|
12
12
|
"resize-observer-polyfill": "^1.5.1",
|
13
|
-
"@gradio/
|
14
|
-
"@gradio/client": "^0.
|
15
|
-
"@gradio/
|
16
|
-
"@gradio/statustracker": "^0.4.
|
17
|
-
"@gradio/
|
18
|
-
"@gradio/
|
13
|
+
"@gradio/atoms": "^0.7.0",
|
14
|
+
"@gradio/client": "^0.16.0",
|
15
|
+
"@gradio/icons": "^0.4.0",
|
16
|
+
"@gradio/statustracker": "^0.4.12",
|
17
|
+
"@gradio/upload": "^0.8.5",
|
18
|
+
"@gradio/utils": "^0.3.2",
|
19
19
|
"@gradio/wasm": "^0.10.0"
|
20
20
|
},
|
21
21
|
"main_changeset": true,
|
package/shared/Webcam.svelte
CHANGED
@@ -1,19 +1,21 @@
|
|
1
1
|
<script lang="ts">
|
2
2
|
import { createEventDispatcher, onMount } from "svelte";
|
3
|
-
import {
|
4
|
-
Camera,
|
5
|
-
Circle,
|
6
|
-
Square,
|
7
|
-
DropdownArrow,
|
8
|
-
Webcam as WebcamIcon
|
9
|
-
} from "@gradio/icons";
|
3
|
+
import { Camera, Circle, Square, DropdownArrow } from "@gradio/icons";
|
10
4
|
import type { I18nFormatter } from "@gradio/utils";
|
11
5
|
import type { FileData } from "@gradio/client";
|
12
6
|
import { prepare_files, upload } from "@gradio/client";
|
13
7
|
import WebcamPermissions from "./WebcamPermissions.svelte";
|
14
8
|
import { fade } from "svelte/transition";
|
9
|
+
import {
|
10
|
+
get_devices,
|
11
|
+
get_video_stream,
|
12
|
+
set_available_devices
|
13
|
+
} from "./stream_utils";
|
15
14
|
|
16
15
|
let video_source: HTMLVideoElement;
|
16
|
+
let available_video_devices: MediaDeviceInfo[] = [];
|
17
|
+
let selected_device: MediaDeviceInfo | null = null;
|
18
|
+
|
17
19
|
let canvas: HTMLCanvasElement;
|
18
20
|
export let streaming = false;
|
19
21
|
export let pending = false;
|
@@ -33,24 +35,48 @@
|
|
33
35
|
}>();
|
34
36
|
|
35
37
|
onMount(() => (canvas = document.createElement("canvas")));
|
36
|
-
|
37
|
-
|
38
|
-
|
38
|
+
|
39
|
+
const handle_device_change = async (event: InputEvent): Promise<void> => {
|
40
|
+
const target = event.target as HTMLInputElement;
|
41
|
+
const device_id = target.value;
|
42
|
+
|
43
|
+
await get_video_stream(include_audio, video_source, device_id).then(
|
44
|
+
async (local_stream) => {
|
45
|
+
stream = local_stream;
|
46
|
+
selected_device =
|
47
|
+
available_video_devices.find(
|
48
|
+
(device) => device.deviceId === device_id
|
49
|
+
) || null;
|
50
|
+
options_open = false;
|
51
|
+
}
|
52
|
+
);
|
39
53
|
};
|
40
|
-
|
41
|
-
|
42
|
-
dispatch("error", i18n("image.no_webcam_support"));
|
43
|
-
return;
|
44
|
-
}
|
54
|
+
|
55
|
+
async function access_webcam(): Promise<void> {
|
45
56
|
try {
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
57
|
+
get_video_stream(include_audio, video_source)
|
58
|
+
.then(async (local_stream) => {
|
59
|
+
webcam_accessed = true;
|
60
|
+
available_video_devices = await get_devices();
|
61
|
+
stream = local_stream;
|
62
|
+
})
|
63
|
+
.then(() => set_available_devices(available_video_devices))
|
64
|
+
.then((devices) => {
|
65
|
+
available_video_devices = devices;
|
66
|
+
|
67
|
+
const used_devices = stream
|
68
|
+
.getTracks()
|
69
|
+
.map((track) => track.getSettings()?.deviceId)[0];
|
70
|
+
|
71
|
+
selected_device = used_devices
|
72
|
+
? devices.find((device) => device.deviceId === used_devices) ||
|
73
|
+
available_video_devices[0]
|
74
|
+
: available_video_devices[0];
|
75
|
+
});
|
76
|
+
|
77
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
78
|
+
dispatch("error", i18n("image.no_webcam_support"));
|
79
|
+
}
|
54
80
|
} catch (err) {
|
55
81
|
if (err instanceof DOMException && err.name == "NotAllowedError") {
|
56
82
|
dispatch("error", i18n("image.allow_webcam_access"));
|
@@ -169,18 +195,6 @@
|
|
169
195
|
}, 500);
|
170
196
|
}
|
171
197
|
|
172
|
-
async function select_source(): Promise<void> {
|
173
|
-
const devices = await navigator.mediaDevices.enumerateDevices();
|
174
|
-
video_sources = devices.filter((device) => device.kind === "videoinput");
|
175
|
-
options_open = true;
|
176
|
-
}
|
177
|
-
|
178
|
-
let video_sources: MediaDeviceInfo[] = [];
|
179
|
-
async function selectVideoSource(device_id: string): Promise<void> {
|
180
|
-
await access_webcam(device_id);
|
181
|
-
options_open = false;
|
182
|
-
}
|
183
|
-
|
184
198
|
let options_open = false;
|
185
199
|
|
186
200
|
export function click_outside(node: Node, cb: any): any {
|
@@ -247,18 +261,19 @@
|
|
247
261
|
{#if !recording}
|
248
262
|
<button
|
249
263
|
class="icon"
|
250
|
-
on:click={
|
264
|
+
on:click={() => (options_open = true)}
|
251
265
|
aria-label="select input source"
|
252
266
|
>
|
253
267
|
<DropdownArrow />
|
254
268
|
</button>
|
255
269
|
{/if}
|
256
270
|
</div>
|
257
|
-
{#if options_open}
|
271
|
+
{#if options_open && selected_device}
|
258
272
|
<select
|
259
273
|
class="select-wrap"
|
260
274
|
aria-label="select source"
|
261
275
|
use:click_outside={handle_click_outside}
|
276
|
+
on:change={handle_device_change}
|
262
277
|
>
|
263
278
|
<button
|
264
279
|
class="inset-icon"
|
@@ -266,12 +281,15 @@
|
|
266
281
|
>
|
267
282
|
<DropdownArrow />
|
268
283
|
</button>
|
269
|
-
{#if
|
284
|
+
{#if available_video_devices.length === 0}
|
270
285
|
<option value="">{i18n("common.no_devices")}</option>
|
271
286
|
{:else}
|
272
|
-
{#each
|
273
|
-
<option
|
274
|
-
{
|
287
|
+
{#each available_video_devices as device}
|
288
|
+
<option
|
289
|
+
value={device.deviceId}
|
290
|
+
selected={selected_device.deviceId === device.deviceId}
|
291
|
+
>
|
292
|
+
{device.label}
|
275
293
|
</option>
|
276
294
|
{/each}
|
277
295
|
{/if}
|
@@ -0,0 +1,134 @@
|
|
1
|
+
import { describe, expect, vi } from "vitest";
|
2
|
+
import {
|
3
|
+
get_devices,
|
4
|
+
get_video_stream,
|
5
|
+
set_available_devices,
|
6
|
+
set_local_stream
|
7
|
+
} from "./stream_utils";
|
8
|
+
import * as stream_utils from "./stream_utils";
|
9
|
+
|
10
|
+
let test_device: MediaDeviceInfo = {
|
11
|
+
deviceId: "test-device",
|
12
|
+
kind: "videoinput",
|
13
|
+
label: "Test Device",
|
14
|
+
groupId: "camera",
|
15
|
+
toJSON: () => ({
|
16
|
+
deviceId: "test-device",
|
17
|
+
kind: "videoinput",
|
18
|
+
label: "Test Device",
|
19
|
+
groupId: "camera"
|
20
|
+
})
|
21
|
+
};
|
22
|
+
|
23
|
+
const mock_enumerateDevices = vi.fn(async () => {
|
24
|
+
return new Promise<MediaDeviceInfo[]>((resolve) => {
|
25
|
+
resolve([test_device]);
|
26
|
+
});
|
27
|
+
});
|
28
|
+
const mock_getUserMedia = vi.fn(async () => {
|
29
|
+
return new Promise<MediaStream>((resolve) => {
|
30
|
+
resolve(new MediaStream());
|
31
|
+
});
|
32
|
+
});
|
33
|
+
|
34
|
+
window.MediaStream = vi.fn().mockImplementation(() => ({}));
|
35
|
+
|
36
|
+
Object.defineProperty(global.navigator, "mediaDevices", {
|
37
|
+
value: {
|
38
|
+
getUserMedia: mock_getUserMedia,
|
39
|
+
enumerateDevices: mock_enumerateDevices
|
40
|
+
}
|
41
|
+
});
|
42
|
+
|
43
|
+
describe("stream_utils", () => {
|
44
|
+
test("get_devices should enumerate media devices", async () => {
|
45
|
+
const devices = await get_devices();
|
46
|
+
expect(devices).toEqual([test_device]);
|
47
|
+
});
|
48
|
+
|
49
|
+
test("set_local_stream should set the local stream to the video source", () => {
|
50
|
+
const mock_stream = {}; // mocked MediaStream obj as it's not available in a node env
|
51
|
+
|
52
|
+
const mock_video_source = {
|
53
|
+
srcObject: null,
|
54
|
+
muted: false,
|
55
|
+
play: vi.fn()
|
56
|
+
};
|
57
|
+
|
58
|
+
// @ts-ignore
|
59
|
+
set_local_stream(mock_stream, mock_video_source);
|
60
|
+
|
61
|
+
expect(mock_video_source.srcObject).toEqual(mock_stream);
|
62
|
+
expect(mock_video_source.muted).toBeTruthy();
|
63
|
+
expect(mock_video_source.play).toHaveBeenCalled();
|
64
|
+
});
|
65
|
+
|
66
|
+
test("get_video_stream requests user media with the correct constraints and sets the local stream", async () => {
|
67
|
+
const mock_video_source = document.createElement("video");
|
68
|
+
const mock_stream = new MediaStream();
|
69
|
+
|
70
|
+
global.navigator.mediaDevices.getUserMedia = vi
|
71
|
+
.fn()
|
72
|
+
.mockResolvedValue(mock_stream);
|
73
|
+
|
74
|
+
await get_video_stream(true, mock_video_source);
|
75
|
+
|
76
|
+
expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({
|
77
|
+
video: { width: { ideal: 1920 }, height: { ideal: 1440 } },
|
78
|
+
audio: true
|
79
|
+
});
|
80
|
+
|
81
|
+
const spy_set_local_stream = vi.spyOn(stream_utils, "set_local_stream");
|
82
|
+
stream_utils.set_local_stream(mock_stream, mock_video_source);
|
83
|
+
|
84
|
+
expect(spy_set_local_stream).toHaveBeenCalledWith(
|
85
|
+
mock_stream,
|
86
|
+
mock_video_source
|
87
|
+
);
|
88
|
+
spy_set_local_stream.mockRestore();
|
89
|
+
});
|
90
|
+
|
91
|
+
test("set_available_devices should return only video input devices", () => {
|
92
|
+
const mockDevices: MediaDeviceInfo[] = [
|
93
|
+
{
|
94
|
+
deviceId: "camera1",
|
95
|
+
kind: "videoinput",
|
96
|
+
label: "Camera 1",
|
97
|
+
groupId: "camera",
|
98
|
+
toJSON: () => ({
|
99
|
+
deviceId: "camera1",
|
100
|
+
kind: "videoinput",
|
101
|
+
label: "Camera 1",
|
102
|
+
groupId: "camera"
|
103
|
+
})
|
104
|
+
},
|
105
|
+
{
|
106
|
+
deviceId: "camera2",
|
107
|
+
kind: "videoinput",
|
108
|
+
label: "Camera 2",
|
109
|
+
groupId: "camera",
|
110
|
+
toJSON: () => ({
|
111
|
+
deviceId: "camera2",
|
112
|
+
kind: "videoinput",
|
113
|
+
label: "Camera 2",
|
114
|
+
groupId: "camera"
|
115
|
+
})
|
116
|
+
},
|
117
|
+
{
|
118
|
+
deviceId: "audio1",
|
119
|
+
kind: "audioinput",
|
120
|
+
label: "Audio 2",
|
121
|
+
groupId: "audio",
|
122
|
+
toJSON: () => ({
|
123
|
+
deviceId: "audio1",
|
124
|
+
kind: "audioinput",
|
125
|
+
label: "Audio 2",
|
126
|
+
groupId: "audio"
|
127
|
+
})
|
128
|
+
}
|
129
|
+
];
|
130
|
+
|
131
|
+
const videoDevices = set_available_devices(mockDevices);
|
132
|
+
expect(videoDevices).toEqual(mockDevices.splice(0, 2));
|
133
|
+
});
|
134
|
+
});
|
@@ -0,0 +1,49 @@
|
|
1
|
+
export function get_devices(): Promise<MediaDeviceInfo[]> {
|
2
|
+
return navigator.mediaDevices.enumerateDevices();
|
3
|
+
}
|
4
|
+
|
5
|
+
export function handle_error(error: string): void {
|
6
|
+
throw new Error(error);
|
7
|
+
}
|
8
|
+
|
9
|
+
export function set_local_stream(
|
10
|
+
local_stream: MediaStream | null,
|
11
|
+
video_source: HTMLVideoElement
|
12
|
+
): void {
|
13
|
+
video_source.srcObject = local_stream;
|
14
|
+
video_source.muted = true;
|
15
|
+
video_source.play();
|
16
|
+
}
|
17
|
+
|
18
|
+
export async function get_video_stream(
|
19
|
+
include_audio: boolean,
|
20
|
+
video_source: HTMLVideoElement,
|
21
|
+
device_id?: string
|
22
|
+
): Promise<MediaStream> {
|
23
|
+
const size = {
|
24
|
+
width: { ideal: 1920 },
|
25
|
+
height: { ideal: 1440 }
|
26
|
+
};
|
27
|
+
|
28
|
+
const constraints = {
|
29
|
+
video: device_id ? { deviceId: { exact: device_id }, ...size } : size,
|
30
|
+
audio: include_audio
|
31
|
+
};
|
32
|
+
|
33
|
+
return navigator.mediaDevices
|
34
|
+
.getUserMedia(constraints)
|
35
|
+
.then((local_stream: MediaStream) => {
|
36
|
+
set_local_stream(local_stream, video_source);
|
37
|
+
return local_stream;
|
38
|
+
});
|
39
|
+
}
|
40
|
+
|
41
|
+
export function set_available_devices(
|
42
|
+
devices: MediaDeviceInfo[]
|
43
|
+
): MediaDeviceInfo[] {
|
44
|
+
const cameras = devices.filter(
|
45
|
+
(device: MediaDeviceInfo) => device.kind === "videoinput"
|
46
|
+
);
|
47
|
+
|
48
|
+
return cameras;
|
49
|
+
}
|