@gradio/video 0.1.0-beta.5

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.
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import { playable } from "../shared";
3
+ import { onMount } from "svelte";
4
+
5
+ export let type: "gallery" | "table";
6
+ export let selected = false;
7
+ export let value: string;
8
+ export let samples_dir: string;
9
+ let video: HTMLVideoElement;
10
+
11
+ async function init(): Promise<void> {
12
+ video.muted = true;
13
+ video.playsInline = true;
14
+ video.controls = false;
15
+ video.setAttribute("muted", "");
16
+
17
+ await video.play();
18
+ video.pause();
19
+ }
20
+
21
+ onMount(() => {
22
+ init();
23
+ });
24
+ </script>
25
+
26
+ {#if playable()}
27
+ <video
28
+ muted
29
+ playsinline
30
+ bind:this={video}
31
+ class:table={type === "table"}
32
+ class:gallery={type === "gallery"}
33
+ class:selected
34
+ on:mouseover={video.play}
35
+ on:mouseout={video.pause}
36
+ src={samples_dir + value}
37
+ />
38
+ {:else}
39
+ <div>{value}</div>
40
+ {/if}
41
+
42
+ <style>
43
+ video {
44
+ flex: none;
45
+ border: 2px solid var(--border-color-primary);
46
+ border-radius: var(--radius-lg);
47
+ max-width: none;
48
+ }
49
+
50
+ video:hover,
51
+ video.selected {
52
+ border-color: var(--border-color-accent);
53
+ }
54
+ .table {
55
+ margin: 0 auto;
56
+ width: var(--size-20);
57
+ height: var(--size-20);
58
+ object-fit: cover;
59
+ }
60
+
61
+ .gallery {
62
+ max-height: var(--size-20);
63
+ object-fit: cover;
64
+ }
65
+ </style>
@@ -0,0 +1 @@
1
+ export { default } from "./Video.svelte";
@@ -0,0 +1,130 @@
1
+ <svelte:options accessors={true} />
2
+
3
+ <script lang="ts">
4
+ import type { Gradio } from "@gradio/utils";
5
+ import type { FileData } from "@gradio/upload";
6
+ import { normalise_file } from "@gradio/upload";
7
+ import { Block } from "@gradio/atoms";
8
+ import Video from "./Video.svelte";
9
+ import { UploadText } from "@gradio/atoms";
10
+
11
+ import { StatusTracker } from "@gradio/statustracker";
12
+ import type { LoadingStatus } from "@gradio/statustracker";
13
+
14
+ export let elem_id = "";
15
+ export let elem_classes: string[] = [];
16
+ export let visible = true;
17
+ export let value: { video: FileData; subtitles: FileData | null } | null =
18
+ null;
19
+ let old_value: { video: FileData; subtitles: FileData | null } | null = null;
20
+
21
+ export let label: string;
22
+ export let source: "upload" | "webcam";
23
+ export let root: string;
24
+ export let root_url: null | string;
25
+ export let show_label: boolean;
26
+ export let loading_status: LoadingStatus;
27
+ export let height: number | undefined;
28
+ export let width: number | undefined;
29
+ export let mirror_webcam: boolean;
30
+ export let include_audio: boolean;
31
+ export let container = false;
32
+ export let scale: number | null = null;
33
+ export let min_width: number | undefined = undefined;
34
+ export let autoplay = false;
35
+ export let gradio: Gradio<{
36
+ change: never;
37
+ clear: never;
38
+ play: never;
39
+ pause: never;
40
+ upload: never;
41
+ stop: never;
42
+ end: never;
43
+ start_recording: never;
44
+ stop_recording: never;
45
+ }>;
46
+
47
+ let _video: FileData | null = null;
48
+ let _subtitle: FileData | null = null;
49
+
50
+ $: {
51
+ if (value != null) {
52
+ _video = normalise_file(value.video, root, root_url);
53
+ _subtitle = normalise_file(value.subtitles, root, root_url);
54
+ } else {
55
+ _video = null;
56
+ _subtitle = null;
57
+ }
58
+ }
59
+
60
+ let dragging = false;
61
+
62
+ function handle_change({ detail }: CustomEvent<FileData | null>): void {
63
+ if (detail != null) {
64
+ value = { video: detail, subtitles: null } as {
65
+ video: FileData;
66
+ subtitles: FileData | null;
67
+ } | null;
68
+ } else {
69
+ value = null;
70
+ }
71
+ }
72
+
73
+ $: {
74
+ if (JSON.stringify(value) !== JSON.stringify(old_value)) {
75
+ old_value = value;
76
+ gradio.dispatch("change");
77
+ }
78
+ }
79
+ </script>
80
+
81
+ <Block
82
+ {visible}
83
+ variant={value === null && source === "upload" ? "dashed" : "solid"}
84
+ border_mode={dragging ? "focus" : "base"}
85
+ padding={false}
86
+ {elem_id}
87
+ {elem_classes}
88
+ {height}
89
+ {width}
90
+ {container}
91
+ {scale}
92
+ {min_width}
93
+ allow_overflow={false}
94
+ >
95
+ <StatusTracker
96
+ autoscroll={gradio.autoscroll}
97
+ i18n={gradio.i18n}
98
+ {...loading_status}
99
+ />
100
+
101
+ <Video
102
+ value={_video}
103
+ subtitle={_subtitle}
104
+ on:change={handle_change}
105
+ on:drag={({ detail }) => (dragging = detail)}
106
+ on:error={({ detail }) => {
107
+ loading_status = loading_status || {};
108
+ loading_status.status = "error";
109
+ loading_status.message = detail;
110
+ }}
111
+ {label}
112
+ {show_label}
113
+ {source}
114
+ {mirror_webcam}
115
+ {include_audio}
116
+ {autoplay}
117
+ {root}
118
+ on:clear={() => gradio.dispatch("clear")}
119
+ on:play={() => gradio.dispatch("play")}
120
+ on:pause={() => gradio.dispatch("pause")}
121
+ on:upload={() => gradio.dispatch("upload")}
122
+ on:stop={() => gradio.dispatch("stop")}
123
+ on:end={() => gradio.dispatch("end")}
124
+ on:start_recording={() => gradio.dispatch("start_recording")}
125
+ on:stop_recording={() => gradio.dispatch("stop_recording")}
126
+ i18n={gradio.i18n}
127
+ >
128
+ <UploadText i18n={gradio.i18n} type="video" />
129
+ </Video>
130
+ </Block>
@@ -0,0 +1,111 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+ import { Upload, ModifyUpload } from "@gradio/upload";
4
+ import type { FileData } from "@gradio/upload";
5
+ import { BlockLabel } from "@gradio/atoms";
6
+ import { Webcam } from "@gradio/image/interactive";
7
+ import { Video } from "@gradio/icons";
8
+
9
+ import { prettyBytes, playable } from "../shared/utils";
10
+ import Player from "../shared/Player.svelte";
11
+ import type { I18nFormatter } from "@gradio/utils";
12
+
13
+ export let value: FileData | null = null;
14
+ export let subtitle: FileData | null = null;
15
+ export let source: string;
16
+ export let label: string | undefined = undefined;
17
+ export let show_label = true;
18
+ export let mirror_webcam = false;
19
+ export let include_audio: boolean;
20
+ export let autoplay: boolean;
21
+ export let root: string;
22
+ export let i18n: I18nFormatter;
23
+
24
+ const dispatch = createEventDispatcher<{
25
+ change: any;
26
+ clear: undefined;
27
+ play: undefined;
28
+ pause: undefined;
29
+ end: undefined;
30
+ drag: boolean;
31
+ error: string;
32
+ upload: FileData;
33
+ start_recording: undefined;
34
+ stop_recording: undefined;
35
+ }>();
36
+
37
+ function handle_load({ detail }: CustomEvent<FileData | null>): void {
38
+ value = detail;
39
+ dispatch("change", detail);
40
+ dispatch("upload", detail!);
41
+ }
42
+
43
+ function handle_clear({ detail }: CustomEvent<FileData | null>): void {
44
+ value = null;
45
+ dispatch("change", detail);
46
+ dispatch("clear");
47
+ }
48
+
49
+ let dragging = false;
50
+ $: dispatch("drag", dragging);
51
+ </script>
52
+
53
+ <BlockLabel {show_label} Icon={Video} label={label || "Video"} />
54
+ {#if value === null}
55
+ {#if source === "upload"}
56
+ <Upload
57
+ bind:dragging
58
+ filetype="video/x-m4v,video/*"
59
+ on:load={handle_load}
60
+ {root}
61
+ >
62
+ <slot />
63
+ </Upload>
64
+ {:else if source === "webcam"}
65
+ <Webcam
66
+ {mirror_webcam}
67
+ {include_audio}
68
+ mode="video"
69
+ on:error
70
+ on:capture={({ detail }) => dispatch("change", detail)}
71
+ on:start_recording
72
+ on:stop_recording
73
+ {i18n}
74
+ />
75
+ {/if}
76
+ {:else}
77
+ <ModifyUpload {i18n} on:clear={handle_clear} />
78
+ {#if playable()}
79
+ {#key value?.data}
80
+ <Player
81
+ {autoplay}
82
+ src={value.data}
83
+ subtitle={subtitle?.data}
84
+ on:play
85
+ on:pause
86
+ on:stop
87
+ on:end
88
+ mirror={mirror_webcam && source === "webcam"}
89
+ {label}
90
+ />
91
+ {/key}
92
+ {:else if value.size}
93
+ <div class="file-name">{value.name}</div>
94
+ <div class="file-size">
95
+ {prettyBytes(value.size)}
96
+ </div>
97
+ {/if}
98
+ {/if}
99
+
100
+ <style>
101
+ .file-name {
102
+ padding: var(--size-6);
103
+ font-size: var(--text-xxl);
104
+ word-break: break-all;
105
+ }
106
+
107
+ .file-size {
108
+ padding: var(--size-2);
109
+ font-size: var(--text-xl);
110
+ }
111
+ </style>
@@ -0,0 +1 @@
1
+ export { default } from "./InteractiveVideo.svelte";
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@gradio/video",
3
+ "version": "0.1.0-beta.5",
4
+ "description": "Gradio UI packages",
5
+ "type": "module",
6
+ "main": "index.svelte",
7
+ "author": "",
8
+ "license": "ISC",
9
+ "private": false,
10
+ "dependencies": {
11
+ "@gradio/atoms": "^0.2.0-beta.3",
12
+ "@gradio/icons": "^0.2.0-beta.0",
13
+ "@gradio/image": "^0.3.0-beta.5",
14
+ "@gradio/statustracker": "^0.3.0-beta.5",
15
+ "@gradio/upload": "^0.3.0-beta.3",
16
+ "@gradio/utils": "^0.2.0-beta.3"
17
+ },
18
+ "main_changeset": true,
19
+ "exports": {
20
+ "./package.json": "./package.json",
21
+ "./interactive": "./interactive/index.ts",
22
+ "./static": "./static/index.ts",
23
+ "./example": "./example/index.ts"
24
+ }
25
+ }
@@ -0,0 +1,231 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+ import { Play, Pause, Maximise, Undo } from "@gradio/icons";
4
+ import { loaded } from "./utils";
5
+
6
+ export let src: string;
7
+ export let subtitle: string | null = null;
8
+ export let mirror: boolean;
9
+ export let autoplay: boolean;
10
+ export let label = "test";
11
+
12
+ const dispatch = createEventDispatcher<{
13
+ play: undefined;
14
+ pause: undefined;
15
+ stop: undefined;
16
+ end: undefined;
17
+ }>();
18
+
19
+ let time = 0;
20
+ let duration: number;
21
+ let paused = true;
22
+ let video: HTMLVideoElement;
23
+
24
+ function handleMove(e: TouchEvent | MouseEvent): void {
25
+ if (!duration) return;
26
+
27
+ if (e.type === "click") {
28
+ handle_click(e as MouseEvent);
29
+ return;
30
+ }
31
+
32
+ if (e.type !== "touchmove" && !((e as MouseEvent).buttons & 1)) return;
33
+
34
+ const clientX =
35
+ e.type === "touchmove"
36
+ ? (e as TouchEvent).touches[0].clientX
37
+ : (e as MouseEvent).clientX;
38
+ const { left, right } = (
39
+ e.currentTarget as HTMLProgressElement
40
+ ).getBoundingClientRect();
41
+ time = (duration * (clientX - left)) / (right - left);
42
+ }
43
+
44
+ async function play_pause(): Promise<void> {
45
+ if (document.fullscreenElement != video) {
46
+ const isPlaying =
47
+ video.currentTime > 0 &&
48
+ !video.paused &&
49
+ !video.ended &&
50
+ video.readyState > video.HAVE_CURRENT_DATA;
51
+
52
+ if (!isPlaying) {
53
+ await video.play();
54
+ } else video.pause();
55
+ }
56
+ }
57
+
58
+ function handle_click(e: MouseEvent): void {
59
+ const { left, right } = (
60
+ e.currentTarget as HTMLProgressElement
61
+ ).getBoundingClientRect();
62
+ time = (duration * (e.clientX - left)) / (right - left);
63
+ }
64
+
65
+ function format(seconds: number): string {
66
+ if (isNaN(seconds) || !isFinite(seconds)) return "...";
67
+
68
+ const minutes = Math.floor(seconds / 60);
69
+ let _seconds: number | string = Math.floor(seconds % 60);
70
+ if (_seconds < 10) _seconds = `0${_seconds}`;
71
+
72
+ return `${minutes}:${_seconds}`;
73
+ }
74
+
75
+ function handle_end(): void {
76
+ dispatch("stop");
77
+ dispatch("end");
78
+ }
79
+
80
+ function open_full_screen(): void {
81
+ video.requestFullscreen();
82
+ }
83
+ </script>
84
+
85
+ <div class="wrap">
86
+ <video
87
+ {src}
88
+ preload="auto"
89
+ on:click={play_pause}
90
+ on:play
91
+ on:pause
92
+ on:ended={handle_end}
93
+ bind:currentTime={time}
94
+ bind:duration
95
+ bind:paused
96
+ bind:this={video}
97
+ class:mirror
98
+ use:loaded={{ autoplay }}
99
+ data-testid={`${label}-player`}
100
+ >
101
+ <track kind="captions" src={subtitle} default />
102
+ </video>
103
+
104
+ <div class="controls">
105
+ <div class="inner">
106
+ <span
107
+ role="button"
108
+ tabindex="0"
109
+ class="icon"
110
+ aria-label="play-pause-replay-button"
111
+ on:click={play_pause}
112
+ on:keydown={play_pause}
113
+ >
114
+ {#if time === duration}
115
+ <Undo />
116
+ {:else if paused}
117
+ <Play />
118
+ {:else}
119
+ <Pause />
120
+ {/if}
121
+ </span>
122
+
123
+ <span class="time">{format(time)} / {format(duration)}</span>
124
+
125
+ <!-- TODO: implement accessible video timeline for 4.0 -->
126
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
127
+ <!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
128
+ <progress
129
+ value={time / duration || 0}
130
+ on:mousemove={handleMove}
131
+ on:touchmove|preventDefault={handleMove}
132
+ on:click|stopPropagation|preventDefault={handle_click}
133
+ />
134
+
135
+ <div
136
+ role="button"
137
+ tabindex="0"
138
+ class="icon"
139
+ aria-label="full-screen"
140
+ on:click={open_full_screen}
141
+ on:keypress={open_full_screen}
142
+ >
143
+ <Maximise />
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+
149
+ <style lang="postcss">
150
+ span {
151
+ text-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
152
+ }
153
+
154
+ progress {
155
+ margin-right: var(--size-3);
156
+ border-radius: var(--radius-sm);
157
+ width: var(--size-full);
158
+ height: var(--size-2);
159
+ }
160
+
161
+ progress::-webkit-progress-bar {
162
+ border-radius: 2px;
163
+ background-color: rgba(255, 255, 255, 0.2);
164
+ overflow: hidden;
165
+ }
166
+
167
+ progress::-webkit-progress-value {
168
+ background-color: rgba(255, 255, 255, 0.9);
169
+ }
170
+
171
+ video {
172
+ position: inherit;
173
+ background-color: black;
174
+ width: var(--size-full);
175
+ height: var(--size-full);
176
+ object-fit: contain;
177
+ }
178
+
179
+ .mirror {
180
+ transform: scaleX(-1);
181
+ }
182
+
183
+ .controls {
184
+ position: absolute;
185
+ bottom: 0;
186
+ opacity: 0;
187
+ transition: 500ms;
188
+ margin: var(--size-2);
189
+ border-radius: var(--radius-md);
190
+ background: var(--color-grey-800);
191
+ padding: var(--size-2) var(--size-1);
192
+ width: calc(100% - 0.375rem * 2);
193
+ width: calc(100% - var(--size-2) * 2);
194
+ }
195
+ .wrap:hover .controls {
196
+ opacity: 1;
197
+ }
198
+
199
+ .inner {
200
+ display: flex;
201
+ justify-content: space-between;
202
+ align-items: center;
203
+ padding-right: var(--size-2);
204
+ padding-left: var(--size-2);
205
+ width: var(--size-full);
206
+ height: var(--size-full);
207
+ }
208
+
209
+ .icon {
210
+ display: flex;
211
+ justify-content: center;
212
+ cursor: pointer;
213
+ width: var(--size-6);
214
+ color: white;
215
+ }
216
+
217
+ .time {
218
+ flex-shrink: 0;
219
+ margin-right: var(--size-3);
220
+ margin-left: var(--size-3);
221
+ color: white;
222
+ font-size: var(--text-sm);
223
+ font-family: var(--font-mono);
224
+ }
225
+ .wrap {
226
+ position: relative;
227
+ background-color: var(--background-fill-secondary);
228
+ height: var(--size-full);
229
+ width: var(--size-full);
230
+ }
231
+ </style>
@@ -0,0 +1,2 @@
1
+ export * from "./utils";
2
+ export { default as Player } from "./Player.svelte";
@@ -0,0 +1,38 @@
1
+ import type { ActionReturn } from "svelte/action";
2
+
3
+ export const prettyBytes = (bytes: number): string => {
4
+ let units = ["B", "KB", "MB", "GB", "PB"];
5
+ let i = 0;
6
+ while (bytes > 1024) {
7
+ bytes /= 1024;
8
+ i++;
9
+ }
10
+ let unit = units[i];
11
+ return bytes.toFixed(1) + " " + unit;
12
+ };
13
+
14
+ export const playable = (): boolean => {
15
+ // TODO: Fix this
16
+ // let video_element = document.createElement("video");
17
+ // let mime_type = mime.lookup(filename);
18
+ // return video_element.canPlayType(mime_type) != "";
19
+ return true; // FIX BEFORE COMMIT - mime import causing issues
20
+ };
21
+
22
+ export function loaded(
23
+ node: HTMLVideoElement,
24
+ { autoplay }: { autoplay: boolean }
25
+ ): ActionReturn {
26
+ async function handle_playback(): Promise<void> {
27
+ if (!autoplay) return;
28
+ await node.play();
29
+ }
30
+
31
+ node.addEventListener("loadeddata", handle_playback);
32
+
33
+ return {
34
+ destroy(): void {
35
+ node.removeEventListener("loadeddata", handle_playback);
36
+ }
37
+ };
38
+ }