@gradio/video 0.10.3 → 0.10.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.
@@ -0,0 +1,85 @@
1
+ <script>import { createEventDispatcher, afterUpdate, tick } from "svelte";
2
+ import { BlockLabel, Empty, IconButton, ShareButton } from "@gradio/atoms";
3
+ import { Video, Download } from "@gradio/icons";
4
+ import { uploadToHuggingFace } from "@gradio/utils";
5
+ import { DownloadLink } from "@gradio/wasm/svelte";
6
+ import Player from "./Player.svelte";
7
+ export let value = null;
8
+ export let subtitle = null;
9
+ export let label = void 0;
10
+ export let show_label = true;
11
+ export let autoplay;
12
+ export let show_share_button = true;
13
+ export let show_download_button = true;
14
+ export let loop;
15
+ export let i18n;
16
+ export let upload;
17
+ let old_value = null;
18
+ let old_subtitle = null;
19
+ const dispatch = createEventDispatcher();
20
+ $:
21
+ value && dispatch("change", value);
22
+ afterUpdate(async () => {
23
+ if (value !== old_value && subtitle !== old_subtitle && old_subtitle !== null) {
24
+ old_value = value;
25
+ value = null;
26
+ await tick();
27
+ value = old_value;
28
+ }
29
+ old_value = value;
30
+ old_subtitle = subtitle;
31
+ });
32
+ </script>
33
+
34
+ <BlockLabel {show_label} Icon={Video} label={label || "Video"} />
35
+ {#if value === null || value.url === undefined}
36
+ <Empty unpadded_box={true} size="large"><Video /></Empty>
37
+ {:else}
38
+ {#key value.url}
39
+ <Player
40
+ src={value.url}
41
+ subtitle={subtitle?.url}
42
+ {autoplay}
43
+ on:play
44
+ on:pause
45
+ on:stop
46
+ on:end
47
+ on:load
48
+ mirror={false}
49
+ {label}
50
+ {loop}
51
+ interactive={false}
52
+ {upload}
53
+ />
54
+ {/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}
74
+ </div>
75
+ {/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>
@@ -0,0 +1,36 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import type { FileData, Client } from "@gradio/client";
3
+ import type { I18nFormatter } from "js/core/src/gradio_helper";
4
+ declare const __propDef: {
5
+ props: {
6
+ value?: (FileData | null) | undefined;
7
+ subtitle?: (FileData | null) | undefined;
8
+ label?: string | undefined;
9
+ show_label?: boolean | undefined;
10
+ autoplay: boolean;
11
+ show_share_button?: boolean | undefined;
12
+ show_download_button?: boolean | undefined;
13
+ loop: boolean;
14
+ i18n: I18nFormatter;
15
+ upload: Client["upload"];
16
+ };
17
+ events: {
18
+ play: CustomEvent<any>;
19
+ pause: CustomEvent<any>;
20
+ stop: CustomEvent<any>;
21
+ end: CustomEvent<any>;
22
+ load: Event;
23
+ error: CustomEvent<string>;
24
+ share: CustomEvent<import("@gradio/utils").ShareData>;
25
+ change: CustomEvent<FileData>;
26
+ } & {
27
+ [evt: string]: CustomEvent<any>;
28
+ };
29
+ slots: {};
30
+ };
31
+ export type VideoPreviewProps = typeof __propDef.props;
32
+ export type VideoPreviewEvents = typeof __propDef.events;
33
+ export type VideoPreviewSlots = typeof __propDef.slots;
34
+ export default class VideoPreview extends SvelteComponent<VideoPreviewProps, VideoPreviewEvents, VideoPreviewSlots> {
35
+ }
36
+ export {};
@@ -0,0 +1,240 @@
1
+ <script>import { onMount, onDestroy } from "svelte";
2
+ export let videoElement;
3
+ export let trimmedDuration;
4
+ export let dragStart;
5
+ export let dragEnd;
6
+ export let loadingTimeline;
7
+ let thumbnails = [];
8
+ let numberOfThumbnails = 10;
9
+ let intervalId;
10
+ let videoDuration;
11
+ let leftHandlePosition = 0;
12
+ let rightHandlePosition = 100;
13
+ let dragging = null;
14
+ const startDragging = (side) => {
15
+ dragging = side;
16
+ };
17
+ $:
18
+ loadingTimeline = thumbnails.length !== numberOfThumbnails;
19
+ const stopDragging = () => {
20
+ dragging = null;
21
+ };
22
+ const drag = (event, distance) => {
23
+ if (dragging) {
24
+ const timeline = document.getElementById("timeline");
25
+ if (!timeline)
26
+ return;
27
+ const rect = timeline.getBoundingClientRect();
28
+ let newPercentage = (event.clientX - rect.left) / rect.width * 100;
29
+ if (distance) {
30
+ newPercentage = dragging === "left" ? leftHandlePosition + distance : rightHandlePosition + distance;
31
+ } else {
32
+ newPercentage = (event.clientX - rect.left) / rect.width * 100;
33
+ }
34
+ newPercentage = Math.max(0, Math.min(newPercentage, 100));
35
+ if (dragging === "left") {
36
+ leftHandlePosition = Math.min(newPercentage, rightHandlePosition);
37
+ const newTimeLeft = leftHandlePosition / 100 * videoDuration;
38
+ videoElement.currentTime = newTimeLeft;
39
+ dragStart = newTimeLeft;
40
+ } else if (dragging === "right") {
41
+ rightHandlePosition = Math.max(newPercentage, leftHandlePosition);
42
+ const newTimeRight = rightHandlePosition / 100 * videoDuration;
43
+ videoElement.currentTime = newTimeRight;
44
+ dragEnd = newTimeRight;
45
+ }
46
+ const startTime = leftHandlePosition / 100 * videoDuration;
47
+ const endTime = rightHandlePosition / 100 * videoDuration;
48
+ trimmedDuration = endTime - startTime;
49
+ leftHandlePosition = leftHandlePosition;
50
+ rightHandlePosition = rightHandlePosition;
51
+ }
52
+ };
53
+ const moveHandle = (e) => {
54
+ if (dragging) {
55
+ const distance = 1 / videoDuration * 100;
56
+ if (e.key === "ArrowLeft") {
57
+ drag({ clientX: 0 }, -distance);
58
+ } else if (e.key === "ArrowRight") {
59
+ drag({ clientX: 0 }, distance);
60
+ }
61
+ }
62
+ };
63
+ const generateThumbnail = () => {
64
+ const canvas = document.createElement("canvas");
65
+ const ctx = canvas.getContext("2d");
66
+ if (!ctx)
67
+ return;
68
+ canvas.width = videoElement.videoWidth;
69
+ canvas.height = videoElement.videoHeight;
70
+ ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
71
+ const thumbnail = canvas.toDataURL("image/jpeg", 0.7);
72
+ thumbnails = [...thumbnails, thumbnail];
73
+ };
74
+ onMount(() => {
75
+ const loadMetadata = () => {
76
+ videoDuration = videoElement.duration;
77
+ const interval = videoDuration / numberOfThumbnails;
78
+ let captures = 0;
79
+ const onSeeked = () => {
80
+ generateThumbnail();
81
+ captures++;
82
+ if (captures < numberOfThumbnails) {
83
+ videoElement.currentTime += interval;
84
+ } else {
85
+ videoElement.removeEventListener("seeked", onSeeked);
86
+ }
87
+ };
88
+ videoElement.addEventListener("seeked", onSeeked);
89
+ videoElement.currentTime = 0;
90
+ };
91
+ if (videoElement.readyState >= 1) {
92
+ loadMetadata();
93
+ } else {
94
+ videoElement.addEventListener("loadedmetadata", loadMetadata);
95
+ }
96
+ });
97
+ onDestroy(() => {
98
+ window.removeEventListener("mousemove", drag);
99
+ window.removeEventListener("mouseup", stopDragging);
100
+ window.removeEventListener("keydown", moveHandle);
101
+ if (intervalId !== void 0) {
102
+ clearInterval(intervalId);
103
+ }
104
+ });
105
+ onMount(() => {
106
+ window.addEventListener("mousemove", drag);
107
+ window.addEventListener("mouseup", stopDragging);
108
+ window.addEventListener("keydown", moveHandle);
109
+ });
110
+ </script>
111
+
112
+ <div class="container">
113
+ {#if loadingTimeline}
114
+ <div class="load-wrap">
115
+ <span aria-label="loading timeline" class="loader" />
116
+ </div>
117
+ {:else}
118
+ <div id="timeline" class="thumbnail-wrapper">
119
+ <button
120
+ aria-label="start drag handle for trimming video"
121
+ class="handle left"
122
+ on:mousedown={() => startDragging("left")}
123
+ on:blur={stopDragging}
124
+ on:keydown={(e) => {
125
+ if (e.key === "ArrowLeft" || e.key == "ArrowRight") {
126
+ startDragging("left");
127
+ }
128
+ }}
129
+ style="left: {leftHandlePosition}%;"
130
+ />
131
+
132
+ <div
133
+ class="opaque-layer"
134
+ style="left: {leftHandlePosition}%; right: {100 - rightHandlePosition}%"
135
+ />
136
+
137
+ {#each thumbnails as thumbnail, i (i)}
138
+ <img src={thumbnail} alt={`frame-${i}`} draggable="false" />
139
+ {/each}
140
+ <button
141
+ aria-label="end drag handle for trimming video"
142
+ class="handle right"
143
+ on:mousedown={() => startDragging("right")}
144
+ on:blur={stopDragging}
145
+ on:keydown={(e) => {
146
+ if (e.key === "ArrowLeft" || e.key == "ArrowRight") {
147
+ startDragging("right");
148
+ }
149
+ }}
150
+ style="left: {rightHandlePosition}%;"
151
+ />
152
+ </div>
153
+ {/if}
154
+ </div>
155
+
156
+ <style>
157
+ .load-wrap {
158
+ display: flex;
159
+ justify-content: center;
160
+ align-items: center;
161
+ height: 100%;
162
+ }
163
+ .loader {
164
+ display: flex;
165
+ position: relative;
166
+ background-color: var(--border-color-accent-subdued);
167
+ animation: shadowPulse 2s linear infinite;
168
+ box-shadow:
169
+ -24px 0 var(--border-color-accent-subdued),
170
+ 24px 0 var(--border-color-accent-subdued);
171
+ margin: var(--spacing-md);
172
+ border-radius: 50%;
173
+ width: 10px;
174
+ height: 10px;
175
+ scale: 0.5;
176
+ }
177
+
178
+ @keyframes shadowPulse {
179
+ 33% {
180
+ box-shadow:
181
+ -24px 0 var(--border-color-accent-subdued),
182
+ 24px 0 #fff;
183
+ background: #fff;
184
+ }
185
+ 66% {
186
+ box-shadow:
187
+ -24px 0 #fff,
188
+ 24px 0 #fff;
189
+ background: var(--border-color-accent-subdued);
190
+ }
191
+ 100% {
192
+ box-shadow:
193
+ -24px 0 #fff,
194
+ 24px 0 var(--border-color-accent-subdued);
195
+ background: #fff;
196
+ }
197
+ }
198
+
199
+ .container {
200
+ display: flex;
201
+ flex-direction: column;
202
+ align-items: center;
203
+ justify-content: center;
204
+ margin: var(--spacing-lg) var(--spacing-lg) 0 var(--spacing-lg);
205
+ }
206
+
207
+ #timeline {
208
+ display: flex;
209
+ height: var(--size-10);
210
+ flex: 1;
211
+ position: relative;
212
+ }
213
+
214
+ img {
215
+ flex: 1 1 auto;
216
+ min-width: 0;
217
+ object-fit: cover;
218
+ height: var(--size-12);
219
+ border: 1px solid var(--block-border-color);
220
+ user-select: none;
221
+ z-index: 1;
222
+ }
223
+
224
+ .handle {
225
+ width: 3px;
226
+ background-color: var(--color-accent);
227
+ cursor: ew-resize;
228
+ height: var(--size-12);
229
+ z-index: 3;
230
+ position: absolute;
231
+ }
232
+
233
+ .opaque-layer {
234
+ background-color: rgba(230, 103, 40, 0.25);
235
+ border: 1px solid var(--color-accent);
236
+ height: var(--size-12);
237
+ position: absolute;
238
+ z-index: 2;
239
+ }
240
+ </style>
@@ -0,0 +1,20 @@
1
+ import { SvelteComponent } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ videoElement: HTMLVideoElement;
5
+ trimmedDuration: number | null;
6
+ dragStart: number;
7
+ dragEnd: number;
8
+ loadingTimeline: boolean;
9
+ };
10
+ events: {
11
+ [evt: string]: CustomEvent<any>;
12
+ };
13
+ slots: {};
14
+ };
15
+ export type VideoTimelineProps = typeof __propDef.props;
16
+ export type VideoTimelineEvents = typeof __propDef.events;
17
+ export type VideoTimelineSlots = typeof __propDef.slots;
18
+ export default class VideoTimeline extends SvelteComponent<VideoTimelineProps, VideoTimelineEvents, VideoTimelineSlots> {
19
+ }
20
+ export {};
@@ -0,0 +1 @@
1
+ export { default as Video } from "./Video.svelte";
@@ -0,0 +1 @@
1
+ export { default as Video } from "./Video.svelte";
@@ -0,0 +1,9 @@
1
+ import { FFmpeg } from "@ffmpeg/ffmpeg";
2
+ export declare const prettyBytes: (bytes: number) => string;
3
+ export declare const playable: () => boolean;
4
+ export declare function loaded(node: HTMLVideoElement, { autoplay }: {
5
+ autoplay: boolean;
6
+ }): any;
7
+ export default function loadFfmpeg(): Promise<FFmpeg>;
8
+ export declare function blob_to_data_url(blob: Blob): Promise<string>;
9
+ export declare function trimVideo(ffmpeg: FFmpeg, startTime: number, endTime: number, videoElement: HTMLVideoElement): Promise<any>;
@@ -0,0 +1,120 @@
1
+ import { toBlobURL } from "@ffmpeg/util";
2
+ import { FFmpeg } from "@ffmpeg/ffmpeg";
3
+ import { lookup } from "mrmime";
4
+ export const prettyBytes = (bytes) => {
5
+ let units = ["B", "KB", "MB", "GB", "PB"];
6
+ let i = 0;
7
+ while (bytes > 1024) {
8
+ bytes /= 1024;
9
+ i++;
10
+ }
11
+ let unit = units[i];
12
+ return bytes.toFixed(1) + " " + unit;
13
+ };
14
+ export const playable = () => {
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
+ export function loaded(node, { autoplay }) {
22
+ async function handle_playback() {
23
+ if (!autoplay)
24
+ return;
25
+ await node.play();
26
+ }
27
+ node.addEventListener("loadeddata", handle_playback);
28
+ return {
29
+ destroy() {
30
+ node.removeEventListener("loadeddata", handle_playback);
31
+ }
32
+ };
33
+ }
34
+ export default async function loadFfmpeg() {
35
+ const ffmpeg = new FFmpeg();
36
+ const baseURL = "https://unpkg.com/@ffmpeg/core@0.12.4/dist/esm";
37
+ await ffmpeg.load({
38
+ coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
39
+ wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm")
40
+ });
41
+ return ffmpeg;
42
+ }
43
+ export function blob_to_data_url(blob) {
44
+ return new Promise((fulfill, reject) => {
45
+ let reader = new FileReader();
46
+ reader.onerror = reject;
47
+ reader.onload = () => fulfill(reader.result);
48
+ reader.readAsDataURL(blob);
49
+ });
50
+ }
51
+ export async function trimVideo(ffmpeg, startTime, endTime, videoElement) {
52
+ const videoUrl = videoElement.src;
53
+ const mimeType = lookup(videoElement.src) || "video/mp4";
54
+ const blobUrl = await toBlobURL(videoUrl, mimeType);
55
+ const response = await fetch(blobUrl);
56
+ const vidBlob = await response.blob();
57
+ const type = getVideoExtensionFromMimeType(mimeType) || "mp4";
58
+ const inputName = `input.${type}`;
59
+ const outputName = `output.${type}`;
60
+ try {
61
+ if (startTime === 0 && endTime === 0) {
62
+ return vidBlob;
63
+ }
64
+ await ffmpeg.writeFile(inputName, new Uint8Array(await vidBlob.arrayBuffer()));
65
+ let command = [
66
+ "-i",
67
+ inputName,
68
+ ...(startTime !== 0 ? ["-ss", startTime.toString()] : []),
69
+ ...(endTime !== 0 ? ["-to", endTime.toString()] : []),
70
+ "-c:a",
71
+ "copy",
72
+ outputName
73
+ ];
74
+ await ffmpeg.exec(command);
75
+ const outputData = await ffmpeg.readFile(outputName);
76
+ const outputBlob = new Blob([outputData], {
77
+ type: `video/${type}`
78
+ });
79
+ return outputBlob;
80
+ }
81
+ catch (error) {
82
+ console.error("Error initializing FFmpeg:", error);
83
+ return vidBlob;
84
+ }
85
+ }
86
+ const getVideoExtensionFromMimeType = (mimeType) => {
87
+ const videoMimeToExtensionMap = {
88
+ "video/mp4": "mp4",
89
+ "video/webm": "webm",
90
+ "video/ogg": "ogv",
91
+ "video/quicktime": "mov",
92
+ "video/x-msvideo": "avi",
93
+ "video/x-matroska": "mkv",
94
+ "video/mpeg": "mpeg",
95
+ "video/3gpp": "3gp",
96
+ "video/3gpp2": "3g2",
97
+ "video/h261": "h261",
98
+ "video/h263": "h263",
99
+ "video/h264": "h264",
100
+ "video/jpeg": "jpgv",
101
+ "video/jpm": "jpm",
102
+ "video/mj2": "mj2",
103
+ "video/mpv": "mpv",
104
+ "video/vnd.ms-playready.media.pyv": "pyv",
105
+ "video/vnd.uvvu.mp4": "uvu",
106
+ "video/vnd.vivo": "viv",
107
+ "video/x-f4v": "f4v",
108
+ "video/x-fli": "fli",
109
+ "video/x-flv": "flv",
110
+ "video/x-m4v": "m4v",
111
+ "video/x-ms-asf": "asf",
112
+ "video/x-ms-wm": "wm",
113
+ "video/x-ms-wmv": "wmv",
114
+ "video/x-ms-wmx": "wmx",
115
+ "video/x-ms-wvx": "wvx",
116
+ "video/x-sgi-movie": "movie",
117
+ "video/x-smv": "smv"
118
+ };
119
+ return videoMimeToExtensionMap[mimeType] || null;
120
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/video",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -10,24 +10,43 @@
10
10
  "@ffmpeg/ffmpeg": "^0.12.7",
11
11
  "@ffmpeg/util": "^0.12.1",
12
12
  "mrmime": "^2.0.0",
13
- "@gradio/atoms": "^0.8.0",
14
- "@gradio/client": "^1.5.1",
15
- "@gradio/icons": "^0.7.1",
16
- "@gradio/statustracker": "^0.7.5",
17
- "@gradio/image": "^0.15.0",
18
- "@gradio/utils": "^0.6.0",
19
- "@gradio/wasm": "^0.13.0",
20
- "@gradio/upload": "^0.12.3"
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"
21
21
  },
22
22
  "devDependencies": {
23
- "@gradio/preview": "^0.11.0"
23
+ "@gradio/preview": "^0.11.1"
24
24
  },
25
25
  "exports": {
26
- ".": "./index.ts",
27
- "./example": "./Example.svelte",
28
- "./shared": "./shared/index.ts",
29
- "./base": "./shared/VideoPreview.svelte",
30
- "./package.json": "./package.json"
26
+ "./package.json": "./package.json",
27
+ ".": {
28
+ "gradio": "./index.ts",
29
+ "svelte": "./dist/index.js",
30
+ "types": "./dist/index.d.ts"
31
+ },
32
+ "./example": {
33
+ "gradio": "./Example.svelte",
34
+ "svelte": "./dist/Example.svelte",
35
+ "types": "./dist/Example.svelte.d.ts"
36
+ },
37
+ "./shared": {
38
+ "gradio": "./shared/index.ts",
39
+ "svelte": "./dist/shared/index.js",
40
+ "types": "./dist/shared/index.d.ts"
41
+ },
42
+ "./base": {
43
+ "gradio": "./shared/VideoPreview.svelte",
44
+ "svelte": "./dist/shared/VideoPreview.svelte",
45
+ "types": "./dist/shared/VideoPreview.svelte.d.ts"
46
+ }
47
+ },
48
+ "peerDependencies": {
49
+ "svelte": "^4.0.0"
31
50
  },
32
51
  "main": "index.ts",
33
52
  "main_changeset": true,