@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.
- package/CHANGELOG.md +34 -1
- package/Index.svelte +43 -11
- package/README.md +38 -6
- package/Video.test.ts +80 -97
- package/index.ts +7 -0
- package/package.json +13 -11
- package/shared/InteractiveVideo.svelte +80 -21
- package/shared/Player.svelte +28 -1
- package/shared/Video.svelte +56 -0
- package/shared/VideoControls.svelte +214 -0
- package/shared/VideoPreview.svelte +16 -12
- package/shared/VideoTimeline.svelte +270 -0
- package/shared/utils.ts +107 -2
- package/shared/StaticVideo.svelte +0 -70
@@ -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/
|
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
|
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:
|
26
|
-
clear
|
27
|
-
play
|
28
|
-
pause
|
29
|
-
end
|
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
|
34
|
-
stop_recording
|
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(
|
49
|
+
function handle_clear(): void {
|
44
50
|
value = null;
|
45
|
-
|
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
|
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
|
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={(
|
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?.
|
90
|
+
{#key value?.url}
|
80
91
|
<Player
|
92
|
+
{root}
|
93
|
+
interactive
|
81
94
|
{autoplay}
|
82
|
-
src={value.
|
83
|
-
subtitle={subtitle?.
|
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 &&
|
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.
|
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>
|
package/shared/Player.svelte
CHANGED
@@ -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 "./
|
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>
|
package/shared/Video.svelte
CHANGED
@@ -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/
|
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.
|
54
|
+
{#key value.url}
|
54
55
|
<Player
|
55
|
-
src={value.
|
56
|
-
subtitle={subtitle?.
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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}
|