@gradio/video 0.1.0-beta.7 → 0.1.0

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,270 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from "svelte";
3
+
4
+ export let videoElement: HTMLVideoElement;
5
+ export let trimmedDuration: number | null;
6
+ export let dragStart: number;
7
+ export let dragEnd: number;
8
+ export let loadingTimeline: boolean;
9
+
10
+ let thumbnails: string[] = [];
11
+ let numberOfThumbnails = 10;
12
+ let intervalId: number | NodeJS.Timer;
13
+ let videoDuration: number;
14
+
15
+ let leftHandlePosition = 0;
16
+ let rightHandlePosition = 100;
17
+
18
+ let dragging: string | null = null;
19
+
20
+ const startDragging = (side: string | null): void => {
21
+ dragging = side;
22
+ };
23
+
24
+ $: loadingTimeline = thumbnails.length !== numberOfThumbnails;
25
+
26
+ const stopDragging = (): void => {
27
+ dragging = null;
28
+ };
29
+
30
+ const drag = (event: { clientX: number }, distance?: number): void => {
31
+ if (dragging) {
32
+ const timeline = document.getElementById("timeline");
33
+
34
+ if (!timeline) return;
35
+
36
+ const rect = timeline.getBoundingClientRect();
37
+ let newPercentage = ((event.clientX - rect.left) / rect.width) * 100;
38
+
39
+ if (distance) {
40
+ // Move handle based on arrow key press
41
+ newPercentage =
42
+ dragging === "left"
43
+ ? leftHandlePosition + distance
44
+ : rightHandlePosition + distance;
45
+ } else {
46
+ // Move handle based on mouse drag
47
+ newPercentage = ((event.clientX - rect.left) / rect.width) * 100;
48
+ }
49
+
50
+ newPercentage = Math.max(0, Math.min(newPercentage, 100)); // Keep within 0 and 100
51
+
52
+ if (dragging === "left") {
53
+ leftHandlePosition = Math.min(newPercentage, rightHandlePosition);
54
+
55
+ // Calculate the new time and set it for the videoElement
56
+ const newTimeLeft = (leftHandlePosition / 100) * videoDuration;
57
+ videoElement.currentTime = newTimeLeft;
58
+
59
+ dragStart = newTimeLeft;
60
+ } else if (dragging === "right") {
61
+ rightHandlePosition = Math.max(newPercentage, leftHandlePosition);
62
+
63
+ const newTimeRight = (rightHandlePosition / 100) * videoDuration;
64
+ videoElement.currentTime = newTimeRight;
65
+
66
+ dragEnd = newTimeRight;
67
+ }
68
+
69
+ const startTime = (leftHandlePosition / 100) * videoDuration;
70
+ const endTime = (rightHandlePosition / 100) * videoDuration;
71
+ trimmedDuration = endTime - startTime;
72
+
73
+ leftHandlePosition = leftHandlePosition;
74
+ rightHandlePosition = rightHandlePosition;
75
+ }
76
+ };
77
+
78
+ const moveHandle = (e: KeyboardEvent): void => {
79
+ if (dragging) {
80
+ // Calculate the movement distance as a percentage of the video duration
81
+ const distance = (1 / videoDuration) * 100;
82
+
83
+ if (e.key === "ArrowLeft") {
84
+ drag({ clientX: 0 }, -distance);
85
+ } else if (e.key === "ArrowRight") {
86
+ drag({ clientX: 0 }, distance);
87
+ }
88
+ }
89
+ };
90
+
91
+ const generateThumbnail = (): void => {
92
+ const canvas = document.createElement("canvas");
93
+ const ctx = canvas.getContext("2d");
94
+ if (!ctx) return;
95
+
96
+ canvas.width = videoElement.videoWidth;
97
+ canvas.height = videoElement.videoHeight;
98
+
99
+ ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
100
+
101
+ const thumbnail: string = canvas.toDataURL("image/jpeg", 0.7);
102
+ thumbnails = [...thumbnails, thumbnail];
103
+ };
104
+
105
+ onMount(() => {
106
+ const loadMetadata = (): void => {
107
+ videoDuration = videoElement.duration;
108
+
109
+ const interval = videoDuration / numberOfThumbnails;
110
+ let captures = 0;
111
+
112
+ const onSeeked = (): void => {
113
+ generateThumbnail();
114
+ captures++;
115
+
116
+ if (captures < numberOfThumbnails) {
117
+ videoElement.currentTime += interval;
118
+ } else {
119
+ videoElement.removeEventListener("seeked", onSeeked);
120
+ }
121
+ };
122
+
123
+ videoElement.addEventListener("seeked", onSeeked);
124
+ videoElement.currentTime = 0;
125
+ };
126
+
127
+ if (videoElement.readyState >= 1) {
128
+ loadMetadata();
129
+ } else {
130
+ videoElement.addEventListener("loadedmetadata", loadMetadata);
131
+ }
132
+ });
133
+
134
+ onDestroy(() => {
135
+ window.removeEventListener("mousemove", drag);
136
+ window.removeEventListener("mouseup", stopDragging);
137
+ window.removeEventListener("keydown", moveHandle);
138
+
139
+ if (intervalId) {
140
+ clearInterval(intervalId);
141
+ }
142
+ });
143
+
144
+ onMount(() => {
145
+ window.addEventListener("mousemove", drag);
146
+ window.addEventListener("mouseup", stopDragging);
147
+ window.addEventListener("keydown", moveHandle);
148
+ });
149
+ </script>
150
+
151
+ <div class="container">
152
+ {#if loadingTimeline}
153
+ <div class="load-wrap">
154
+ <span aria-label="loading timeline" class="loader" />
155
+ </div>
156
+ {:else}
157
+ <div id="timeline" class="thumbnail-wrapper">
158
+ <button
159
+ class="handle left"
160
+ on:mousedown={() => startDragging("left")}
161
+ on:blur={stopDragging}
162
+ on:keydown={(e) => {
163
+ if (e.key === "ArrowLeft" || e.key == "ArrowRight") {
164
+ startDragging("left");
165
+ }
166
+ }}
167
+ style="left: {leftHandlePosition}%;"
168
+ />
169
+
170
+ <div
171
+ class="opaque-layer"
172
+ style="left: {leftHandlePosition}%; right: {100 - rightHandlePosition}%"
173
+ />
174
+
175
+ {#each thumbnails as thumbnail, i (i)}
176
+ <img src={thumbnail} alt={`frame-${i}`} draggable="false" />
177
+ {/each}
178
+ <button
179
+ class="handle right"
180
+ on:mousedown={() => startDragging("right")}
181
+ on:blur={stopDragging}
182
+ on:keydown={(e) => {
183
+ if (e.key === "ArrowLeft" || e.key == "ArrowRight") {
184
+ startDragging("right");
185
+ }
186
+ }}
187
+ style="left: {rightHandlePosition}%;"
188
+ />
189
+ </div>
190
+ {/if}
191
+ </div>
192
+
193
+ <style>
194
+ .load-wrap {
195
+ display: flex;
196
+ justify-content: center;
197
+ align-items: center;
198
+ height: 100%;
199
+ }
200
+ .loader {
201
+ display: flex;
202
+ position: relative;
203
+ background-color: var(--border-color-accent-subdued);
204
+ animation: shadowPulse 2s linear infinite;
205
+ box-shadow: -24px 0 var(--border-color-accent-subdued),
206
+ 24px 0 var(--border-color-accent-subdued);
207
+ margin: var(--spacing-md);
208
+ border-radius: 50%;
209
+ width: 10px;
210
+ height: 10px;
211
+ scale: 0.5;
212
+ }
213
+
214
+ @keyframes shadowPulse {
215
+ 33% {
216
+ box-shadow: -24px 0 var(--border-color-accent-subdued), 24px 0 #fff;
217
+ background: #fff;
218
+ }
219
+ 66% {
220
+ box-shadow: -24px 0 #fff, 24px 0 #fff;
221
+ background: var(--border-color-accent-subdued);
222
+ }
223
+ 100% {
224
+ box-shadow: -24px 0 #fff, 24px 0 var(--border-color-accent-subdued);
225
+ background: #fff;
226
+ }
227
+ }
228
+
229
+ .container {
230
+ display: flex;
231
+ flex-direction: column;
232
+ align-items: center;
233
+ justify-content: center;
234
+ margin: var(--spacing-lg) var(--spacing-lg) 0 var(--spacing-lg);
235
+ }
236
+
237
+ #timeline {
238
+ display: flex;
239
+ height: var(--size-10);
240
+ flex: 1;
241
+ position: relative;
242
+ }
243
+
244
+ img {
245
+ flex: 1 1 auto;
246
+ min-width: 0;
247
+ object-fit: cover;
248
+ height: var(--size-12);
249
+ border: 1px solid var(--block-border-color);
250
+ user-select: none;
251
+ z-index: 1;
252
+ }
253
+
254
+ .handle {
255
+ width: 3px;
256
+ background-color: var(--color-accent);
257
+ cursor: ew-resize;
258
+ height: var(--size-12);
259
+ z-index: 3;
260
+ position: absolute;
261
+ }
262
+
263
+ .opaque-layer {
264
+ background-color: rgba(230, 103, 40, 0.25);
265
+ border: 1px solid var(--color-accent);
266
+ height: var(--size-12);
267
+ position: absolute;
268
+ z-index: 2;
269
+ }
270
+ </style>
package/shared/utils.ts CHANGED
@@ -1,4 +1,6 @@
1
- import type { ActionReturn } from "svelte/action";
1
+ import { toBlobURL } from "@ffmpeg/util";
2
+ import { FFmpeg } from "@ffmpeg/ffmpeg";
3
+ import { lookup } from "mrmime";
2
4
 
3
5
  export const prettyBytes = (bytes: number): string => {
4
6
  let units = ["B", "KB", "MB", "GB", "PB"];
@@ -22,7 +24,7 @@ export const playable = (): boolean => {
22
24
  export function loaded(
23
25
  node: HTMLVideoElement,
24
26
  { autoplay }: { autoplay: boolean }
25
- ): ActionReturn {
27
+ ): any {
26
28
  async function handle_playback(): Promise<void> {
27
29
  if (!autoplay) return;
28
30
  await node.play();
@@ -36,3 +38,106 @@ export function loaded(
36
38
  }
37
39
  };
38
40
  }
41
+
42
+ export default async function loadFfmpeg(): Promise<FFmpeg> {
43
+ const ffmpeg = new FFmpeg();
44
+ const baseURL = "https://unpkg.com/@ffmpeg/core@0.12.4/dist/esm";
45
+
46
+ await ffmpeg.load({
47
+ coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
48
+ wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm")
49
+ });
50
+
51
+ return ffmpeg;
52
+ }
53
+
54
+ export function blob_to_data_url(blob: Blob): Promise<string> {
55
+ return new Promise((fulfill, reject) => {
56
+ let reader = new FileReader();
57
+ reader.onerror = reject;
58
+ reader.onload = () => fulfill(reader.result as string);
59
+ reader.readAsDataURL(blob);
60
+ });
61
+ }
62
+
63
+ export async function trimVideo(
64
+ ffmpeg: FFmpeg,
65
+ startTime: number,
66
+ endTime: number,
67
+ videoElement: HTMLVideoElement
68
+ ): Promise<any> {
69
+ try {
70
+ const videoUrl = videoElement.src;
71
+ const mimeType = lookup(videoElement.src) || "video/mp4";
72
+ const blobUrl = await toBlobURL(videoUrl, mimeType);
73
+ const response = await fetch(blobUrl);
74
+ const vidBlob = await response.blob();
75
+ const type = getVideoExtensionFromMimeType(mimeType) || "mp4";
76
+ const inputName = `input.${type}`;
77
+ const outputName = `output.${type}`;
78
+
79
+ await ffmpeg.writeFile(
80
+ inputName,
81
+ new Uint8Array(await vidBlob.arrayBuffer())
82
+ );
83
+
84
+ let command = [
85
+ "-i",
86
+ inputName,
87
+ "-ss",
88
+ startTime.toString(),
89
+ "-to",
90
+ endTime.toString(),
91
+ "-c:a",
92
+ "copy",
93
+ outputName
94
+ ];
95
+
96
+ await ffmpeg.exec(command);
97
+ const outputData = await ffmpeg.readFile(outputName);
98
+ const outputBlob = new Blob([outputData], {
99
+ type: `video/${type}`
100
+ });
101
+
102
+ return outputBlob;
103
+ } catch (error) {
104
+ console.error("Error initializing FFmpeg:", error);
105
+ }
106
+ }
107
+
108
+ const getVideoExtensionFromMimeType = (mimeType: string): string | null => {
109
+ const videoMimeToExtensionMap: { [key: string]: string } = {
110
+ "video/mp4": "mp4",
111
+ "video/webm": "webm",
112
+ "video/ogg": "ogv",
113
+ "video/quicktime": "mov",
114
+ "video/x-msvideo": "avi",
115
+ "video/x-matroska": "mkv",
116
+ "video/mpeg": "mpeg",
117
+ "video/3gpp": "3gp",
118
+ "video/3gpp2": "3g2",
119
+ "video/h261": "h261",
120
+ "video/h263": "h263",
121
+ "video/h264": "h264",
122
+ "video/jpeg": "jpgv",
123
+ "video/jpm": "jpm",
124
+ "video/mj2": "mj2",
125
+ "video/mpv": "mpv",
126
+ "video/vnd.ms-playready.media.pyv": "pyv",
127
+ "video/vnd.uvvu.mp4": "uvu",
128
+ "video/vnd.vivo": "viv",
129
+ "video/x-f4v": "f4v",
130
+ "video/x-fli": "fli",
131
+ "video/x-flv": "flv",
132
+ "video/x-m4v": "m4v",
133
+ "video/x-ms-asf": "asf",
134
+ "video/x-ms-wm": "wm",
135
+ "video/x-ms-wmv": "wmv",
136
+ "video/x-ms-wmx": "wmx",
137
+ "video/x-ms-wvx": "wvx",
138
+ "video/x-sgi-movie": "movie",
139
+ "video/x-smv": "smv"
140
+ };
141
+
142
+ return videoMimeToExtensionMap[mimeType] || null;
143
+ };
@@ -1,70 +0,0 @@
1
- <script lang="ts">
2
- import type { HTMLVideoAttributes } from "svelte/elements";
3
- import { createEventDispatcher } from "svelte";
4
- import { loaded } from "./utils";
5
-
6
- import { resolve_wasm_src } from "@gradio/wasm/svelte";
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
-
22
- const dispatch = createEventDispatcher();
23
- </script>
24
-
25
- {#await resolve_wasm_src(src) then resolved_src}
26
- <!--
27
- The spread operator with `$$props` or `$$restProps` can't be used here
28
- to pass props from the parent component to the <video> element
29
- because of its unexpected behavior: https://github.com/sveltejs/svelte/issues/7404
30
- 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
- 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
- -->
33
- <video
34
- src={resolved_src}
35
- {muted}
36
- {playsinline}
37
- {preload}
38
- {autoplay}
39
- {controls}
40
- on:loadeddata={dispatch.bind(null, "loadeddata")}
41
- on:click={dispatch.bind(null, "click")}
42
- on:play={dispatch.bind(null, "play")}
43
- on:pause={dispatch.bind(null, "pause")}
44
- on:ended={dispatch.bind(null, "ended")}
45
- on:mouseover={dispatch.bind(null, "mouseover")}
46
- on:mouseout={dispatch.bind(null, "mouseout")}
47
- on:focus={dispatch.bind(null, "focus")}
48
- on:blur={dispatch.bind(null, "blur")}
49
- bind:currentTime
50
- bind:duration
51
- bind:paused
52
- bind:this={node}
53
- use:loaded={{ autoplay: autoplay ?? false }}
54
- data-testid={$$props["data-testid"]}
55
- >
56
- <slot />
57
- </video>
58
- {:catch error}
59
- <p style="color: red;">{error.message}</p>
60
- {/await}
61
-
62
- <style>
63
- video {
64
- position: inherit;
65
- background-color: black;
66
- width: var(--size-full);
67
- height: var(--size-full);
68
- object-fit: contain;
69
- }
70
- </style>