@gradio/video 0.10.4 → 0.11.0-beta.2

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,42 @@
1
1
  # @gradio/video
2
2
 
3
+ ## 0.11.0-beta.2
4
+
5
+ ### Features
6
+
7
+ - [#9339](https://github.com/gradio-app/gradio/pull/9339) [`4c8c6f2`](https://github.com/gradio-app/gradio/commit/4c8c6f2fe603081941c5fdc43f48a0632b9f31ad) - Ssr part 2. Thanks @pngwn!
8
+ - [#9250](https://github.com/gradio-app/gradio/pull/9250) [`350b0a5`](https://github.com/gradio-app/gradio/commit/350b0a5cafb9176f914f62e7c90de51d4352cc77) - Improve Icon Button consistency. Thanks @hannahblair!
9
+ - [#9253](https://github.com/gradio-app/gradio/pull/9253) [`99648ec`](https://github.com/gradio-app/gradio/commit/99648ec7c4443e74799941e47b0015ac9ca581e1) - Adds ability to block event trigger when file is uploading. Thanks @dawoodkhan82!
10
+
11
+ ### Dependency updates
12
+
13
+ - @gradio/atoms@0.9.0-beta.2
14
+ - @gradio/upload@0.13.0-beta.2
15
+ - @gradio/wasm@0.14.0-beta.2
16
+ - @gradio/client@1.6.0-beta.2
17
+ - @gradio/icons@0.8.0-beta.2
18
+ - @gradio/statustracker@0.8.0-beta.2
19
+ - @gradio/utils@0.7.0-beta.2
20
+ - @gradio/image@0.16.0-beta.2
21
+
22
+ ## 0.11.0-beta.1
23
+
24
+ ### Dependency updates
25
+
26
+ - @gradio/atoms@0.8.1-beta.1
27
+ - @gradio/icons@0.8.0-beta.1
28
+ - @gradio/statustracker@0.8.0-beta.1
29
+ - @gradio/utils@0.7.0-beta.1
30
+ - @gradio/client@1.6.0-beta.1
31
+ - @gradio/image@0.16.0-beta.1
32
+ - @gradio/upload@0.12.4-beta.1
33
+ - @gradio/wasm@0.13.1-beta.1
34
+
35
+ ## 0.11.0
36
+
37
+ ### Features
38
+
39
+ - [#8941](https://github.com/gradio-app/gradio/pull/8941) [`97a7bf6`](https://github.com/gradio-app/gradio/commit/97a7bf66a79179d1b91a3199d68e5c11216ca500) - Streaming inputs for 5.0. Thanks @freddyaboulton!
3
40
  ## 0.10.4
4
41
 
5
42
  ### Fixes
package/Example.svelte CHANGED
@@ -36,6 +36,7 @@
36
36
  on:mouseover={video.play.bind(video)}
37
37
  on:mouseout={video.pause.bind(video)}
38
38
  src={value?.video.url}
39
+ is_stream={false}
39
40
  {loop}
40
41
  />
41
42
  </div>
package/Index.svelte CHANGED
@@ -54,6 +54,9 @@
54
54
  export let mirror_webcam: boolean;
55
55
  export let include_audio: boolean;
56
56
  export let loop = false;
57
+ export let input_ready: boolean;
58
+ let uploading = false;
59
+ $: input_ready = !uploading;
57
60
 
58
61
  let _video: FileData | null = null;
59
62
  let _subtitle: FileData | null = null;
@@ -158,7 +161,7 @@
158
161
  on:share={({ detail }) => gradio.dispatch("share", detail)}
159
162
  on:error={({ detail }) => gradio.dispatch("error", detail)}
160
163
  i18n={gradio.i18n}
161
- upload={gradio.client.upload}
164
+ upload={(...args) => gradio.client.upload(...args)}
162
165
  />
163
166
  </Block>
164
167
  {:else}
@@ -189,6 +192,7 @@
189
192
  on:change={handle_change}
190
193
  on:drag={({ detail }) => (dragging = detail)}
191
194
  on:error={handle_error}
195
+ bind:uploading
192
196
  {label}
193
197
  {show_label}
194
198
  {show_download_button}
@@ -210,8 +214,8 @@
210
214
  on:stop_recording={() => gradio.dispatch("stop_recording")}
211
215
  i18n={gradio.i18n}
212
216
  max_file_size={gradio.max_file_size}
213
- upload={gradio.client.upload}
214
- stream_handler={gradio.client.stream}
217
+ upload={(...args) => gradio.client.upload(...args)}
218
+ stream_handler={(...args) => gradio.client.stream(...args)}
215
219
  >
216
220
  <UploadText i18n={gradio.i18n} type="video" />
217
221
  </Video>
@@ -32,6 +32,7 @@ async function init() {
32
32
  on:mouseover={video.play.bind(video)}
33
33
  on:mouseout={video.pause.bind(video)}
34
34
  src={value?.video.url}
35
+ is_stream={false}
35
36
  {loop}
36
37
  />
37
38
  </div>
package/dist/Index.svelte CHANGED
@@ -27,6 +27,10 @@ export let interactive;
27
27
  export let mirror_webcam;
28
28
  export let include_audio;
29
29
  export let loop = false;
30
+ export let input_ready;
31
+ let uploading = false;
32
+ $:
33
+ input_ready = !uploading;
30
34
  let _video = null;
31
35
  let _subtitle = null;
32
36
  let active_source;
@@ -115,7 +119,7 @@ function handle_error({ detail }) {
115
119
  on:share={({ detail }) => gradio.dispatch("share", detail)}
116
120
  on:error={({ detail }) => gradio.dispatch("error", detail)}
117
121
  i18n={gradio.i18n}
118
- upload={gradio.client.upload}
122
+ upload={(...args) => gradio.client.upload(...args)}
119
123
  />
120
124
  </Block>
121
125
  {:else}
@@ -146,6 +150,7 @@ function handle_error({ detail }) {
146
150
  on:change={handle_change}
147
151
  on:drag={({ detail }) => (dragging = detail)}
148
152
  on:error={handle_error}
153
+ bind:uploading
149
154
  {label}
150
155
  {show_label}
151
156
  {show_download_button}
@@ -167,8 +172,8 @@ function handle_error({ detail }) {
167
172
  on:stop_recording={() => gradio.dispatch("stop_recording")}
168
173
  i18n={gradio.i18n}
169
174
  max_file_size={gradio.max_file_size}
170
- upload={gradio.client.upload}
171
- stream_handler={gradio.client.stream}
175
+ upload={(...args) => gradio.client.upload(...args)}
176
+ stream_handler={(...args) => gradio.client.stream(...args)}
172
177
  >
173
178
  <UploadText i18n={gradio.i18n} type="video" />
174
179
  </Video>
@@ -43,6 +43,7 @@ declare const __propDef: {
43
43
  mirror_webcam: boolean;
44
44
  include_audio: boolean;
45
45
  loop?: boolean | undefined;
46
+ input_ready: boolean;
46
47
  };
47
48
  events: {
48
49
  [evt: string]: CustomEvent<any>;
@@ -153,5 +154,8 @@ export default class Index extends SvelteComponent<IndexProps, IndexEvents, Inde
153
154
  get loop(): boolean | undefined;
154
155
  /**accessor*/
155
156
  set loop(_: boolean | undefined);
157
+ get input_ready(): boolean;
158
+ /**accessor*/
159
+ set input_ready(_: boolean);
156
160
  }
157
161
  export {};
@@ -24,6 +24,8 @@ export let max_file_size = null;
24
24
  export let upload;
25
25
  export let stream_handler;
26
26
  export let loop;
27
+ export let uploading = false;
28
+ let has_change_history = false;
27
29
  const dispatch = createEventDispatcher();
28
30
  function handle_load({ detail }) {
29
31
  value = detail;
@@ -36,6 +38,7 @@ function handle_clear() {
36
38
  dispatch("clear");
37
39
  }
38
40
  function handle_change(video) {
41
+ has_change_history = true;
39
42
  dispatch("change", video);
40
43
  }
41
44
  function handle_capture({
@@ -55,6 +58,7 @@ $:
55
58
  {#if active_source === "upload"}
56
59
  <Upload
57
60
  bind:dragging
61
+ bind:uploading
58
62
  filetype="video/x-m4v,video/*"
59
63
  on:load={handle_load}
60
64
  {max_file_size}
@@ -77,41 +81,41 @@ $:
77
81
  on:stop_recording
78
82
  {i18n}
79
83
  {upload}
84
+ stream_every={1}
80
85
  />
81
86
  {/if}
82
87
  </div>
83
- {:else}
84
- <ModifyUpload
85
- {i18n}
86
- on:clear={handle_clear}
87
- download={show_download_button ? value.url : null}
88
- />
89
- {#if playable()}
90
- {#key value?.url}
91
- <Player
92
- {upload}
93
- {root}
94
- interactive
95
- {autoplay}
96
- src={value.url}
97
- subtitle={subtitle?.url}
98
- on:play
99
- on:pause
100
- on:stop
101
- on:end
102
- mirror={mirror_webcam && active_source === "webcam"}
103
- {label}
104
- {handle_change}
105
- {handle_reset_value}
106
- {loop}
107
- />
108
- {/key}
109
- {:else if value.size}
110
- <div class="file-name">{value.orig_name || value.url}</div>
111
- <div class="file-size">
112
- {prettyBytes(value.size)}
113
- </div>
114
- {/if}
88
+ {:else if playable()}
89
+ {#key value?.url}
90
+ <Player
91
+ {upload}
92
+ {root}
93
+ interactive
94
+ {autoplay}
95
+ src={value.url}
96
+ subtitle={subtitle?.url}
97
+ is_stream={false}
98
+ on:play
99
+ on:pause
100
+ on:stop
101
+ on:end
102
+ mirror={mirror_webcam && active_source === "webcam"}
103
+ {label}
104
+ {handle_change}
105
+ {handle_reset_value}
106
+ {loop}
107
+ {value}
108
+ {i18n}
109
+ {show_download_button}
110
+ {handle_clear}
111
+ {has_change_history}
112
+ />
113
+ {/key}
114
+ {:else if value.size}
115
+ <div class="file-name">{value.orig_name || value.url}</div>
116
+ <div class="file-size">
117
+ {prettyBytes(value.size)}
118
+ </div>
115
119
  {/if}
116
120
 
117
121
  <SelectSource {sources} bind:active_source {handle_clear} />
@@ -20,6 +20,7 @@ declare const __propDef: {
20
20
  upload: Client["upload"];
21
21
  stream_handler: Client["stream"];
22
22
  loop: boolean;
23
+ uploading?: boolean | undefined;
23
24
  };
24
25
  events: {
25
26
  error: CustomEvent<any>;
@@ -17,6 +17,13 @@ export let handle_change = () => {
17
17
  export let handle_reset_value = () => {
18
18
  };
19
19
  export let upload;
20
+ export let is_stream;
21
+ export let i18n;
22
+ export let show_download_button = false;
23
+ export let value = null;
24
+ export let handle_clear = () => {
25
+ };
26
+ export let has_change_history = false;
20
27
  const dispatch = createEventDispatcher();
21
28
  let time = 0;
22
29
  let duration;
@@ -56,8 +63,8 @@ function handle_end() {
56
63
  const handle_trim_video = async (videoBlob) => {
57
64
  let _video_blob = new File([videoBlob], "video.mp4");
58
65
  const val = await prepare_files([_video_blob]);
59
- let value = ((await upload(val, root))?.filter(Boolean))[0];
60
- handle_change(value);
66
+ let value2 = ((await upload(val, root))?.filter(Boolean))[0];
67
+ handle_change(value2);
61
68
  };
62
69
  function open_full_screen() {
63
70
  video.requestFullscreen();
@@ -71,6 +78,7 @@ function open_full_screen() {
71
78
  preload="auto"
72
79
  {autoplay}
73
80
  {loop}
81
+ {is_stream}
74
82
  on:click={play_pause}
75
83
  on:play
76
84
  on:pause
@@ -138,6 +146,11 @@ function open_full_screen() {
138
146
  {handle_trim_video}
139
147
  {handle_reset_value}
140
148
  bind:processingVideo
149
+ {value}
150
+ {i18n}
151
+ {show_download_button}
152
+ {handle_clear}
153
+ {has_change_history}
141
154
  />
142
155
  {/if}
143
156
 
@@ -1,5 +1,6 @@
1
1
  import { SvelteComponent } from "svelte";
2
2
  import type { FileData, Client } from "@gradio/client";
3
+ import type { I18nFormatter } from "@gradio/utils";
3
4
  declare const __propDef: {
4
5
  props: {
5
6
  root?: string | undefined;
@@ -13,6 +14,12 @@ declare const __propDef: {
13
14
  handle_change?: ((video: FileData) => void) | undefined;
14
15
  handle_reset_value?: (() => void) | undefined;
15
16
  upload: Client["upload"];
17
+ is_stream: boolean | undefined;
18
+ i18n: I18nFormatter;
19
+ show_download_button?: boolean | undefined;
20
+ value?: (FileData | null) | undefined;
21
+ handle_clear?: (() => void) | undefined;
22
+ has_change_history?: boolean | undefined;
16
23
  };
17
24
  events: {
18
25
  play: CustomEvent<any>;
@@ -20,6 +27,7 @@ declare const __propDef: {
20
27
  load: Event;
21
28
  stop: CustomEvent<undefined>;
22
29
  end: CustomEvent<undefined>;
30
+ clear: CustomEvent<undefined>;
23
31
  } & {
24
32
  [evt: string]: CustomEvent<any>;
25
33
  };
@@ -1,6 +1,7 @@
1
1
  <script>import { createEventDispatcher } from "svelte";
2
2
  import { loaded } from "./utils";
3
3
  import { resolve_wasm_src } from "@gradio/wasm/svelte";
4
+ import Hls from "hls.js";
4
5
  export let src = void 0;
5
6
  export let muted = void 0;
6
7
  export let playsinline = void 0;
@@ -12,8 +13,10 @@ export let duration = void 0;
12
13
  export let paused = void 0;
13
14
  export let node = void 0;
14
15
  export let loop;
16
+ export let is_stream;
15
17
  export let processingVideo = false;
16
18
  let resolved_src;
19
+ let stream_active = false;
17
20
  let latest_src;
18
21
  $: {
19
22
  resolved_src = src;
@@ -26,6 +29,53 @@ $: {
26
29
  });
27
30
  }
28
31
  const dispatch = createEventDispatcher();
32
+ function load_stream(src2, is_stream2, node2) {
33
+ if (!src2 || !is_stream2)
34
+ return;
35
+ if (!node2)
36
+ return;
37
+ if (Hls.isSupported() && !stream_active) {
38
+ const hls = new Hls({
39
+ maxBufferLength: 1,
40
+ // 0.5 seconds (500 ms)
41
+ maxMaxBufferLength: 1,
42
+ // Maximum max buffer length in seconds
43
+ lowLatencyMode: true
44
+ // Enable low latency mode
45
+ });
46
+ hls.loadSource(src2);
47
+ hls.attachMedia(node2);
48
+ hls.on(Hls.Events.MANIFEST_PARSED, function() {
49
+ node2.play();
50
+ });
51
+ hls.on(Hls.Events.ERROR, function(event, data) {
52
+ console.error("HLS error:", event, data);
53
+ if (data.fatal) {
54
+ switch (data.type) {
55
+ case Hls.ErrorTypes.NETWORK_ERROR:
56
+ console.error(
57
+ "Fatal network error encountered, trying to recover"
58
+ );
59
+ hls.startLoad();
60
+ break;
61
+ case Hls.ErrorTypes.MEDIA_ERROR:
62
+ console.error("Fatal media error encountered, trying to recover");
63
+ hls.recoverMediaError();
64
+ break;
65
+ default:
66
+ console.error("Fatal error, cannot recover");
67
+ hls.destroy();
68
+ break;
69
+ }
70
+ }
71
+ });
72
+ stream_active = true;
73
+ }
74
+ }
75
+ $:
76
+ src, stream_active = false;
77
+ $:
78
+ load_stream(src, is_stream, node);
29
79
  </script>
30
80
 
31
81
  <!--
@@ -14,6 +14,7 @@ declare const __propDef: {
14
14
  paused?: boolean | undefined;
15
15
  node?: HTMLVideoElement | undefined;
16
16
  loop: boolean;
17
+ is_stream: any;
17
18
  processingVideo?: boolean | undefined;
18
19
  };
19
20
  events: {
@@ -1,10 +1,12 @@
1
- <script>import { Undo, Trim } from "@gradio/icons";
1
+ <script>import { Undo, Trim, Clear } from "@gradio/icons";
2
2
  import VideoTimeline from "./VideoTimeline.svelte";
3
3
  import { trimVideo } from "./utils";
4
4
  import { FFmpeg } from "@ffmpeg/ffmpeg";
5
5
  import loadFfmpeg from "./utils";
6
6
  import { onMount } from "svelte";
7
7
  import { format_time } from "@gradio/utils";
8
+ import { IconButton } from "@gradio/atoms";
9
+ import { ModifyUpload } from "@gradio/upload";
8
10
  export let videoElement;
9
11
  export let showRedo = false;
10
12
  export let interactive = true;
@@ -12,6 +14,12 @@ export let mode = "";
12
14
  export let handle_reset_value;
13
15
  export let handle_trim_video;
14
16
  export let processingVideo = false;
17
+ export let i18n;
18
+ export let value = null;
19
+ export let show_download_button = false;
20
+ export let handle_clear = () => {
21
+ };
22
+ export let has_change_history = false;
15
23
  let ffmpeg;
16
24
  onMount(async () => {
17
25
  ffmpeg = await loadFfmpeg();
@@ -33,7 +41,7 @@ const toggleTrimmingMode = () => {
33
41
  };
34
42
  </script>
35
43
 
36
- <div class="container">
44
+ <div class="container" class:hidden={mode !== "edit"}>
37
45
  {#if mode === "edit"}
38
46
  <div class="timeline-wrapper">
39
47
  <VideoTimeline
@@ -52,62 +60,61 @@ const toggleTrimmingMode = () => {
52
60
  aria-label="duration of selected region in seconds"
53
61
  class:hidden={loadingTimeline}>{format_time(trimmedDuration)}</time
54
62
  >
55
- {:else}
56
- <div />
57
- {/if}
58
-
59
- <div class="settings-wrapper">
60
- {#if showRedo && mode === ""}
63
+ <div class="edit-buttons">
61
64
  <button
62
- class="action icon"
63
- disabled={processingVideo}
64
- aria-label="Reset video to initial value"
65
+ class:hidden={loadingTimeline}
66
+ class="text-button"
65
67
  on:click={() => {
66
- handle_reset_value();
67
68
  mode = "";
68
- }}
69
+ processingVideo = true;
70
+ trimVideo(ffmpeg, dragStart, dragEnd, videoElement)
71
+ .then((videoBlob) => {
72
+ handle_trim_video(videoBlob);
73
+ })
74
+ .then(() => {
75
+ processingVideo = false;
76
+ });
77
+ }}>Trim</button
69
78
  >
70
- <Undo />
71
- </button>
72
- {/if}
73
-
74
- {#if interactive}
75
- {#if mode === ""}
76
- <button
77
- disabled={processingVideo}
78
- class="action icon"
79
- aria-label="Trim video to selection"
80
- on:click={toggleTrimmingMode}
81
- >
82
- <Trim />
83
- </button>
84
- {:else}
85
- <button
86
- class:hidden={loadingTimeline}
87
- class="text-button"
88
- on:click={() => {
89
- mode = "";
90
- processingVideo = true;
91
- trimVideo(ffmpeg, dragStart, dragEnd, videoElement)
92
- .then((videoBlob) => {
93
- handle_trim_video(videoBlob);
94
- })
95
- .then(() => {
96
- processingVideo = false;
97
- });
98
- }}>Trim</button
99
- >
100
- <button
101
- class="text-button"
102
- class:hidden={loadingTimeline}
103
- on:click={toggleTrimmingMode}>Cancel</button
104
- >
105
- {/if}
106
- {/if}
107
- </div>
79
+ <button
80
+ class="text-button"
81
+ class:hidden={loadingTimeline}
82
+ on:click={toggleTrimmingMode}>Cancel</button
83
+ >
84
+ </div>
85
+ {:else}
86
+ <div />
87
+ {/if}
108
88
  </div>
109
89
  </div>
110
90
 
91
+ <ModifyUpload
92
+ {i18n}
93
+ on:clear={() => handle_clear()}
94
+ download={show_download_button ? value?.url : null}
95
+ >
96
+ {#if showRedo && mode === ""}
97
+ <IconButton
98
+ Icon={Undo}
99
+ label="Reset video to initial value"
100
+ disabled={processingVideo || !has_change_history}
101
+ on:click={() => {
102
+ handle_reset_value();
103
+ mode = "";
104
+ }}
105
+ />
106
+ {/if}
107
+
108
+ {#if interactive && mode === ""}
109
+ <IconButton
110
+ Icon={Trim}
111
+ label="Trim video to selection"
112
+ disabled={processingVideo}
113
+ on:click={toggleTrimmingMode}
114
+ />
115
+ {/if}
116
+ </ModifyUpload>
117
+
111
118
  <style>
112
119
  .container {
113
120
  width: 100%;
@@ -124,10 +131,7 @@ const toggleTrimmingMode = () => {
124
131
  justify-content: center;
125
132
  width: 100%;
126
133
  }
127
- .settings-wrapper {
128
- display: flex;
129
- justify-self: self-end;
130
- }
134
+
131
135
  .text-button {
132
136
  border: 1px solid var(--neutral-400);
133
137
  border-radius: var(--radius-sm);
@@ -140,9 +144,6 @@ const toggleTrimmingMode = () => {
140
144
  padding: 0 5px;
141
145
  margin-left: 5px;
142
146
  }
143
- .hidden {
144
- display: none;
145
- }
146
147
 
147
148
  .text-button:hover,
148
149
  .text-button:focus {
@@ -151,17 +152,26 @@ const toggleTrimmingMode = () => {
151
152
  }
152
153
 
153
154
  .controls {
154
- display: grid;
155
- grid-template-columns: 1fr 1fr;
155
+ display: flex;
156
+ justify-content: space-between;
157
+ align-items: center;
156
158
  margin: var(--spacing-lg);
157
159
  overflow: hidden;
158
- text-align: left;
160
+ }
161
+
162
+ .edit-buttons {
163
+ display: flex;
164
+ gap: var(--spacing-sm);
159
165
  }
160
166
 
161
167
  @media (max-width: 320px) {
162
168
  .controls {
163
- display: flex;
164
- flex-wrap: wrap;
169
+ flex-direction: column;
170
+ align-items: flex-start;
171
+ }
172
+
173
+ .edit-buttons {
174
+ margin-top: var(--spacing-sm);
165
175
  }
166
176
 
167
177
  .controls * {
@@ -172,29 +182,13 @@ const toggleTrimmingMode = () => {
172
182
  margin-left: 0;
173
183
  }
174
184
  }
175
- .action {
176
- width: var(--size-5);
177
- width: var(--size-5);
178
- color: var(--neutral-400);
179
- margin-left: var(--spacing-md);
180
- }
181
-
182
- .action:disabled {
183
- cursor: not-allowed;
184
- color: var(--border-color-accent-subdued);
185
- }
186
-
187
- .action:disabled:hover {
188
- color: var(--border-color-accent-subdued);
189
- }
190
-
191
- .icon:hover,
192
- .icon:focus {
193
- color: var(--color-accent);
194
- }
195
185
 
196
186
  .container {
197
187
  display: flex;
198
188
  flex-direction: column;
199
189
  }
190
+
191
+ .hidden {
192
+ display: none;
193
+ }
200
194
  </style>
@@ -1,4 +1,5 @@
1
1
  import { SvelteComponent } from "svelte";
2
+ import type { FileData } from "@gradio/client";
2
3
  declare const __propDef: {
3
4
  props: {
4
5
  videoElement: HTMLVideoElement;
@@ -8,6 +9,11 @@ declare const __propDef: {
8
9
  handle_reset_value: () => void;
9
10
  handle_trim_video: (videoBlob: Blob) => void;
10
11
  processingVideo?: boolean | undefined;
12
+ i18n: (key: string) => string;
13
+ value?: (FileData | null) | undefined;
14
+ show_download_button?: boolean | undefined;
15
+ handle_clear?: (() => void) | undefined;
16
+ has_change_history?: boolean | undefined;
11
17
  };
12
18
  events: {
13
19
  [evt: string]: CustomEvent<any>;
@@ -1,5 +1,11 @@
1
1
  <script>import { createEventDispatcher, afterUpdate, tick } from "svelte";
2
- import { BlockLabel, Empty, IconButton, ShareButton } from "@gradio/atoms";
2
+ import {
3
+ BlockLabel,
4
+ Empty,
5
+ IconButton,
6
+ ShareButton,
7
+ IconButtonWrapper
8
+ } from "@gradio/atoms";
3
9
  import { Video, Download } from "@gradio/icons";
4
10
  import { uploadToHuggingFace } from "@gradio/utils";
5
11
  import { DownloadLink } from "@gradio/wasm/svelte";
@@ -32,13 +38,14 @@ afterUpdate(async () => {
32
38
  </script>
33
39
 
34
40
  <BlockLabel {show_label} Icon={Video} label={label || "Video"} />
35
- {#if value === null || value.url === undefined}
41
+ {#if !value || value.url === undefined}
36
42
  <Empty unpadded_box={true} size="large"><Video /></Empty>
37
43
  {:else}
38
44
  {#key value.url}
39
45
  <Player
40
46
  src={value.url}
41
47
  subtitle={subtitle?.url}
48
+ is_stream={value.is_stream}
42
49
  {autoplay}
43
50
  on:play
44
51
  on:pause
@@ -50,36 +57,34 @@ afterUpdate(async () => {
50
57
  {loop}
51
58
  interactive={false}
52
59
  {upload}
60
+ {i18n}
53
61
  />
54
62
  {/key}
55
- <div class="icon-buttons" data-testid="download-div">
56
- {#if show_download_button}
57
- <DownloadLink href={value.url} download={value.orig_name || value.path}>
58
- <IconButton Icon={Download} label="Download" />
59
- </DownloadLink>
60
- {/if}
61
- {#if show_share_button}
62
- <ShareButton
63
- {i18n}
64
- on:error
65
- on:share
66
- {value}
67
- formatter={async (value) => {
68
- if (!value) return "";
69
- let url = await uploadToHuggingFace(value.data, "url");
70
- return url;
71
- }}
72
- />
73
- {/if}
63
+ <div data-testid="download-div">
64
+ <IconButtonWrapper>
65
+ {#if show_download_button}
66
+ <DownloadLink
67
+ href={value.is_stream
68
+ ? value.url?.replace("playlist.m3u8", "playlist-file")
69
+ : value.url}
70
+ download={value.orig_name || value.path}
71
+ >
72
+ <IconButton Icon={Download} label="Download" />
73
+ </DownloadLink>
74
+ {/if}
75
+ {#if show_share_button}
76
+ <ShareButton
77
+ {i18n}
78
+ on:error
79
+ on:share
80
+ {value}
81
+ formatter={async (value) => {
82
+ if (!value) return "";
83
+ let url = await uploadToHuggingFace(value.data, "url");
84
+ return url;
85
+ }}
86
+ />
87
+ {/if}
88
+ </IconButtonWrapper>
74
89
  </div>
75
90
  {/if}
76
-
77
- <style>
78
- .icon-buttons {
79
- display: flex;
80
- position: absolute;
81
- top: 6px;
82
- right: 6px;
83
- gap: var(--size-1);
84
- }
85
- </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/video",
3
- "version": "0.10.4",
3
+ "version": "0.11.0-beta.2",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -9,15 +9,16 @@
9
9
  "dependencies": {
10
10
  "@ffmpeg/ffmpeg": "^0.12.7",
11
11
  "@ffmpeg/util": "^0.12.1",
12
+ "hls.js": "^1.5.13",
12
13
  "mrmime": "^2.0.0",
13
- "@gradio/atoms": "^0.8.1",
14
- "@gradio/icons": "^0.7.2",
15
- "@gradio/image": "^0.15.1",
16
- "@gradio/client": "^1.5.2",
17
- "@gradio/statustracker": "^0.7.6",
18
- "@gradio/utils": "^0.6.1",
19
- "@gradio/upload": "^0.12.4",
20
- "@gradio/wasm": "^0.13.1"
14
+ "@gradio/atoms": "^0.9.0-beta.2",
15
+ "@gradio/client": "^1.6.0-beta.2",
16
+ "@gradio/icons": "^0.8.0-beta.2",
17
+ "@gradio/statustracker": "^0.8.0-beta.2",
18
+ "@gradio/image": "^0.16.0-beta.2",
19
+ "@gradio/upload": "^0.13.0-beta.2",
20
+ "@gradio/utils": "^0.7.0-beta.2",
21
+ "@gradio/wasm": "^0.14.0-beta.2"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@gradio/preview": "^0.11.1"
@@ -32,6 +32,9 @@
32
32
  export let upload: Client["upload"];
33
33
  export let stream_handler: Client["stream"];
34
34
  export let loop: boolean;
35
+ export let uploading = false;
36
+
37
+ let has_change_history = false;
35
38
 
36
39
  const dispatch = createEventDispatcher<{
37
40
  change: FileData | null;
@@ -59,6 +62,7 @@
59
62
  }
60
63
 
61
64
  function handle_change(video: FileData): void {
65
+ has_change_history = true;
62
66
  dispatch("change", video);
63
67
  }
64
68
 
@@ -79,6 +83,7 @@
79
83
  {#if active_source === "upload"}
80
84
  <Upload
81
85
  bind:dragging
86
+ bind:uploading
82
87
  filetype="video/x-m4v,video/*"
83
88
  on:load={handle_load}
84
89
  {max_file_size}
@@ -101,41 +106,41 @@
101
106
  on:stop_recording
102
107
  {i18n}
103
108
  {upload}
109
+ stream_every={1}
104
110
  />
105
111
  {/if}
106
112
  </div>
107
- {:else}
108
- <ModifyUpload
109
- {i18n}
110
- on:clear={handle_clear}
111
- download={show_download_button ? value.url : null}
112
- />
113
- {#if playable()}
114
- {#key value?.url}
115
- <Player
116
- {upload}
117
- {root}
118
- interactive
119
- {autoplay}
120
- src={value.url}
121
- subtitle={subtitle?.url}
122
- on:play
123
- on:pause
124
- on:stop
125
- on:end
126
- mirror={mirror_webcam && active_source === "webcam"}
127
- {label}
128
- {handle_change}
129
- {handle_reset_value}
130
- {loop}
131
- />
132
- {/key}
133
- {:else if value.size}
134
- <div class="file-name">{value.orig_name || value.url}</div>
135
- <div class="file-size">
136
- {prettyBytes(value.size)}
137
- </div>
138
- {/if}
113
+ {:else if playable()}
114
+ {#key value?.url}
115
+ <Player
116
+ {upload}
117
+ {root}
118
+ interactive
119
+ {autoplay}
120
+ src={value.url}
121
+ subtitle={subtitle?.url}
122
+ is_stream={false}
123
+ on:play
124
+ on:pause
125
+ on:stop
126
+ on:end
127
+ mirror={mirror_webcam && active_source === "webcam"}
128
+ {label}
129
+ {handle_change}
130
+ {handle_reset_value}
131
+ {loop}
132
+ {value}
133
+ {i18n}
134
+ {show_download_button}
135
+ {handle_clear}
136
+ {has_change_history}
137
+ />
138
+ {/key}
139
+ {:else if value.size}
140
+ <div class="file-name">{value.orig_name || value.url}</div>
141
+ <div class="file-size">
142
+ {prettyBytes(value.size)}
143
+ </div>
139
144
  {/if}
140
145
 
141
146
  <SelectSource {sources} bind:active_source {handle_clear} />
@@ -6,6 +6,7 @@
6
6
  import type { FileData, Client } from "@gradio/client";
7
7
  import { prepare_files } from "@gradio/client";
8
8
  import { format_time } from "@gradio/utils";
9
+ import type { I18nFormatter } from "@gradio/utils";
9
10
 
10
11
  export let root = "";
11
12
  export let src: string;
@@ -18,12 +19,19 @@
18
19
  export let handle_change: (video: FileData) => void = () => {};
19
20
  export let handle_reset_value: () => void = () => {};
20
21
  export let upload: Client["upload"];
22
+ export let is_stream: boolean | undefined;
23
+ export let i18n: I18nFormatter;
24
+ export let show_download_button = false;
25
+ export let value: FileData | null = null;
26
+ export let handle_clear: () => void = () => {};
27
+ export let has_change_history = false;
21
28
 
22
29
  const dispatch = createEventDispatcher<{
23
30
  play: undefined;
24
31
  pause: undefined;
25
32
  stop: undefined;
26
33
  end: undefined;
34
+ clear: undefined;
27
35
  }>();
28
36
 
29
37
  let time = 0;
@@ -98,6 +106,7 @@
98
106
  preload="auto"
99
107
  {autoplay}
100
108
  {loop}
109
+ {is_stream}
101
110
  on:click={play_pause}
102
111
  on:play
103
112
  on:pause
@@ -165,6 +174,11 @@
165
174
  {handle_trim_video}
166
175
  {handle_reset_value}
167
176
  bind:processingVideo
177
+ {value}
178
+ {i18n}
179
+ {show_download_button}
180
+ {handle_clear}
181
+ {has_change_history}
168
182
  />
169
183
  {/if}
170
184
 
@@ -5,6 +5,8 @@
5
5
 
6
6
  import { resolve_wasm_src } from "@gradio/wasm/svelte";
7
7
 
8
+ import Hls from "hls.js";
9
+
8
10
  export let src: HTMLVideoAttributes["src"] = undefined;
9
11
 
10
12
  export let muted: HTMLVideoAttributes["muted"] = undefined;
@@ -19,10 +21,12 @@
19
21
 
20
22
  export let node: HTMLVideoElement | undefined = undefined;
21
23
  export let loop: boolean;
24
+ export let is_stream;
22
25
 
23
26
  export let processingVideo = false;
24
27
 
25
28
  let resolved_src: typeof src;
29
+ let stream_active = false;
26
30
 
27
31
  // The `src` prop can be updated before the Promise from `resolve_wasm_src` is resolved.
28
32
  // In such a case, the resolved value for the old `src` has to be discarded,
@@ -46,6 +50,53 @@
46
50
  }
47
51
 
48
52
  const dispatch = createEventDispatcher();
53
+
54
+ function load_stream(
55
+ src: string | null | undefined,
56
+ is_stream: boolean,
57
+ node: HTMLVideoElement | undefined
58
+ ): void {
59
+ if (!src || !is_stream) return;
60
+ if (!node) return;
61
+ if (Hls.isSupported() && !stream_active) {
62
+ const hls = new Hls({
63
+ maxBufferLength: 1, // 0.5 seconds (500 ms)
64
+ maxMaxBufferLength: 1, // Maximum max buffer length in seconds
65
+ lowLatencyMode: true // Enable low latency mode
66
+ });
67
+ hls.loadSource(src);
68
+ hls.attachMedia(node);
69
+ hls.on(Hls.Events.MANIFEST_PARSED, function () {
70
+ (node as HTMLVideoElement).play();
71
+ });
72
+ hls.on(Hls.Events.ERROR, function (event, data) {
73
+ console.error("HLS error:", event, data);
74
+ if (data.fatal) {
75
+ switch (data.type) {
76
+ case Hls.ErrorTypes.NETWORK_ERROR:
77
+ console.error(
78
+ "Fatal network error encountered, trying to recover"
79
+ );
80
+ hls.startLoad();
81
+ break;
82
+ case Hls.ErrorTypes.MEDIA_ERROR:
83
+ console.error("Fatal media error encountered, trying to recover");
84
+ hls.recoverMediaError();
85
+ break;
86
+ default:
87
+ console.error("Fatal error, cannot recover");
88
+ hls.destroy();
89
+ break;
90
+ }
91
+ }
92
+ });
93
+ stream_active = true;
94
+ }
95
+ }
96
+
97
+ $: src, (stream_active = false);
98
+
99
+ $: load_stream(src, is_stream, node);
49
100
  </script>
50
101
 
51
102
  <!--
@@ -1,11 +1,14 @@
1
1
  <script lang="ts">
2
- import { Undo, Trim } from "@gradio/icons";
2
+ import { Undo, Trim, Clear } from "@gradio/icons";
3
3
  import VideoTimeline from "./VideoTimeline.svelte";
4
4
  import { trimVideo } from "./utils";
5
5
  import { FFmpeg } from "@ffmpeg/ffmpeg";
6
6
  import loadFfmpeg from "./utils";
7
7
  import { onMount } from "svelte";
8
8
  import { format_time } from "@gradio/utils";
9
+ import { IconButton } from "@gradio/atoms";
10
+ import { ModifyUpload } from "@gradio/upload";
11
+ import type { FileData } from "@gradio/client";
9
12
 
10
13
  export let videoElement: HTMLVideoElement;
11
14
 
@@ -15,6 +18,11 @@
15
18
  export let handle_reset_value: () => void;
16
19
  export let handle_trim_video: (videoBlob: Blob) => void;
17
20
  export let processingVideo = false;
21
+ export let i18n: (key: string) => string;
22
+ export let value: FileData | null = null;
23
+ export let show_download_button = false;
24
+ export let handle_clear: () => void = () => {};
25
+ export let has_change_history = false;
18
26
 
19
27
  let ffmpeg: FFmpeg;
20
28
 
@@ -41,7 +49,7 @@
41
49
  };
42
50
  </script>
43
51
 
44
- <div class="container">
52
+ <div class="container" class:hidden={mode !== "edit"}>
45
53
  {#if mode === "edit"}
46
54
  <div class="timeline-wrapper">
47
55
  <VideoTimeline
@@ -60,62 +68,61 @@
60
68
  aria-label="duration of selected region in seconds"
61
69
  class:hidden={loadingTimeline}>{format_time(trimmedDuration)}</time
62
70
  >
63
- {:else}
64
- <div />
65
- {/if}
66
-
67
- <div class="settings-wrapper">
68
- {#if showRedo && mode === ""}
71
+ <div class="edit-buttons">
69
72
  <button
70
- class="action icon"
71
- disabled={processingVideo}
72
- aria-label="Reset video to initial value"
73
+ class:hidden={loadingTimeline}
74
+ class="text-button"
73
75
  on:click={() => {
74
- handle_reset_value();
75
76
  mode = "";
76
- }}
77
+ processingVideo = true;
78
+ trimVideo(ffmpeg, dragStart, dragEnd, videoElement)
79
+ .then((videoBlob) => {
80
+ handle_trim_video(videoBlob);
81
+ })
82
+ .then(() => {
83
+ processingVideo = false;
84
+ });
85
+ }}>Trim</button
77
86
  >
78
- <Undo />
79
- </button>
80
- {/if}
81
-
82
- {#if interactive}
83
- {#if mode === ""}
84
- <button
85
- disabled={processingVideo}
86
- class="action icon"
87
- aria-label="Trim video to selection"
88
- on:click={toggleTrimmingMode}
89
- >
90
- <Trim />
91
- </button>
92
- {:else}
93
- <button
94
- class:hidden={loadingTimeline}
95
- class="text-button"
96
- on:click={() => {
97
- mode = "";
98
- processingVideo = true;
99
- trimVideo(ffmpeg, dragStart, dragEnd, videoElement)
100
- .then((videoBlob) => {
101
- handle_trim_video(videoBlob);
102
- })
103
- .then(() => {
104
- processingVideo = false;
105
- });
106
- }}>Trim</button
107
- >
108
- <button
109
- class="text-button"
110
- class:hidden={loadingTimeline}
111
- on:click={toggleTrimmingMode}>Cancel</button
112
- >
113
- {/if}
114
- {/if}
115
- </div>
87
+ <button
88
+ class="text-button"
89
+ class:hidden={loadingTimeline}
90
+ on:click={toggleTrimmingMode}>Cancel</button
91
+ >
92
+ </div>
93
+ {:else}
94
+ <div />
95
+ {/if}
116
96
  </div>
117
97
  </div>
118
98
 
99
+ <ModifyUpload
100
+ {i18n}
101
+ on:clear={() => handle_clear()}
102
+ download={show_download_button ? value?.url : null}
103
+ >
104
+ {#if showRedo && mode === ""}
105
+ <IconButton
106
+ Icon={Undo}
107
+ label="Reset video to initial value"
108
+ disabled={processingVideo || !has_change_history}
109
+ on:click={() => {
110
+ handle_reset_value();
111
+ mode = "";
112
+ }}
113
+ />
114
+ {/if}
115
+
116
+ {#if interactive && mode === ""}
117
+ <IconButton
118
+ Icon={Trim}
119
+ label="Trim video to selection"
120
+ disabled={processingVideo}
121
+ on:click={toggleTrimmingMode}
122
+ />
123
+ {/if}
124
+ </ModifyUpload>
125
+
119
126
  <style>
120
127
  .container {
121
128
  width: 100%;
@@ -132,10 +139,7 @@
132
139
  justify-content: center;
133
140
  width: 100%;
134
141
  }
135
- .settings-wrapper {
136
- display: flex;
137
- justify-self: self-end;
138
- }
142
+
139
143
  .text-button {
140
144
  border: 1px solid var(--neutral-400);
141
145
  border-radius: var(--radius-sm);
@@ -148,9 +152,6 @@
148
152
  padding: 0 5px;
149
153
  margin-left: 5px;
150
154
  }
151
- .hidden {
152
- display: none;
153
- }
154
155
 
155
156
  .text-button:hover,
156
157
  .text-button:focus {
@@ -159,17 +160,26 @@
159
160
  }
160
161
 
161
162
  .controls {
162
- display: grid;
163
- grid-template-columns: 1fr 1fr;
163
+ display: flex;
164
+ justify-content: space-between;
165
+ align-items: center;
164
166
  margin: var(--spacing-lg);
165
167
  overflow: hidden;
166
- text-align: left;
168
+ }
169
+
170
+ .edit-buttons {
171
+ display: flex;
172
+ gap: var(--spacing-sm);
167
173
  }
168
174
 
169
175
  @media (max-width: 320px) {
170
176
  .controls {
171
- display: flex;
172
- flex-wrap: wrap;
177
+ flex-direction: column;
178
+ align-items: flex-start;
179
+ }
180
+
181
+ .edit-buttons {
182
+ margin-top: var(--spacing-sm);
173
183
  }
174
184
 
175
185
  .controls * {
@@ -180,29 +190,13 @@
180
190
  margin-left: 0;
181
191
  }
182
192
  }
183
- .action {
184
- width: var(--size-5);
185
- width: var(--size-5);
186
- color: var(--neutral-400);
187
- margin-left: var(--spacing-md);
188
- }
189
-
190
- .action:disabled {
191
- cursor: not-allowed;
192
- color: var(--border-color-accent-subdued);
193
- }
194
-
195
- .action:disabled:hover {
196
- color: var(--border-color-accent-subdued);
197
- }
198
-
199
- .icon:hover,
200
- .icon:focus {
201
- color: var(--color-accent);
202
- }
203
193
 
204
194
  .container {
205
195
  display: flex;
206
196
  flex-direction: column;
207
197
  }
198
+
199
+ .hidden {
200
+ display: none;
201
+ }
208
202
  </style>
@@ -1,6 +1,12 @@
1
1
  <script lang="ts">
2
2
  import { createEventDispatcher, afterUpdate, tick } from "svelte";
3
- import { BlockLabel, Empty, IconButton, ShareButton } from "@gradio/atoms";
3
+ import {
4
+ BlockLabel,
5
+ Empty,
6
+ IconButton,
7
+ ShareButton,
8
+ IconButtonWrapper
9
+ } from "@gradio/atoms";
4
10
  import type { FileData, Client } from "@gradio/client";
5
11
  import { Video, Download } from "@gradio/icons";
6
12
  import { uploadToHuggingFace } from "@gradio/utils";
@@ -51,13 +57,14 @@
51
57
  </script>
52
58
 
53
59
  <BlockLabel {show_label} Icon={Video} label={label || "Video"} />
54
- {#if value === null || value.url === undefined}
60
+ {#if !value || value.url === undefined}
55
61
  <Empty unpadded_box={true} size="large"><Video /></Empty>
56
62
  {:else}
57
63
  {#key value.url}
58
64
  <Player
59
65
  src={value.url}
60
66
  subtitle={subtitle?.url}
67
+ is_stream={value.is_stream}
61
68
  {autoplay}
62
69
  on:play
63
70
  on:pause
@@ -69,36 +76,34 @@
69
76
  {loop}
70
77
  interactive={false}
71
78
  {upload}
79
+ {i18n}
72
80
  />
73
81
  {/key}
74
- <div class="icon-buttons" data-testid="download-div">
75
- {#if show_download_button}
76
- <DownloadLink href={value.url} download={value.orig_name || value.path}>
77
- <IconButton Icon={Download} label="Download" />
78
- </DownloadLink>
79
- {/if}
80
- {#if show_share_button}
81
- <ShareButton
82
- {i18n}
83
- on:error
84
- on:share
85
- {value}
86
- formatter={async (value) => {
87
- if (!value) return "";
88
- let url = await uploadToHuggingFace(value.data, "url");
89
- return url;
90
- }}
91
- />
92
- {/if}
82
+ <div data-testid="download-div">
83
+ <IconButtonWrapper>
84
+ {#if show_download_button}
85
+ <DownloadLink
86
+ href={value.is_stream
87
+ ? value.url?.replace("playlist.m3u8", "playlist-file")
88
+ : value.url}
89
+ download={value.orig_name || value.path}
90
+ >
91
+ <IconButton Icon={Download} label="Download" />
92
+ </DownloadLink>
93
+ {/if}
94
+ {#if show_share_button}
95
+ <ShareButton
96
+ {i18n}
97
+ on:error
98
+ on:share
99
+ {value}
100
+ formatter={async (value) => {
101
+ if (!value) return "";
102
+ let url = await uploadToHuggingFace(value.data, "url");
103
+ return url;
104
+ }}
105
+ />
106
+ {/if}
107
+ </IconButtonWrapper>
93
108
  </div>
94
109
  {/if}
95
-
96
- <style>
97
- .icon-buttons {
98
- display: flex;
99
- position: absolute;
100
- top: 6px;
101
- right: 6px;
102
- gap: var(--size-1);
103
- }
104
- </style>