@gradio/image 0.9.10 → 0.9.11

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @gradio/image
2
2
 
3
+ ## 0.9.11
4
+
5
+ ### Fixes
6
+
7
+ - [#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!
8
+
9
+ ### Dependency updates
10
+
11
+ - @gradio/utils@0.3.1
12
+ - @gradio/atoms@0.6.2
13
+ - @gradio/statustracker@0.4.11
14
+ - @gradio/upload@0.8.4
15
+ - @gradio/client@0.15.1
16
+
3
17
  ## 0.9.10
4
18
 
5
19
  ### Dependency updates
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/image",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
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/atoms": "^0.6.2",
14
+ "@gradio/client": "^0.15.1",
13
15
  "@gradio/icons": "^0.3.4",
14
- "@gradio/client": "^0.15.0",
15
- "@gradio/utils": "^0.3.0",
16
- "@gradio/statustracker": "^0.4.10",
17
- "@gradio/atoms": "^0.6.1",
18
- "@gradio/upload": "^0.8.3",
16
+ "@gradio/statustracker": "^0.4.11",
17
+ "@gradio/upload": "^0.8.4",
18
+ "@gradio/utils": "^0.3.1",
19
19
  "@gradio/wasm": "^0.10.0"
20
20
  },
21
21
  "main_changeset": true,
@@ -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
- const size = {
37
- width: { ideal: 1920 },
38
- height: { ideal: 1440 }
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
- async function access_webcam(device_id?: string): Promise<void> {
41
- if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
42
- dispatch("error", i18n("image.no_webcam_support"));
43
- return;
44
- }
54
+
55
+ async function access_webcam(): Promise<void> {
45
56
  try {
46
- stream = await navigator.mediaDevices.getUserMedia({
47
- video: device_id ? { deviceId: { exact: device_id }, ...size } : size,
48
- audio: include_audio
49
- });
50
- video_source.srcObject = stream;
51
- video_source.muted = true;
52
- video_source.play();
53
- webcam_accessed = true;
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={select_source}
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 video_sources.length === 0}
284
+ {#if available_video_devices.length === 0}
270
285
  <option value="">{i18n("common.no_devices")}</option>
271
286
  {:else}
272
- {#each video_sources as source}
273
- <option on:click={() => selectVideoSource(source.deviceId)}>
274
- {source.label}
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
+ }