@gradio/video 0.20.2 → 0.20.4

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.
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
- import { createEventDispatcher } from "svelte";
3
- import { Upload, ModifyUpload } from "@gradio/upload";
2
+ import { Upload } from "@gradio/upload";
4
3
  import type { FileData, Client } from "@gradio/client";
5
4
  import { BlockLabel } from "@gradio/atoms";
6
5
  import { Webcam } from "@gradio/image";
@@ -10,72 +9,123 @@
10
9
  import Player from "./Player.svelte";
11
10
  import type { I18nFormatter } from "@gradio/utils";
12
11
  import { SelectSource } from "@gradio/atoms";
12
+ import type { Snippet } from "svelte";
13
13
 
14
- export let value: FileData | null = null;
15
- export let subtitle: FileData | null = null;
16
- export let sources:
17
- | ["webcam"]
18
- | ["upload"]
19
- | ["webcam", "upload"]
20
- | ["upload", "webcam"] = ["webcam", "upload"];
21
- export let label: string | undefined = undefined;
22
- export let show_download_button = false;
23
- export let show_label = true;
24
- export let webcam_options: WebcamOptions;
25
- export let include_audio: boolean;
26
- export let autoplay: boolean;
27
- export let root: string;
28
- export let i18n: I18nFormatter;
29
- export let active_source: "webcam" | "upload" = "webcam";
30
- export let handle_reset_value: () => void = () => {};
31
- export let max_file_size: number | null = null;
32
- export let upload: Client["upload"];
33
- export let stream_handler: Client["stream"];
34
- export let loop: boolean;
35
- export let uploading = false;
36
- export let upload_promise: Promise<any> | null = null;
37
- export let playback_position = 0;
38
-
39
- let has_change_history = false;
40
-
41
- const dispatch = createEventDispatcher<{
42
- change: FileData | null;
43
- clear?: never;
44
- play?: never;
45
- pause?: never;
46
- end?: never;
47
- drag: boolean;
48
- error: string;
49
- upload: FileData;
50
- start_recording?: never;
51
- stop_recording?: never;
52
- }>();
14
+ interface Props {
15
+ value?: FileData | null;
16
+ subtitle?: FileData | null;
17
+ sources?:
18
+ | ["webcam"]
19
+ | ["upload"]
20
+ | ["webcam", "upload"]
21
+ | ["upload", "webcam"];
22
+ label?: string;
23
+ show_download_button?: boolean;
24
+ show_label?: boolean;
25
+ webcam_options: WebcamOptions;
26
+ include_audio: boolean;
27
+ autoplay: boolean;
28
+ root: string;
29
+ i18n: I18nFormatter;
30
+ active_source?: "webcam" | "upload";
31
+ handle_reset_value?: () => void;
32
+ max_file_size?: number | null;
33
+ upload: Client["upload"];
34
+ stream_handler: Client["stream"];
35
+ loop: boolean;
36
+ uploading?: boolean;
37
+ upload_promise?: Promise<any> | null;
38
+ playback_position?: number;
39
+ buttons?: (string | CustomButtonType)[] | null;
40
+ on_custom_button_click?: ((id: number) => void) | null;
41
+ onchange?: (value: FileData | null) => void;
42
+ onclear?: () => void;
43
+ onplay?: () => void;
44
+ onpause?: () => void;
45
+ onend?: () => void;
46
+ ondrag?: (dragging: boolean) => void;
47
+ onerror?: (error: string) => void;
48
+ onupload?: (value: FileData) => void;
49
+ onstart_recording?: () => void;
50
+ onstop_recording?: () => void;
51
+ onstop?: () => void;
52
+ children?: Snippet;
53
+ }
54
+
55
+ import type { CustomButton as CustomButtonType } from "@gradio/utils";
56
+
57
+ let {
58
+ value = $bindable(null),
59
+ subtitle = null,
60
+ sources = ["webcam", "upload"],
61
+ label = undefined,
62
+ show_download_button = false,
63
+ show_label = true,
64
+ webcam_options,
65
+ include_audio,
66
+ autoplay,
67
+ root,
68
+ i18n,
69
+ active_source: initial_active_source = "webcam",
70
+ handle_reset_value = () => {},
71
+ max_file_size = null,
72
+ upload,
73
+ stream_handler,
74
+ loop,
75
+ uploading = $bindable(),
76
+ upload_promise = $bindable(),
77
+ playback_position = $bindable(),
78
+ buttons = null,
79
+ on_custom_button_click = null,
80
+ onchange,
81
+ onclear,
82
+ onplay,
83
+ onpause,
84
+ onend,
85
+ ondrag,
86
+ onerror,
87
+ onupload,
88
+ onstart_recording,
89
+ onstop_recording,
90
+ onstop,
91
+ children
92
+ }: Props = $props();
93
+
94
+ let has_change_history = $state(false);
95
+ let active_source = $derived.by(() => {
96
+ return initial_active_source ?? "webcam";
97
+ });
53
98
 
54
99
  function handle_load(detail: FileData | null): void {
55
100
  value = detail;
56
- dispatch("change", detail);
57
- dispatch("upload", detail!);
101
+ onchange?.(detail);
102
+ if (detail) {
103
+ onupload?.(detail);
104
+ }
58
105
  }
59
106
 
60
107
  function handle_clear(): void {
61
108
  value = null;
62
- dispatch("change", null);
63
- dispatch("clear");
109
+ onchange?.(null);
110
+ onclear?.();
64
111
  }
65
112
 
66
113
  function handle_change(video: FileData): void {
67
114
  has_change_history = true;
68
- dispatch("change", video);
115
+ onchange?.(video);
69
116
  }
70
117
 
71
118
  function handle_capture({
72
119
  detail
73
120
  }: CustomEvent<FileData | any | null>): void {
74
- dispatch("change", detail);
121
+ onchange?.(detail);
75
122
  }
76
123
 
77
- let dragging = false;
78
- $: dispatch("drag", dragging);
124
+ let dragging = $state(false);
125
+
126
+ $effect(() => {
127
+ ondrag?.(dragging);
128
+ });
79
129
  </script>
80
130
 
81
131
  <BlockLabel {show_label} Icon={Video} label={label || "Video"} />
@@ -90,13 +140,15 @@
90
140
  filetype="video/x-m4v,video/*"
91
141
  onload={handle_load}
92
142
  {max_file_size}
93
- onerror={(detail) => dispatch("error", detail)}
143
+ onerror={(detail) => onerror?.(detail)}
94
144
  {root}
95
145
  {upload}
96
146
  {stream_handler}
97
147
  aria_label={i18n("video.drop_to_upload")}
98
148
  >
99
- <slot />
149
+ {#if children}
150
+ {@render children()}
151
+ {/if}
100
152
  </Upload>
101
153
  {:else if active_source === "webcam"}
102
154
  <Webcam
@@ -105,10 +157,10 @@
105
157
  webcam_constraints={webcam_options.constraints}
106
158
  {include_audio}
107
159
  mode="video"
108
- on:error
160
+ on:error={({ detail }) => onerror?.(detail)}
109
161
  on:capture={handle_capture}
110
- on:start_recording
111
- on:stop_recording
162
+ on:start_recording={() => onstart_recording?.()}
163
+ on:stop_recording={() => onstop_recording?.()}
112
164
  {i18n}
113
165
  {upload}
114
166
  stream_every={1}
@@ -125,11 +177,11 @@
125
177
  src={value.url}
126
178
  subtitle={subtitle?.url}
127
179
  is_stream={false}
128
- on:play
129
- on:pause
130
- on:stop
131
- on:end
132
- on:error
180
+ onplay={() => onplay?.()}
181
+ onpause={() => onpause?.()}
182
+ onstop={() => onstop?.()}
183
+ onend={() => onend?.()}
184
+ onerror={(error) => onerror?.(error)}
133
185
  mirror={webcam_options.mirror && active_source === "webcam"}
134
186
  {label}
135
187
  {handle_change}
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { createEventDispatcher, onMount, onDestroy } from "svelte";
2
+ import { onMount, onDestroy } from "svelte";
3
3
  import { Play, Pause, Maximize, Undo } from "@gradio/icons";
4
4
  import Video from "./Video.svelte";
5
5
  import VideoControls from "./VideoControls.svelte";
@@ -10,41 +10,72 @@
10
10
  import { format_time } from "@gradio/utils";
11
11
  import type { I18nFormatter } from "@gradio/utils";
12
12
 
13
- export let root = "";
14
- export let src: string;
15
- export let subtitle: string | null = null;
16
- export let mirror: boolean;
17
- export let autoplay: boolean;
18
- export let loop: boolean;
19
- export let label = "test";
20
- export let interactive = false;
21
- export let handle_change: (video: FileData) => void = () => {};
22
- export let handle_reset_value: () => void = () => {};
23
- export let upload: Client["upload"];
24
- export let is_stream: boolean | undefined;
25
- export let i18n: I18nFormatter;
26
- export let show_download_button = false;
27
- export let value: FileData | null = null;
28
- export let handle_clear: () => void = () => {};
29
- export let has_change_history = false;
30
- export let playback_position = 0;
31
-
32
- const dispatch = createEventDispatcher<{
33
- play: undefined;
34
- pause: undefined;
35
- stop: undefined;
36
- end: undefined;
37
- clear: undefined;
38
- }>();
39
-
40
- let time = 0;
41
- let duration: number;
42
- let paused = true;
43
- let video: HTMLVideoElement;
44
- let processingVideo = false;
45
- let show_volume_slider = false;
46
- let current_volume = 1;
47
- let is_fullscreen = false;
13
+ interface Props {
14
+ root?: string;
15
+ src: string;
16
+ subtitle?: string | null;
17
+ mirror: boolean;
18
+ autoplay: boolean;
19
+ loop: boolean;
20
+ label?: string;
21
+ interactive?: boolean;
22
+ handle_change?: (video: FileData) => void;
23
+ handle_reset_value?: () => void;
24
+ upload: Client["upload"];
25
+ is_stream?: boolean;
26
+ i18n: I18nFormatter;
27
+ show_download_button?: boolean;
28
+ value?: FileData | null;
29
+ handle_clear?: () => void;
30
+ has_change_history?: boolean;
31
+ playback_position?: number;
32
+ onplay?: () => void;
33
+ onpause?: () => void;
34
+ onstop?: () => void;
35
+ onend?: () => void;
36
+ onerror?: (error: string) => void;
37
+ onloadstart?: () => void;
38
+ onloadeddata?: () => void;
39
+ onloadedmetadata?: () => void;
40
+ }
41
+
42
+ let {
43
+ root = "",
44
+ src,
45
+ subtitle = null,
46
+ mirror,
47
+ autoplay,
48
+ loop,
49
+ label = "test",
50
+ interactive = false,
51
+ handle_change = () => {},
52
+ handle_reset_value = () => {},
53
+ upload,
54
+ is_stream = undefined,
55
+ i18n,
56
+ show_download_button = false,
57
+ value = null,
58
+ handle_clear = () => {},
59
+ has_change_history = false,
60
+ playback_position = $bindable(),
61
+ onplay,
62
+ onpause,
63
+ onstop,
64
+ onend,
65
+ onerror,
66
+ onloadstart,
67
+ onloadeddata,
68
+ onloadedmetadata
69
+ }: Props = $props();
70
+
71
+ let time = $state(0);
72
+ let duration = $state<number>(0);
73
+ let paused = $state(true);
74
+ let video = $state<HTMLVideoElement>();
75
+ let processingVideo = $state(false);
76
+ let show_volume_slider = $state(false);
77
+ let current_volume = $state(1);
78
+ let is_fullscreen = $state(false);
48
79
 
49
80
  function handleMove(e: TouchEvent | MouseEvent): void {
50
81
  if (!duration) return;
@@ -67,6 +98,7 @@
67
98
  }
68
99
 
69
100
  async function play_pause(): Promise<void> {
101
+ if (!video) return;
70
102
  if (document.fullscreenElement != video) {
71
103
  const isPlaying =
72
104
  video.currentTime > 0 &&
@@ -81,6 +113,7 @@
81
113
  }
82
114
 
83
115
  function handle_click(e: MouseEvent): void {
116
+ if (!duration) return;
84
117
  const { left, right } = (
85
118
  e.currentTarget as HTMLProgressElement
86
119
  ).getBoundingClientRect();
@@ -88,8 +121,8 @@
88
121
  }
89
122
 
90
123
  function handle_end(): void {
91
- dispatch("stop");
92
- dispatch("end");
124
+ onstop?.();
125
+ onend?.();
93
126
  }
94
127
 
95
128
  const handle_trim_video = async (videoBlob: Blob): Promise<void> => {
@@ -101,6 +134,7 @@
101
134
  };
102
135
 
103
136
  function open_full_screen(): void {
137
+ if (!video) return;
104
138
  if (!is_fullscreen) {
105
139
  video.requestFullscreen();
106
140
  } else {
@@ -140,30 +174,41 @@
140
174
  }
141
175
  });
142
176
 
143
- $: if (video && video !== previous_video) {
144
- if (previous_video) {
145
- previous_video.removeEventListener("volumechange", handleVolumeChange);
177
+ $effect(() => {
178
+ if (video && video !== previous_video) {
179
+ if (previous_video) {
180
+ previous_video.removeEventListener("volumechange", handleVolumeChange);
181
+ }
182
+ video.addEventListener("volumechange", handleVolumeChange);
183
+ previous_video = video;
146
184
  }
147
- video.addEventListener("volumechange", handleVolumeChange);
148
- previous_video = video;
149
- }
185
+ });
150
186
 
151
- $: time = time || 0;
152
- $: duration = duration || 0;
153
- $: playback_position = time;
154
- $: if (playback_position !== time && video) {
155
- video.currentTime = playback_position;
156
- }
157
- $: if (video && !is_fullscreen) {
158
- if (Math.abs(video.volume - current_volume) > VOLUME_EPSILON) {
159
- video.volume = current_volume;
160
- last_synced_volume = current_volume;
187
+ $effect(() => {
188
+ playback_position = time;
189
+ });
190
+
191
+ $effect(() => {
192
+ if (playback_position !== time && video) {
193
+ video.currentTime = playback_position;
161
194
  }
162
- video.controls = false;
163
- }
164
- $: if (video && is_fullscreen) {
165
- last_synced_volume = video.volume;
166
- }
195
+ });
196
+
197
+ $effect(() => {
198
+ if (video && !is_fullscreen) {
199
+ if (Math.abs(video.volume - current_volume) > VOLUME_EPSILON) {
200
+ video.volume = current_volume;
201
+ last_synced_volume = current_volume;
202
+ }
203
+ video.controls = false;
204
+ }
205
+ });
206
+
207
+ $effect(() => {
208
+ if (video && is_fullscreen) {
209
+ last_synced_volume = video.volume;
210
+ }
211
+ });
167
212
  </script>
168
213
 
169
214
  <div class="wrap">
@@ -175,20 +220,20 @@
175
220
  {loop}
176
221
  {is_stream}
177
222
  controls={is_fullscreen}
178
- on:click={play_pause}
179
- on:play
180
- on:pause
181
- on:error
182
- on:ended={handle_end}
223
+ onclick={play_pause}
224
+ onplay={() => onplay?.()}
225
+ onpause={() => onpause?.()}
226
+ onerror={(error) => onerror?.(error)}
227
+ onended={handle_end}
183
228
  bind:currentTime={time}
184
229
  bind:duration
185
230
  bind:paused
186
231
  bind:node={video}
187
232
  data-testid={`${label}-player`}
188
233
  {processingVideo}
189
- on:loadstart
190
- on:loadeddata
191
- on:loadedmetadata
234
+ onloadstart={() => onloadstart?.()}
235
+ onloadeddata={() => onloadeddata?.()}
236
+ onloadedmetadata={() => onloadedmetadata?.()}
192
237
  >
193
238
  <track kind="captions" src={subtitle} default />
194
239
  </Video>
@@ -201,8 +246,8 @@
201
246
  tabindex="0"
202
247
  class="icon"
203
248
  aria-label="play-pause-replay-button"
204
- on:click={play_pause}
205
- on:keydown={play_pause}
249
+ onclick={play_pause}
250
+ onkeydown={play_pause}
206
251
  >
207
252
  {#if time === duration}
208
253
  <Undo />
@@ -216,21 +261,28 @@
216
261
  <span class="time">{format_time(time)} / {format_time(duration)}</span>
217
262
 
218
263
  <!-- TODO: implement accessible video timeline for 4.0 -->
219
- <!-- svelte-ignore a11y-click-events-have-key-events -->
220
- <!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
264
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
265
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
221
266
  <progress
222
267
  value={time / duration || 0}
223
- on:mousemove={handleMove}
224
- on:touchmove|preventDefault={handleMove}
225
- on:click|stopPropagation|preventDefault={handle_click}
226
- />
268
+ onmousemove={handleMove}
269
+ ontouchmove={(e) => {
270
+ e.preventDefault();
271
+ handleMove(e);
272
+ }}
273
+ onclick={(e) => {
274
+ e.stopPropagation();
275
+ e.preventDefault();
276
+ handle_click(e);
277
+ }}
278
+ ></progress>
227
279
 
228
280
  <div class="volume-control-wrapper">
229
281
  <button
230
282
  class="icon volume-button"
231
283
  style:color={show_volume_slider ? "var(--color-accent)" : "white"}
232
284
  aria-label="Adjust volume"
233
- on:click={() => (show_volume_slider = !show_volume_slider)}
285
+ onclick={() => (show_volume_slider = !show_volume_slider)}
234
286
  >
235
287
  <VolumeLevels currentVolume={current_volume} />
236
288
  </button>
@@ -246,8 +298,8 @@
246
298
  tabindex="0"
247
299
  class="icon"
248
300
  aria-label="full-screen"
249
- on:click={open_full_screen}
250
- on:keypress={open_full_screen}
301
+ onclick={open_full_screen}
302
+ onkeypress={open_full_screen}
251
303
  >
252
304
  <Maximize />
253
305
  </div>
@@ -1,31 +1,71 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLVideoAttributes } from "svelte/elements";
3
- import { createEventDispatcher } from "svelte";
4
3
  import { loaded } from "./utils";
4
+ import type { Snippet } from "svelte";
5
5
 
6
6
  import Hls from "hls.js";
7
7
 
8
- export let src: HTMLVideoAttributes["src"] = undefined;
9
-
10
- export let muted: HTMLVideoAttributes["muted"] = undefined;
11
- export let playsinline: HTMLVideoAttributes["playsinline"] = undefined;
12
- export let preload: HTMLVideoAttributes["preload"] = undefined;
13
- export let autoplay: HTMLVideoAttributes["autoplay"] = undefined;
14
- export let controls: HTMLVideoAttributes["controls"] = undefined;
15
-
16
- export let currentTime: number | undefined = undefined;
17
- export let duration: number | undefined = undefined;
18
- export let paused: boolean | undefined = undefined;
19
-
20
- export let node: HTMLVideoElement | undefined = undefined;
21
- export let loop: boolean;
22
- export let is_stream;
23
-
24
- export let processingVideo = false;
25
-
26
- let stream_active = false;
8
+ interface Props {
9
+ src?: HTMLVideoAttributes["src"];
10
+ muted?: HTMLVideoAttributes["muted"];
11
+ playsinline?: HTMLVideoAttributes["playsinline"];
12
+ preload?: HTMLVideoAttributes["preload"];
13
+ autoplay?: HTMLVideoAttributes["autoplay"];
14
+ controls?: HTMLVideoAttributes["controls"];
15
+ currentTime?: number;
16
+ duration?: number;
17
+ paused?: boolean;
18
+ node?: HTMLVideoElement;
19
+ loop: boolean;
20
+ is_stream: boolean;
21
+ processingVideo?: boolean;
22
+ onloadeddata?: () => void;
23
+ onclick?: () => void;
24
+ onplay?: () => void;
25
+ onpause?: () => void;
26
+ onended?: () => void;
27
+ onmouseover?: () => void;
28
+ onmouseout?: () => void;
29
+ onfocus?: () => void;
30
+ onblur?: () => void;
31
+ onerror?: (error: string) => void;
32
+ onloadstart?: () => void;
33
+ onloadedmetadata?: () => void;
34
+ "data-testid"?: string;
35
+ children?: Snippet;
36
+ }
27
37
 
28
- const dispatch = createEventDispatcher();
38
+ let {
39
+ src = undefined,
40
+ muted = undefined,
41
+ playsinline = undefined,
42
+ preload = undefined,
43
+ autoplay = undefined,
44
+ controls = undefined,
45
+ currentTime = $bindable(undefined),
46
+ duration = $bindable(undefined),
47
+ paused = $bindable(undefined),
48
+ node = $bindable(undefined),
49
+ loop,
50
+ is_stream,
51
+ processingVideo = false,
52
+ onloadeddata,
53
+ onclick,
54
+ onplay,
55
+ onpause,
56
+ onended,
57
+ onmouseover,
58
+ onmouseout,
59
+ onfocus,
60
+ onblur,
61
+ onerror,
62
+ onloadstart,
63
+ onloadedmetadata,
64
+ "data-testid": dataTestid,
65
+ children
66
+ }: Props = $props();
67
+
68
+ let stream_active = $state(false);
29
69
 
30
70
  function load_stream(
31
71
  src: string | null | undefined,
@@ -70,11 +110,16 @@
70
110
  }
71
111
  }
72
112
 
73
- $: (src, (stream_active = false));
113
+ $effect(() => {
114
+ src;
115
+ stream_active = false;
116
+ });
74
117
 
75
- $: if (node && src && is_stream) {
76
- load_stream(src, is_stream, node);
77
- }
118
+ $effect(() => {
119
+ if (node && src && is_stream) {
120
+ load_stream(src, is_stream, node);
121
+ }
122
+ });
78
123
  </script>
79
124
 
80
125
  <!--
@@ -97,28 +142,29 @@ Then, even when `controls` is false, the compiled DOM would be `<video controls=
97
142
  {autoplay}
98
143
  {controls}
99
144
  {loop}
100
- on:loadeddata={dispatch.bind(null, "loadeddata")}
101
- on:click={dispatch.bind(null, "click")}
102
- on:play={dispatch.bind(null, "play")}
103
- on:pause={dispatch.bind(null, "pause")}
104
- on:ended={dispatch.bind(null, "ended")}
105
- on:mouseover={dispatch.bind(null, "mouseover")}
106
- on:mouseout={dispatch.bind(null, "mouseout")}
107
- on:focus={dispatch.bind(null, "focus")}
108
- on:blur={dispatch.bind(null, "blur")}
109
- on:error={dispatch.bind(null, "error", "Video not playable")}
110
- on:loadstart
111
- on:loadeddata
112
- on:loadedmetadata
145
+ onloadeddata={() => onloadeddata?.()}
146
+ onclick={() => onclick?.()}
147
+ onplay={() => onplay?.()}
148
+ onpause={() => onpause?.()}
149
+ onended={() => onended?.()}
150
+ onmouseover={() => onmouseover?.()}
151
+ onmouseout={() => onmouseout?.()}
152
+ onfocus={() => onfocus?.()}
153
+ onblur={() => onblur?.()}
154
+ onerror={() => onerror?.("Video not playable")}
155
+ onloadstart={() => onloadstart?.()}
156
+ onloadedmetadata={() => onloadedmetadata?.()}
113
157
  bind:currentTime
114
158
  bind:duration
115
159
  bind:paused
116
160
  bind:this={node}
117
161
  use:loaded={{ autoplay: autoplay ?? false }}
118
- data-testid={$$props["data-testid"]}
162
+ data-testid={dataTestid}
119
163
  crossorigin="anonymous"
120
164
  >
121
- <slot />
165
+ {#if children}
166
+ {@render children()}
167
+ {/if}
122
168
  </video>
123
169
 
124
170
  <style>