@gradio/video 0.1.0-beta.6 → 0.1.0-beta.9

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,10 +1,10 @@
1
1
  <script lang="ts">
2
2
  import { createEventDispatcher } from "svelte";
3
3
  import { Upload, ModifyUpload } from "@gradio/upload";
4
- import type { FileData } from "@gradio/upload";
4
+ import type { FileData } from "@gradio/client";
5
5
  import { BlockLabel } from "@gradio/atoms";
6
6
  import { Webcam } from "@gradio/image";
7
- import { Video } from "@gradio/icons";
7
+ import { Video, Upload as UploadIcon } from "@gradio/icons";
8
8
 
9
9
  import { prettyBytes, playable } from "./utils";
10
10
  import Player from "./Player.svelte";
@@ -12,7 +12,11 @@
12
12
 
13
13
  export let value: FileData | null = null;
14
14
  export let subtitle: FileData | null = null;
15
- export let source: string;
15
+ export let sources:
16
+ | ["webcam"]
17
+ | ["upload"]
18
+ | ["webcam", "upload"]
19
+ | ["upload", "webcam"] = ["webcam", "upload"];
16
20
  export let label: string | undefined = undefined;
17
21
  export let show_label = true;
18
22
  export let mirror_webcam = false;
@@ -20,18 +24,20 @@
20
24
  export let autoplay: boolean;
21
25
  export let root: string;
22
26
  export let i18n: I18nFormatter;
27
+ export let active_source: "webcam" | "upload" = "webcam";
28
+ export let handle_reset_value: () => void = () => {};
23
29
 
24
30
  const dispatch = createEventDispatcher<{
25
- change: any;
26
- clear: undefined;
27
- play: undefined;
28
- pause: undefined;
29
- end: undefined;
31
+ change: FileData | null;
32
+ clear?: never;
33
+ play?: never;
34
+ pause?: never;
35
+ end?: never;
30
36
  drag: boolean;
31
37
  error: string;
32
38
  upload: FileData;
33
- start_recording: undefined;
34
- stop_recording: undefined;
39
+ start_recording?: never;
40
+ stop_recording?: never;
35
41
  }>();
36
42
 
37
43
  function handle_load({ detail }: CustomEvent<FileData | null>): void {
@@ -40,19 +46,24 @@
40
46
  dispatch("upload", detail!);
41
47
  }
42
48
 
43
- function handle_clear({ detail }: CustomEvent<FileData | null>): void {
49
+ function handle_clear(): void {
44
50
  value = null;
45
- dispatch("change", detail);
51
+ active_source = sources[0];
52
+ dispatch("change", null);
46
53
  dispatch("clear");
47
54
  }
48
55
 
56
+ function handle_change(video: FileData): void {
57
+ dispatch("change", video);
58
+ }
59
+
49
60
  let dragging = false;
50
61
  $: dispatch("drag", dragging);
51
62
  </script>
52
63
 
53
64
  <BlockLabel {show_label} Icon={Video} label={label || "Video"} />
54
- {#if value === null}
55
- {#if source === "upload"}
65
+ {#if value === null || value.url === undefined}
66
+ {#if active_source === "upload"}
56
67
  <Upload
57
68
  bind:dragging
58
69
  filetype="video/x-m4v,video/*"
@@ -61,13 +72,13 @@
61
72
  >
62
73
  <slot />
63
74
  </Upload>
64
- {:else if source === "webcam"}
75
+ {:else if active_source === "webcam"}
65
76
  <Webcam
66
77
  {mirror_webcam}
67
78
  {include_audio}
68
79
  mode="video"
69
80
  on:error
70
- on:capture={({ detail }) => dispatch("change", detail)}
81
+ on:capture={() => dispatch("change")}
71
82
  on:start_recording
72
83
  on:stop_recording
73
84
  {i18n}
@@ -76,27 +87,52 @@
76
87
  {:else}
77
88
  <ModifyUpload {i18n} on:clear={handle_clear} />
78
89
  {#if playable()}
79
- {#key value?.data}
90
+ {#key value?.url}
80
91
  <Player
92
+ {root}
93
+ interactive
81
94
  {autoplay}
82
- src={value.data}
83
- subtitle={subtitle?.data}
95
+ src={value.url}
96
+ subtitle={subtitle?.url}
84
97
  on:play
85
98
  on:pause
86
99
  on:stop
87
100
  on:end
88
- mirror={mirror_webcam && source === "webcam"}
101
+ mirror={mirror_webcam && active_source === "webcam"}
89
102
  {label}
103
+ {handle_change}
104
+ {handle_reset_value}
90
105
  />
91
106
  {/key}
92
107
  {:else if value.size}
93
- <div class="file-name">{value.name}</div>
108
+ <div class="file-name">{value.orig_name || value.url}</div>
94
109
  <div class="file-size">
95
110
  {prettyBytes(value.size)}
96
111
  </div>
97
112
  {/if}
98
113
  {/if}
99
114
 
115
+ {#if sources.length > 1}
116
+ <span class="source-selection">
117
+ <button
118
+ class="icon"
119
+ aria-label="Upload video"
120
+ on:click={() => {
121
+ handle_clear();
122
+ active_source = "upload";
123
+ }}><UploadIcon /></button
124
+ >
125
+ <button
126
+ class="icon"
127
+ aria-label="Record audio"
128
+ on:click={() => {
129
+ handle_clear();
130
+ active_source = "webcam";
131
+ }}><Video /></button
132
+ >
133
+ </span>
134
+ {/if}
135
+
100
136
  <style>
101
137
  .file-name {
102
138
  padding: var(--size-6);
@@ -108,4 +144,27 @@
108
144
  padding: var(--size-2);
109
145
  font-size: var(--text-xl);
110
146
  }
147
+
148
+ .source-selection {
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: center;
152
+ border-top: 1px solid var(--border-color-primary);
153
+ width: 95%;
154
+ margin: 0 auto;
155
+ }
156
+
157
+ .icon {
158
+ width: 22px;
159
+ height: 22px;
160
+ margin: var(--spacing-lg) var(--spacing-xs);
161
+ padding: var(--spacing-xs);
162
+ color: var(--neutral-400);
163
+ border-radius: var(--radius-md);
164
+ }
165
+
166
+ .icon:hover,
167
+ .icon:focus {
168
+ color: var(--color-accent);
169
+ }
111
170
  </style>
@@ -1,13 +1,20 @@
1
1
  <script lang="ts">
2
2
  import { createEventDispatcher } from "svelte";
3
3
  import { Play, Pause, Maximise, Undo } from "@gradio/icons";
4
- import Video from "./StaticVideo.svelte";
4
+ import Video from "./Video.svelte";
5
+ import VideoControls from "./VideoControls.svelte";
6
+ import type { FileData } from "@gradio/client";
7
+ import { prepare_files, upload } from "@gradio/client";
5
8
 
9
+ export let root = "";
6
10
  export let src: string;
7
11
  export let subtitle: string | null = null;
8
12
  export let mirror: boolean;
9
13
  export let autoplay: boolean;
10
14
  export let label = "test";
15
+ export let interactive = false;
16
+ export let handle_change: (video: FileData) => void = () => {};
17
+ export let handle_reset_value: () => void = () => {};
11
18
 
12
19
  const dispatch = createEventDispatcher<{
13
20
  play: undefined;
@@ -20,6 +27,7 @@
20
27
  let duration: number;
21
28
  let paused = true;
22
29
  let video: HTMLVideoElement;
30
+ let processingVideo = false;
23
31
 
24
32
  function handleMove(e: TouchEvent | MouseEvent): void {
25
33
  if (!duration) return;
@@ -77,6 +85,14 @@
77
85
  dispatch("end");
78
86
  }
79
87
 
88
+ const handle_trim_video = async (videoBlob: Blob): Promise<void> => {
89
+ let _video_blob = new File([videoBlob], "video.mp4");
90
+ const val = await prepare_files([_video_blob]);
91
+ let value = ((await upload(val, root))?.filter(Boolean) as FileData[])[0];
92
+
93
+ handle_change(value);
94
+ };
95
+
80
96
  function open_full_screen(): void {
81
97
  video.requestFullscreen();
82
98
  }
@@ -97,6 +113,7 @@
97
113
  bind:paused
98
114
  bind:node={video}
99
115
  data-testid={`${label}-player`}
116
+ {processingVideo}
100
117
  >
101
118
  <track kind="captions" src={subtitle} default />
102
119
  </Video>
@@ -146,6 +163,15 @@
146
163
  </div>
147
164
  </div>
148
165
  </div>
166
+ {#if interactive}
167
+ <VideoControls
168
+ videoElement={video}
169
+ showRedo
170
+ {handle_trim_video}
171
+ {handle_reset_value}
172
+ bind:processingVideo
173
+ />
174
+ {/if}
149
175
 
150
176
  <style lang="postcss">
151
177
  span {
@@ -220,5 +246,6 @@
220
246
  background-color: var(--background-fill-secondary);
221
247
  height: var(--size-full);
222
248
  width: var(--size-full);
249
+ border-radius: var(--radius-xl);
223
250
  }
224
251
  </style>
@@ -19,6 +19,8 @@
19
19
 
20
20
  export let node: HTMLVideoElement | undefined = undefined;
21
21
 
22
+ export let processingVideo = false;
23
+
22
24
  const dispatch = createEventDispatcher();
23
25
  </script>
24
26
 
@@ -30,6 +32,11 @@
30
32
  For example, if we add {...$$props} or {...$$restProps}, the boolean props aside it like `controls` will be compiled as string "true" or "false" on the actual DOM.
31
33
  Then, even when `controls` is false, the compiled DOM would be `<video controls="false">` which is equivalent to `<video controls>` since the string "false" is even truthy.
32
34
  -->
35
+ <div class:hidden={!processingVideo} class="overlay">
36
+ <span class="load-wrap">
37
+ <span class="loader" />
38
+ </span>
39
+ </div>
33
40
  <video
34
41
  src={resolved_src}
35
42
  {muted}
@@ -52,6 +59,7 @@
52
59
  bind:this={node}
53
60
  use:loaded={{ autoplay: autoplay ?? false }}
54
61
  data-testid={$$props["data-testid"]}
62
+ crossorigin="anonymous"
55
63
  >
56
64
  <slot />
57
65
  </video>
@@ -60,11 +68,59 @@
60
68
  {/await}
61
69
 
62
70
  <style>
71
+ .overlay {
72
+ position: absolute;
73
+ background-color: rgba(0, 0, 0, 0.4);
74
+ width: 100%;
75
+ height: 100%;
76
+ }
77
+
78
+ .hidden {
79
+ display: none;
80
+ }
81
+
82
+ .load-wrap {
83
+ display: flex;
84
+ justify-content: center;
85
+ align-items: center;
86
+ height: 100%;
87
+ }
88
+
89
+ .loader {
90
+ display: flex;
91
+ position: relative;
92
+ background-color: var(--border-color-accent-subdued);
93
+ animation: shadowPulse 2s linear infinite;
94
+ box-shadow: -24px 0 var(--border-color-accent-subdued),
95
+ 24px 0 var(--border-color-accent-subdued);
96
+ margin: var(--spacing-md);
97
+ border-radius: 50%;
98
+ width: 10px;
99
+ height: 10px;
100
+ scale: 0.5;
101
+ }
102
+
103
+ @keyframes shadowPulse {
104
+ 33% {
105
+ box-shadow: -24px 0 var(--border-color-accent-subdued), 24px 0 #fff;
106
+ background: #fff;
107
+ }
108
+ 66% {
109
+ box-shadow: -24px 0 #fff, 24px 0 #fff;
110
+ background: var(--border-color-accent-subdued);
111
+ }
112
+ 100% {
113
+ box-shadow: -24px 0 #fff, 24px 0 var(--border-color-accent-subdued);
114
+ background: #fff;
115
+ }
116
+ }
117
+
63
118
  video {
64
119
  position: inherit;
65
120
  background-color: black;
66
121
  width: var(--size-full);
67
122
  height: var(--size-full);
68
123
  object-fit: contain;
124
+ border-radius: var(--radius-xl);
69
125
  }
70
126
  </style>
@@ -0,0 +1,214 @@
1
+ <script lang="ts">
2
+ import { Undo, Trim } from "@gradio/icons";
3
+ import VideoTimeline from "./VideoTimeline.svelte";
4
+ import { trimVideo } from "./utils";
5
+ import { FFmpeg } from "@ffmpeg/ffmpeg";
6
+ import loadFfmpeg from "./utils";
7
+ import { onMount } from "svelte";
8
+
9
+ export let videoElement: HTMLVideoElement;
10
+
11
+ export let showRedo = false;
12
+ export let interactive = true;
13
+ export let mode = "";
14
+ export let handle_reset_value: () => void;
15
+ export let handle_trim_video: (videoBlob: Blob) => void;
16
+ export let processingVideo = false;
17
+
18
+ let ffmpeg: FFmpeg;
19
+
20
+ onMount(async () => {
21
+ ffmpeg = await loadFfmpeg();
22
+ });
23
+
24
+ $: if (mode === "edit" && trimmedDuration === null && videoElement)
25
+ trimmedDuration = videoElement.duration;
26
+
27
+ const formatTime = (seconds: number): string => {
28
+ const minutes = Math.floor(seconds / 60);
29
+ const secondsRemainder = Math.round(seconds) % 60;
30
+ const paddedSeconds = `0${secondsRemainder}`.slice(-2);
31
+ return `${minutes}:${paddedSeconds}`;
32
+ };
33
+
34
+ let trimmedDuration: number | null = null;
35
+ let dragStart = 0;
36
+ let dragEnd = 0;
37
+
38
+ let loadingTimeline = false;
39
+
40
+ const toggleTrimmingMode = (): void => {
41
+ if (mode === "edit") {
42
+ mode = "";
43
+ trimmedDuration = videoElement.duration;
44
+ } else {
45
+ mode = "edit";
46
+ }
47
+ };
48
+ </script>
49
+
50
+ <div class="container">
51
+ {#if mode === "edit"}
52
+ <div class="timeline-wrapper">
53
+ <VideoTimeline
54
+ {videoElement}
55
+ bind:dragStart
56
+ bind:dragEnd
57
+ bind:trimmedDuration
58
+ bind:loadingTimeline
59
+ />
60
+ </div>
61
+ {/if}
62
+
63
+ <div class="controls" data-testid="waveform-controls">
64
+ {#if mode === "edit" && trimmedDuration !== null}
65
+ <time
66
+ aria-label="duration of selected region in seconds"
67
+ class:hidden={loadingTimeline}>{formatTime(trimmedDuration)}</time
68
+ >
69
+ {:else}
70
+ <div />
71
+ {/if}
72
+
73
+ <div class="settings-wrapper">
74
+ {#if showRedo && mode === ""}
75
+ <button
76
+ class="action icon"
77
+ disabled={processingVideo}
78
+ aria-label="Reset video to initial value"
79
+ on:click={() => {
80
+ handle_reset_value();
81
+ mode = "";
82
+ }}
83
+ >
84
+ <Undo />
85
+ </button>
86
+ {/if}
87
+
88
+ {#if interactive}
89
+ {#if mode === ""}
90
+ <button
91
+ disabled={processingVideo}
92
+ class="action icon"
93
+ aria-label="Trim video to selection"
94
+ on:click={toggleTrimmingMode}
95
+ >
96
+ <Trim />
97
+ </button>
98
+ {:else}
99
+ <button
100
+ class:hidden={loadingTimeline}
101
+ class="text-button"
102
+ on:click={() => {
103
+ mode = "";
104
+ processingVideo = true;
105
+ trimVideo(ffmpeg, dragStart, dragEnd, videoElement)
106
+ .then((videoBlob) => {
107
+ handle_trim_video(videoBlob);
108
+ })
109
+ .then(() => {
110
+ processingVideo = false;
111
+ });
112
+ }}>Trim</button
113
+ >
114
+ <button
115
+ class="text-button"
116
+ class:hidden={loadingTimeline}
117
+ on:click={toggleTrimmingMode}>Cancel</button
118
+ >
119
+ {/if}
120
+ {/if}
121
+ </div>
122
+ </div>
123
+ </div>
124
+
125
+ <style>
126
+ .container {
127
+ width: 100%;
128
+ }
129
+ time {
130
+ color: var(--color-accent);
131
+ font-weight: bold;
132
+ padding-left: var(--spacing-xs);
133
+ }
134
+
135
+ .timeline-wrapper {
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: center;
139
+ width: 100%;
140
+ }
141
+ .settings-wrapper {
142
+ display: flex;
143
+ justify-self: self-end;
144
+ }
145
+ .text-button {
146
+ border: 1px solid var(--neutral-400);
147
+ border-radius: var(--radius-sm);
148
+ font-weight: 300;
149
+ font-size: var(--size-3);
150
+ text-align: center;
151
+ color: var(--neutral-400);
152
+ height: var(--size-5);
153
+ font-weight: bold;
154
+ padding: 0 5px;
155
+ margin-left: 5px;
156
+ }
157
+ .hidden {
158
+ display: none;
159
+ }
160
+
161
+ .text-button:hover,
162
+ .text-button:focus {
163
+ color: var(--color-accent);
164
+ border-color: var(--color-accent);
165
+ }
166
+
167
+ .controls {
168
+ display: grid;
169
+ grid-template-columns: 1fr 1fr;
170
+ margin: var(--spacing-lg);
171
+ overflow: hidden;
172
+ text-align: left;
173
+ }
174
+
175
+ @media (max-width: 320px) {
176
+ .controls {
177
+ display: flex;
178
+ flex-wrap: wrap;
179
+ }
180
+
181
+ .controls * {
182
+ margin: var(--spacing-sm);
183
+ }
184
+
185
+ .controls .text-button {
186
+ margin-left: 0;
187
+ }
188
+ }
189
+ .action {
190
+ width: var(--size-5);
191
+ width: var(--size-5);
192
+ color: var(--neutral-400);
193
+ margin-left: var(--spacing-md);
194
+ }
195
+
196
+ .action:disabled {
197
+ cursor: not-allowed;
198
+ color: var(--border-color-accent-subdued);
199
+ }
200
+
201
+ .action:disabled:hover {
202
+ color: var(--border-color-accent-subdued);
203
+ }
204
+
205
+ .icon:hover,
206
+ .icon:focus {
207
+ color: var(--color-accent);
208
+ }
209
+
210
+ .container {
211
+ display: flex;
212
+ flex-direction: column;
213
+ }
214
+ </style>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { createEventDispatcher, afterUpdate, tick } from "svelte";
3
3
  import { BlockLabel, Empty, IconButton, ShareButton } from "@gradio/atoms";
4
- import type { FileData } from "@gradio/upload";
4
+ import type { FileData } from "@gradio/client";
5
5
  import { Video, Download } from "@gradio/icons";
6
6
  import { uploadToHuggingFace } from "@gradio/utils";
7
7
 
@@ -14,6 +14,7 @@
14
14
  export let show_label = true;
15
15
  export let autoplay: boolean;
16
16
  export let show_share_button = true;
17
+ export let show_download_button = true;
17
18
  export let i18n: I18nFormatter;
18
19
 
19
20
  let old_value: FileData | null = null;
@@ -47,13 +48,13 @@
47
48
  </script>
48
49
 
49
50
  <BlockLabel {show_label} Icon={Video} label={label || "Video"} />
50
- {#if value === null}
51
+ {#if value === null || value.url === undefined}
51
52
  <Empty unpadded_box={true} size="large"><Video /></Empty>
52
53
  {:else}
53
- {#key value.data}
54
+ {#key value.url}
54
55
  <Player
55
- src={value.data}
56
- subtitle={subtitle?.data}
56
+ src={value.url}
57
+ subtitle={subtitle?.url}
57
58
  {autoplay}
58
59
  on:play
59
60
  on:pause
@@ -61,16 +62,19 @@
61
62
  on:end
62
63
  mirror={false}
63
64
  {label}
65
+ interactive={false}
64
66
  />
65
67
  {/key}
66
68
  <div class="icon-buttons" data-testid="download-div">
67
- <a
68
- href={value.data}
69
- target={window.__is_colab__ ? "_blank" : null}
70
- download={value.orig_name || value.name}
71
- >
72
- <IconButton Icon={Download} label="Download" />
73
- </a>
69
+ {#if show_download_button}
70
+ <a
71
+ href={value.url}
72
+ target={window.__is_colab__ ? "_blank" : null}
73
+ download={value.orig_name || value.path}
74
+ >
75
+ <IconButton Icon={Download} label="Download" />
76
+ </a>
77
+ {/if}
74
78
  {#if show_share_button}
75
79
  <ShareButton
76
80
  {i18n}