@gradio/video 0.17.0-dev.0 → 0.17.0-dev.2

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,43 +1,52 @@
1
- <script>import { Undo, Trim, Clear } from "@gradio/icons";
2
- import VideoTimeline from "./VideoTimeline.svelte";
3
- import { trimVideo } from "./utils";
4
- import { FFmpeg } from "@ffmpeg/ffmpeg";
5
- import loadFfmpeg from "./utils";
6
- import { onMount } from "svelte";
7
- import { format_time } from "@gradio/utils";
8
- import { IconButton } from "@gradio/atoms";
9
- import { ModifyUpload } from "@gradio/upload";
10
- export let videoElement;
11
- export let showRedo = false;
12
- export let interactive = true;
13
- export let mode = "";
14
- export let handle_reset_value;
15
- export let handle_trim_video;
16
- export let processingVideo = false;
17
- export let i18n;
18
- export let value = null;
19
- export let show_download_button = false;
20
- export let handle_clear = () => {
21
- };
22
- export let has_change_history = false;
23
- let ffmpeg;
24
- onMount(async () => {
25
- ffmpeg = await loadFfmpeg();
26
- });
27
- $: if (mode === "edit" && trimmedDuration === null && videoElement)
28
- trimmedDuration = videoElement.duration;
29
- let trimmedDuration = null;
30
- let dragStart = 0;
31
- let dragEnd = 0;
32
- let loadingTimeline = false;
33
- const toggleTrimmingMode = () => {
34
- if (mode === "edit") {
35
- mode = "";
36
- trimmedDuration = videoElement.duration;
37
- } else {
38
- mode = "edit";
39
- }
40
- };
1
+ <script lang="ts">
2
+ import { Undo, Trim, Clear } 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
+ import { format_time } from "@gradio/utils";
9
+ import { IconButton } from "@gradio/atoms";
10
+ import { ModifyUpload } from "@gradio/upload";
11
+ import type { FileData } from "@gradio/client";
12
+
13
+ export let videoElement: HTMLVideoElement;
14
+
15
+ export let showRedo = false;
16
+ export let interactive = true;
17
+ export let mode = "";
18
+ export let handle_reset_value: () => void;
19
+ export let handle_trim_video: (videoBlob: Blob) => void;
20
+ export let processingVideo = false;
21
+ export let i18n: (key: string) => string;
22
+ export let value: FileData | null = null;
23
+ export let show_download_button = false;
24
+ export let handle_clear: () => void = () => {};
25
+ export let has_change_history = false;
26
+
27
+ let ffmpeg: FFmpeg;
28
+
29
+ onMount(async () => {
30
+ ffmpeg = await loadFfmpeg();
31
+ });
32
+
33
+ $: if (mode === "edit" && trimmedDuration === null && videoElement)
34
+ trimmedDuration = videoElement.duration;
35
+
36
+ let trimmedDuration: number | null = null;
37
+ let dragStart = 0;
38
+ let dragEnd = 0;
39
+
40
+ let loadingTimeline = false;
41
+
42
+ const toggleTrimmingMode = (): void => {
43
+ if (mode === "edit") {
44
+ mode = "";
45
+ trimmedDuration = videoElement.duration;
46
+ } else {
47
+ mode = "edit";
48
+ }
49
+ };
41
50
  </script>
42
51
 
43
52
  <div class="container" class:hidden={mode !== "edit"}>
@@ -1,30 +1,32 @@
1
- import { SvelteComponent } from "svelte";
2
1
  import type { FileData } from "@gradio/client";
3
- declare const __propDef: {
4
- props: {
5
- videoElement: HTMLVideoElement;
6
- showRedo?: boolean;
7
- interactive?: boolean;
8
- mode?: string;
9
- handle_reset_value: () => void;
10
- handle_trim_video: (videoBlob: Blob) => void;
11
- processingVideo?: boolean;
12
- i18n: (key: string) => string;
13
- value?: FileData | null;
14
- show_download_button?: boolean;
15
- handle_clear?: () => void;
16
- has_change_history?: boolean;
2
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
+ $$bindings?: Bindings;
5
+ } & Exports;
6
+ (internal: unknown, props: Props & {
7
+ $$events?: Events;
8
+ $$slots?: Slots;
9
+ }): Exports & {
10
+ $set?: any;
11
+ $on?: any;
17
12
  };
18
- events: {
19
- [evt: string]: CustomEvent<any>;
20
- };
21
- slots: {};
22
- exports?: {} | undefined;
23
- bindings?: string | undefined;
24
- };
25
- export type VideoControlsProps = typeof __propDef.props;
26
- export type VideoControlsEvents = typeof __propDef.events;
27
- export type VideoControlsSlots = typeof __propDef.slots;
28
- export default class VideoControls extends SvelteComponent<VideoControlsProps, VideoControlsEvents, VideoControlsSlots> {
13
+ z_$$bindings?: Bindings;
29
14
  }
30
- export {};
15
+ declare const VideoControls: $$__sveltets_2_IsomorphicComponent<{
16
+ videoElement: HTMLVideoElement;
17
+ showRedo?: boolean;
18
+ interactive?: boolean;
19
+ mode?: string;
20
+ handle_reset_value: () => void;
21
+ handle_trim_video: (videoBlob: Blob) => void;
22
+ processingVideo?: boolean;
23
+ i18n: (key: string) => string;
24
+ value?: FileData | null;
25
+ show_download_button?: boolean;
26
+ handle_clear?: () => void;
27
+ has_change_history?: boolean;
28
+ }, {
29
+ [evt: string]: CustomEvent<any>;
30
+ }, {}, {}, string>;
31
+ type VideoControls = InstanceType<typeof VideoControls>;
32
+ export default VideoControls;
@@ -1,40 +1,61 @@
1
- <script>import { createEventDispatcher, afterUpdate, tick } from "svelte";
2
- import {
3
- BlockLabel,
4
- Empty,
5
- IconButton,
6
- ShareButton,
7
- IconButtonWrapper,
8
- DownloadLink
9
- } from "@gradio/atoms";
10
- import { Video, Download } from "@gradio/icons";
11
- import { uploadToHuggingFace } from "@gradio/utils";
12
- import Player from "./Player.svelte";
13
- export let value = null;
14
- export let subtitle = null;
15
- export let label = void 0;
16
- export let show_label = true;
17
- export let autoplay;
18
- export let show_share_button = true;
19
- export let show_download_button = true;
20
- export let loop;
21
- export let i18n;
22
- export let upload;
23
- export let display_icon_button_wrapper_top_corner = false;
24
- let old_value = null;
25
- let old_subtitle = null;
26
- const dispatch = createEventDispatcher();
27
- $: value && dispatch("change", value);
28
- afterUpdate(async () => {
29
- if (value !== old_value && subtitle !== old_subtitle && old_subtitle !== null) {
30
- old_value = value;
31
- value = null;
32
- await tick();
33
- value = old_value;
34
- }
35
- old_value = value;
36
- old_subtitle = subtitle;
37
- });
1
+ <script lang="ts">
2
+ import { createEventDispatcher, afterUpdate, tick } from "svelte";
3
+ import {
4
+ BlockLabel,
5
+ Empty,
6
+ IconButton,
7
+ ShareButton,
8
+ IconButtonWrapper,
9
+ DownloadLink
10
+ } from "@gradio/atoms";
11
+ import type { FileData, Client } from "@gradio/client";
12
+ import { Video, Download } from "@gradio/icons";
13
+ import { uploadToHuggingFace } from "@gradio/utils";
14
+
15
+ import Player from "./Player.svelte";
16
+ import type { I18nFormatter } from "js/core/src/gradio_helper";
17
+
18
+ export let value: FileData | null = null;
19
+ export let subtitle: FileData | null = null;
20
+ export let label: string | undefined = undefined;
21
+ export let show_label = true;
22
+ export let autoplay: boolean;
23
+ export let show_share_button = true;
24
+ export let show_download_button = true;
25
+ export let loop: boolean;
26
+ export let i18n: I18nFormatter;
27
+ export let upload: Client["upload"];
28
+ export let display_icon_button_wrapper_top_corner = false;
29
+
30
+ let old_value: FileData | null = null;
31
+ let old_subtitle: FileData | null = null;
32
+
33
+ const dispatch = createEventDispatcher<{
34
+ change: FileData;
35
+ play: undefined;
36
+ pause: undefined;
37
+ end: undefined;
38
+ stop: undefined;
39
+ load: undefined;
40
+ }>();
41
+
42
+ $: value && dispatch("change", value);
43
+
44
+ afterUpdate(async () => {
45
+ // needed to bust subtitle caching issues on Chrome
46
+ if (
47
+ value !== old_value &&
48
+ subtitle !== old_subtitle &&
49
+ old_subtitle !== null
50
+ ) {
51
+ old_value = value;
52
+ value = null;
53
+ await tick();
54
+ value = old_value;
55
+ }
56
+ old_value = value;
57
+ old_subtitle = subtitle;
58
+ });
38
59
  </script>
39
60
 
40
61
  <BlockLabel {show_label} Icon={Video} label={label || "Video"} />
@@ -1,39 +1,41 @@
1
- import { SvelteComponent } from "svelte";
2
1
  import type { FileData, Client } from "@gradio/client";
3
2
  import type { I18nFormatter } from "js/core/src/gradio_helper";
4
- declare const __propDef: {
5
- props: {
6
- value?: FileData | null;
7
- subtitle?: FileData | null;
8
- label?: string | undefined;
9
- show_label?: boolean;
10
- autoplay: boolean;
11
- show_share_button?: boolean;
12
- show_download_button?: boolean;
13
- loop: boolean;
14
- i18n: I18nFormatter;
15
- upload: Client["upload"];
16
- display_icon_button_wrapper_top_corner?: boolean;
3
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
4
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
5
+ $$bindings?: Bindings;
6
+ } & Exports;
7
+ (internal: unknown, props: Props & {
8
+ $$events?: Events;
9
+ $$slots?: Slots;
10
+ }): Exports & {
11
+ $set?: any;
12
+ $on?: any;
17
13
  };
18
- events: {
19
- play: CustomEvent<any>;
20
- pause: CustomEvent<any>;
21
- stop: CustomEvent<any>;
22
- end: CustomEvent<any>;
23
- error: CustomEvent<string>;
24
- share: CustomEvent<import("@gradio/utils").ShareData>;
25
- change: CustomEvent<FileData>;
26
- load: CustomEvent<undefined>;
27
- } & {
28
- [evt: string]: CustomEvent<any>;
29
- };
30
- slots: {};
31
- exports?: {} | undefined;
32
- bindings?: string | undefined;
33
- };
34
- export type VideoPreviewProps = typeof __propDef.props;
35
- export type VideoPreviewEvents = typeof __propDef.events;
36
- export type VideoPreviewSlots = typeof __propDef.slots;
37
- export default class VideoPreview extends SvelteComponent<VideoPreviewProps, VideoPreviewEvents, VideoPreviewSlots> {
14
+ z_$$bindings?: Bindings;
38
15
  }
39
- export {};
16
+ declare const VideoPreview: $$__sveltets_2_IsomorphicComponent<{
17
+ value?: FileData | null;
18
+ subtitle?: FileData | null;
19
+ label?: string | undefined;
20
+ show_label?: boolean;
21
+ autoplay: boolean;
22
+ show_share_button?: boolean;
23
+ show_download_button?: boolean;
24
+ loop: boolean;
25
+ i18n: I18nFormatter;
26
+ upload: Client["upload"];
27
+ display_icon_button_wrapper_top_corner?: boolean;
28
+ }, {
29
+ play: CustomEvent<any>;
30
+ pause: CustomEvent<any>;
31
+ stop: CustomEvent<any>;
32
+ end: CustomEvent<any>;
33
+ error: CustomEvent<string>;
34
+ share: CustomEvent<import("@gradio/utils").ShareData>;
35
+ change: CustomEvent<FileData>;
36
+ load: CustomEvent<undefined>;
37
+ } & {
38
+ [evt: string]: CustomEvent<any>;
39
+ }, {}, {}, string>;
40
+ type VideoPreview = InstanceType<typeof VideoPreview>;
41
+ export default VideoPreview;
@@ -1,109 +1,151 @@
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
- $: loadingTimeline = thumbnails.length !== numberOfThumbnails;
18
- const stopDragging = () => {
19
- dragging = null;
20
- };
21
- const drag = (event, distance) => {
22
- if (dragging) {
23
- const timeline = document.getElementById("timeline");
24
- if (!timeline) return;
25
- const rect = timeline.getBoundingClientRect();
26
- let newPercentage = (event.clientX - rect.left) / rect.width * 100;
27
- if (distance) {
28
- newPercentage = dragging === "left" ? leftHandlePosition + distance : rightHandlePosition + distance;
29
- } else {
30
- newPercentage = (event.clientX - rect.left) / rect.width * 100;
31
- }
32
- newPercentage = Math.max(0, Math.min(newPercentage, 100));
33
- if (dragging === "left") {
34
- leftHandlePosition = Math.min(newPercentage, rightHandlePosition);
35
- const newTimeLeft = leftHandlePosition / 100 * videoDuration;
36
- videoElement.currentTime = newTimeLeft;
37
- dragStart = newTimeLeft;
38
- } else if (dragging === "right") {
39
- rightHandlePosition = Math.max(newPercentage, leftHandlePosition);
40
- const newTimeRight = rightHandlePosition / 100 * videoDuration;
41
- videoElement.currentTime = newTimeRight;
42
- dragEnd = newTimeRight;
43
- }
44
- const startTime = leftHandlePosition / 100 * videoDuration;
45
- const endTime = rightHandlePosition / 100 * videoDuration;
46
- trimmedDuration = endTime - startTime;
47
- leftHandlePosition = leftHandlePosition;
48
- rightHandlePosition = rightHandlePosition;
49
- }
50
- };
51
- const moveHandle = (e) => {
52
- if (dragging) {
53
- const distance = 1 / videoDuration * 100;
54
- if (e.key === "ArrowLeft") {
55
- drag({ clientX: 0 }, -distance);
56
- } else if (e.key === "ArrowRight") {
57
- drag({ clientX: 0 }, distance);
58
- }
59
- }
60
- };
61
- const generateThumbnail = () => {
62
- const canvas = document.createElement("canvas");
63
- const ctx = canvas.getContext("2d");
64
- if (!ctx) return;
65
- canvas.width = videoElement.videoWidth;
66
- canvas.height = videoElement.videoHeight;
67
- ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
68
- const thumbnail = canvas.toDataURL("image/jpeg", 0.7);
69
- thumbnails = [...thumbnails, thumbnail];
70
- };
71
- onMount(() => {
72
- const loadMetadata = () => {
73
- videoDuration = videoElement.duration;
74
- const interval = videoDuration / numberOfThumbnails;
75
- let captures = 0;
76
- const onSeeked = () => {
77
- generateThumbnail();
78
- captures++;
79
- if (captures < numberOfThumbnails) {
80
- videoElement.currentTime += interval;
81
- } else {
82
- videoElement.removeEventListener("seeked", onSeeked);
83
- }
84
- };
85
- videoElement.addEventListener("seeked", onSeeked);
86
- videoElement.currentTime = 0;
87
- };
88
- if (videoElement.readyState >= 1) {
89
- loadMetadata();
90
- } else {
91
- videoElement.addEventListener("loadedmetadata", loadMetadata);
92
- }
93
- });
94
- onDestroy(() => {
95
- window.removeEventListener("mousemove", drag);
96
- window.removeEventListener("mouseup", stopDragging);
97
- window.removeEventListener("keydown", moveHandle);
98
- if (intervalId !== void 0) {
99
- clearInterval(intervalId);
100
- }
101
- });
102
- onMount(() => {
103
- window.addEventListener("mousemove", drag);
104
- window.addEventListener("mouseup", stopDragging);
105
- window.addEventListener("keydown", moveHandle);
106
- });
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: ReturnType<typeof setInterval> | undefined;
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 !== undefined) {
140
+ clearInterval(intervalId);
141
+ }
142
+ });
143
+
144
+ onMount(() => {
145
+ window.addEventListener("mousemove", drag);
146
+ window.addEventListener("mouseup", stopDragging);
147
+ window.addEventListener("keydown", moveHandle);
148
+ });
107
149
  </script>
108
150
 
109
151
  <div class="container">
@@ -1,22 +1,24 @@
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;
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: Props & {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
9
11
  };
10
- events: {
11
- [evt: string]: CustomEvent<any>;
12
- };
13
- slots: {};
14
- exports?: {} | undefined;
15
- bindings?: string | undefined;
16
- };
17
- export type VideoTimelineProps = typeof __propDef.props;
18
- export type VideoTimelineEvents = typeof __propDef.events;
19
- export type VideoTimelineSlots = typeof __propDef.slots;
20
- export default class VideoTimeline extends SvelteComponent<VideoTimelineProps, VideoTimelineEvents, VideoTimelineSlots> {
12
+ z_$$bindings?: Bindings;
21
13
  }
22
- export {};
14
+ declare const VideoTimeline: $$__sveltets_2_IsomorphicComponent<{
15
+ videoElement: HTMLVideoElement;
16
+ trimmedDuration: number | null;
17
+ dragStart: number;
18
+ dragEnd: number;
19
+ loadingTimeline: boolean;
20
+ }, {
21
+ [evt: string]: CustomEvent<any>;
22
+ }, {}, {}, string>;
23
+ type VideoTimeline = InstanceType<typeof VideoTimeline>;
24
+ export default VideoTimeline;