mirror of
https://github.com/gradio-app/gradio.git
synced 2025-03-19 12:00:39 +08:00
4549 autoplay (#4705)
* changes * tests * revert demo * changelog * cleanup audio * more tests * handle video * cleanup * fix notebooks * cleanup * reinstate files * fix notebooks * fixes * fix tests
This commit is contained in:
parent
58c6d68f20
commit
1650e1d383
@ -21,6 +21,8 @@
|
||||
- Fix double upload bug that caused lag in file uploads by [@aliabid94](https://github.com/aliabid94) in [PR 4661](https://github.com/gradio-app/gradio/pull/4661)
|
||||
- `Progress` component now appears even when no `iterable` is specified in `tqdm` constructor by [@itrushkin](https://github.com/itrushkin) in [PR 4475](https://github.com/gradio-app/gradio/pull/4475)
|
||||
- Deprecation warnings now point at the user code using those deprecated features, instead of Gradio internals, by (https://github.com/akx) in [PR 4694](https://github.com/gradio-app/gradio/pull/4694)
|
||||
- Ensure Audio autoplays works when `autoplay=True` and the video source is dynamically updated [@pngwn](https://github.com/pngwn) in [PR 4705](https://github.com/gradio-app/gradio/pull/4705)
|
||||
|
||||
|
||||
## Other Changes:
|
||||
|
||||
|
@ -1 +1 @@
|
||||
{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: audio_component"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "css = \"footer {display: none !important;} .gradio-container {min-height: 0px !important;}\"\n", "\n", "with gr.Blocks(css=css) as demo:\n", " gr.Audio()\n", "\n", "demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
||||
{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: audio_component"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "css = \"footer {display: none !important;} .gradio-container {min-height: 0px !important;}\"\n", "\n", "with gr.Blocks(css=css) as demo:\n", " gr.Audio()\n", "\n", "demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
@ -5,4 +5,4 @@ css = "footer {display: none !important;} .gradio-container {min-height: 0px !im
|
||||
with gr.Blocks(css=css) as demo:
|
||||
gr.Audio()
|
||||
|
||||
demo.launch()
|
||||
demo.launch()
|
@ -18,9 +18,9 @@
|
||||
error: string;
|
||||
}>();
|
||||
|
||||
export let elem_id: string = "";
|
||||
export let elem_classes: Array<string> = [];
|
||||
export let visible: boolean = true;
|
||||
export let elem_id = "";
|
||||
export let elem_classes: string[] = [];
|
||||
export let visible = true;
|
||||
export let mode: "static" | "dynamic";
|
||||
export let value: null | FileData | string = null;
|
||||
export let name: string;
|
||||
@ -31,11 +31,11 @@
|
||||
export let pending: boolean;
|
||||
export let streaming: boolean;
|
||||
export let root_url: null | string;
|
||||
export let container: boolean = false;
|
||||
export let container = false;
|
||||
export let scale: number | null = null;
|
||||
export let min_width: number | undefined = undefined;
|
||||
export let loading_status: LoadingStatus;
|
||||
export let autoplay: boolean = false;
|
||||
export let autoplay = false;
|
||||
|
||||
let _value: null | FileData;
|
||||
$: _value = normalise_file(value, root, root_url);
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { test, describe, assert, afterEach, vi } from "vitest";
|
||||
import { cleanup, render, wait_for_event } from "@gradio/tootils";
|
||||
import { test, describe, assert, afterEach, vi, beforeAll } from "vitest";
|
||||
import { spy, spyOn } from "tinyspy";
|
||||
import { cleanup, render, wait_for_event, wait } from "@gradio/tootils";
|
||||
import event from "@testing-library/user-event";
|
||||
import { setupi18n } from "../../i18n";
|
||||
import { tick } from "svelte";
|
||||
|
||||
import Audio from "./Audio.svelte";
|
||||
import type { LoadingStatus } from "../StatusTracker/types";
|
||||
@ -18,10 +20,14 @@ const loading_status = {
|
||||
};
|
||||
|
||||
describe("Audio", () => {
|
||||
beforeAll(() => {
|
||||
window.HTMLMediaElement.prototype.play = vi.fn();
|
||||
window.HTMLMediaElement.prototype.pause = vi.fn();
|
||||
});
|
||||
afterEach(() => cleanup());
|
||||
|
||||
test("renders provided value and label", async () => {
|
||||
const { getByTestId, queryAllByText } = render(Audio, {
|
||||
const { getByTestId, queryAllByText } = await render(Audio, {
|
||||
show_label: true,
|
||||
loading_status,
|
||||
mode: "dynamic",
|
||||
@ -40,7 +46,7 @@ describe("Audio", () => {
|
||||
});
|
||||
|
||||
assert.isTrue(
|
||||
getByTestId("Audio Component-dynamic-audio").src.endsWith(
|
||||
getByTestId("Audio Component-audio").src.endsWith(
|
||||
"foo/file=https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
|
||||
)
|
||||
);
|
||||
@ -48,7 +54,7 @@ describe("Audio", () => {
|
||||
});
|
||||
|
||||
test("hides label", async () => {
|
||||
const { queryAllByText } = render(Audio, {
|
||||
const { queryAllByText } = await render(Audio, {
|
||||
show_label: false,
|
||||
loading_status,
|
||||
mode: "dynamic",
|
||||
@ -71,7 +77,7 @@ describe("Audio", () => {
|
||||
|
||||
test("upload sets change event", async () => {
|
||||
setupi18n();
|
||||
const { container, component } = render(Audio, {
|
||||
const { container, component } = await render(Audio, {
|
||||
show_label: false,
|
||||
loading_status,
|
||||
value: null,
|
||||
@ -97,7 +103,7 @@ describe("Audio", () => {
|
||||
});
|
||||
|
||||
test("static audio sets value", async () => {
|
||||
const { getByTestId } = render(Audio, {
|
||||
const { getByTestId } = await render(Audio, {
|
||||
show_label: true,
|
||||
loading_status,
|
||||
mode: "static",
|
||||
@ -116,7 +122,7 @@ describe("Audio", () => {
|
||||
});
|
||||
|
||||
assert.isTrue(
|
||||
getByTestId("Audio Component-static-audio").src.endsWith(
|
||||
getByTestId("Audio Component-audio").src.endsWith(
|
||||
"foo/file=https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
|
||||
)
|
||||
);
|
||||
@ -158,7 +164,7 @@ describe("Audio", () => {
|
||||
vi.stubGlobal("navigator", media_mock);
|
||||
vi.stubGlobal("MediaRecorder", media_recorder_mock);
|
||||
|
||||
const { component, getByText } = render(Audio, {
|
||||
const { component, getByText } = await render(Audio, {
|
||||
show_label: true,
|
||||
loading_status,
|
||||
mode: "dynamic",
|
||||
@ -168,7 +174,8 @@ describe("Audio", () => {
|
||||
root_url: null,
|
||||
streaming: false,
|
||||
pending: false,
|
||||
source: "microphone"
|
||||
source: "microphone",
|
||||
name: "bar"
|
||||
});
|
||||
|
||||
const startButton = getByText("Record from microphone");
|
||||
@ -184,4 +191,130 @@ describe("Audio", () => {
|
||||
assert.equal(component.$capture_state().value.name, "audio.wav");
|
||||
assert.equal(mock.callCount, 1);
|
||||
});
|
||||
|
||||
test("when autoplay is true `media.play` should be called in static mode", async () => {
|
||||
const { getByTestId } = await render(Audio, {
|
||||
show_label: true,
|
||||
loading_status,
|
||||
mode: "static",
|
||||
value: {
|
||||
name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
|
||||
data: null,
|
||||
is_file: true
|
||||
},
|
||||
label: "static",
|
||||
root: "foo",
|
||||
root_url: null,
|
||||
streaming: false,
|
||||
pending: false,
|
||||
source: "microphone",
|
||||
autoplay: true
|
||||
});
|
||||
|
||||
const startButton = getByTestId<HTMLAudioElement>("static-audio");
|
||||
const fn = spyOn(startButton, "play");
|
||||
startButton.dispatchEvent(new Event("loadeddata"));
|
||||
|
||||
assert.equal(fn.callCount, 1);
|
||||
});
|
||||
|
||||
test("when autoplay is true `media.play` should be called in dynamic mode", async () => {
|
||||
const { getByTestId } = await render(Audio, {
|
||||
show_label: true,
|
||||
loading_status,
|
||||
mode: "dynamic",
|
||||
value: {
|
||||
name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
|
||||
data: null,
|
||||
is_file: true
|
||||
},
|
||||
label: "dynamic",
|
||||
root: "foo",
|
||||
root_url: null,
|
||||
streaming: false,
|
||||
pending: false,
|
||||
source: "microphone",
|
||||
autoplay: true
|
||||
});
|
||||
|
||||
const startButton = getByTestId<HTMLAudioElement>("dynamic-audio");
|
||||
const fn = spyOn(startButton, "play");
|
||||
startButton.dispatchEvent(new Event("loadeddata"));
|
||||
|
||||
assert.equal(fn.callCount, 1);
|
||||
});
|
||||
|
||||
test("when autoplay is true `media.play` should be called in static mode when the audio data is updated", async () => {
|
||||
const { component, getByTestId } = await render(Audio, {
|
||||
show_label: true,
|
||||
loading_status,
|
||||
mode: "static",
|
||||
value: {
|
||||
name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
|
||||
data: null,
|
||||
is_file: true
|
||||
},
|
||||
label: "static",
|
||||
root: "foo",
|
||||
root_url: null,
|
||||
streaming: false,
|
||||
pending: false,
|
||||
source: "microphone",
|
||||
autoplay: true
|
||||
});
|
||||
|
||||
const startButton = getByTestId<HTMLAudioElement>("static-audio");
|
||||
const fn = spyOn(startButton, "play");
|
||||
|
||||
startButton.dispatchEvent(new Event("loadeddata"));
|
||||
|
||||
component.$set({
|
||||
value: {
|
||||
name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
|
||||
data: null,
|
||||
is_file: true
|
||||
}
|
||||
});
|
||||
|
||||
startButton.dispatchEvent(new Event("loadeddata"));
|
||||
|
||||
assert.equal(fn.callCount, 2);
|
||||
});
|
||||
|
||||
test("when autoplay is true `media.play` should be called in dynamic mode when the audio data is updated", async () => {
|
||||
const { component, getByTestId } = await render(Audio, {
|
||||
show_label: true,
|
||||
loading_status,
|
||||
mode: "dynamic",
|
||||
value: {
|
||||
name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
|
||||
data: null,
|
||||
is_file: true
|
||||
},
|
||||
label: "dynamic",
|
||||
root: "foo",
|
||||
root_url: null,
|
||||
streaming: false,
|
||||
pending: false,
|
||||
source: "microphone",
|
||||
autoplay: true
|
||||
});
|
||||
|
||||
const startButton = getByTestId<HTMLAudioElement>("dynamic-audio");
|
||||
const fn = spyOn(startButton, "play");
|
||||
|
||||
startButton.dispatchEvent(new Event("loadeddata"));
|
||||
|
||||
component.$set({
|
||||
value: {
|
||||
name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
|
||||
data: null,
|
||||
is_file: true
|
||||
}
|
||||
});
|
||||
|
||||
startButton.dispatchEvent(new Event("loadeddata"));
|
||||
|
||||
assert.equal(fn.callCount, 2);
|
||||
});
|
||||
});
|
||||
|
@ -7,13 +7,17 @@ import Button from "./Button.svelte";
|
||||
describe("Hello.svelte", () => {
|
||||
afterEach(() => cleanup());
|
||||
|
||||
test.skip("renders label text", () => {
|
||||
const { container, component } = render(Button, { value: "Click Me" });
|
||||
test.skip("renders label text", async () => {
|
||||
const { container, component } = await render(Button, {
|
||||
value: "Click Me"
|
||||
});
|
||||
assert.equal(container.innerText, "Click Me");
|
||||
});
|
||||
|
||||
test.skip("triggers callback when clicked", async () => {
|
||||
const { container, component } = render(Button, { value: "Click Me" });
|
||||
const { container, component } = await render(Button, {
|
||||
value: "Click Me"
|
||||
});
|
||||
const mock = spy();
|
||||
component.$on("click", mock);
|
||||
|
||||
|
@ -19,7 +19,7 @@ describe.skip("Chatbot", () => {
|
||||
afterEach(() => cleanup());
|
||||
|
||||
test("renders user and bot messages", async () => {
|
||||
const { getAllByTestId } = render(Chatbot, {
|
||||
const { getAllByTestId } = await render(Chatbot, {
|
||||
loading_status,
|
||||
label: "hello",
|
||||
value: [["user message one", "bot message one"]],
|
||||
@ -37,7 +37,7 @@ describe.skip("Chatbot", () => {
|
||||
});
|
||||
|
||||
test("renders additional message as they are passed", async () => {
|
||||
const { component, getAllByTestId } = render(Chatbot, {
|
||||
const { component, getAllByTestId } = await render(Chatbot, {
|
||||
loading_status,
|
||||
label: "hello",
|
||||
value: [["user message one", "bot message one"]],
|
||||
|
@ -17,8 +17,8 @@ const loading_status = {
|
||||
describe("ColorPicker", () => {
|
||||
afterEach(() => cleanup());
|
||||
|
||||
test("renders provided value", () => {
|
||||
const { getByDisplayValue } = render(ColorPicker, {
|
||||
test("renders provided value", async () => {
|
||||
const { getByDisplayValue } = await render(ColorPicker, {
|
||||
loading_status,
|
||||
show_label: true,
|
||||
mode: "dynamic",
|
||||
@ -31,7 +31,7 @@ describe("ColorPicker", () => {
|
||||
});
|
||||
|
||||
test("changing the color should update the value", async () => {
|
||||
const { component, getByDisplayValue } = render(ColorPicker, {
|
||||
const { component, getByDisplayValue } = await render(ColorPicker, {
|
||||
loading_status,
|
||||
show_label: true,
|
||||
mode: "dynamic",
|
||||
|
@ -4,14 +4,15 @@ import { cleanup, render } from "@gradio/tootils";
|
||||
import Gallery from "./Gallery.svelte";
|
||||
import type { LoadingStatus } from "../StatusTracker/types";
|
||||
|
||||
const loading_status = {
|
||||
const loading_status: LoadingStatus = {
|
||||
eta: 0,
|
||||
queue_position: 1,
|
||||
queue_size: 1,
|
||||
status: "complete" as LoadingStatus["status"],
|
||||
status: "complete",
|
||||
scroll_to_output: false,
|
||||
visible: true,
|
||||
fn_index: 0
|
||||
fn_index: 0,
|
||||
show_progress: "full"
|
||||
};
|
||||
|
||||
describe("Gallery", () => {
|
||||
@ -21,7 +22,9 @@ describe("Gallery", () => {
|
||||
});
|
||||
|
||||
test("preview shows detailed image by default", async () => {
|
||||
const { getAllByTestId, getByTestId } = render(Gallery, {
|
||||
window.Element.prototype.scrollTo = vi.fn(() => {});
|
||||
|
||||
const { getAllByTestId, getByTestId } = await render(Gallery, {
|
||||
loading_status,
|
||||
label: "gallery",
|
||||
// @ts-ignore
|
||||
@ -51,14 +54,12 @@ describe("Gallery", () => {
|
||||
});
|
||||
|
||||
const details = getAllByTestId("detailed-image");
|
||||
const container = getByTestId("container_el");
|
||||
container.scrollTo = () => {};
|
||||
|
||||
assert.equal(details.length, 1);
|
||||
});
|
||||
|
||||
test("detailed view does not show larger image", async () => {
|
||||
const { queryAllByTestId, getByTestId } = render(Gallery, {
|
||||
const { queryAllByTestId, getByTestId } = await render(Gallery, {
|
||||
loading_status,
|
||||
label: "gallery",
|
||||
// @ts-ignore
|
||||
|
@ -21,8 +21,8 @@ describe("Radio", () => {
|
||||
afterEach(() => cleanup());
|
||||
const choices = ["dog", "cat", "turtle"];
|
||||
|
||||
test("renders provided value", () => {
|
||||
const { getAllByRole, getByTestId } = render(Radio, {
|
||||
test("renders provided value", async () => {
|
||||
const { getAllByRole, getByTestId } = await render(Radio, {
|
||||
show_label: true,
|
||||
loading_status,
|
||||
choices: choices,
|
||||
@ -46,7 +46,7 @@ describe("Radio", () => {
|
||||
});
|
||||
|
||||
test("should update the value when a radio is clicked", async () => {
|
||||
const { getByDisplayValue, getByTestId } = render(Radio, {
|
||||
const { getByDisplayValue, getByTestId } = await render(Radio, {
|
||||
show_label: true,
|
||||
loading_status,
|
||||
choices: choices,
|
||||
|
@ -19,8 +19,8 @@ const loading_status = {
|
||||
describe("Textbox", () => {
|
||||
afterEach(() => cleanup());
|
||||
|
||||
test("renders provided value", () => {
|
||||
const { getByDisplayValue } = render(Textbox, {
|
||||
test("renders provided value", async () => {
|
||||
const { getByDisplayValue } = await render(Textbox, {
|
||||
show_label: true,
|
||||
max_lines: 1,
|
||||
loading_status,
|
||||
@ -35,7 +35,7 @@ describe("Textbox", () => {
|
||||
});
|
||||
|
||||
test("changing the text should update the value", async () => {
|
||||
const { component, getByDisplayValue } = render(Textbox, {
|
||||
const { component, getByDisplayValue } = await render(Textbox, {
|
||||
show_label: true,
|
||||
max_lines: 10,
|
||||
loading_status,
|
||||
|
@ -10,9 +10,9 @@
|
||||
import type { LoadingStatus } from "../StatusTracker/types";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export let elem_id: string = "";
|
||||
export let elem_classes: Array<string> = [];
|
||||
export let visible: boolean = true;
|
||||
export let elem_id = "";
|
||||
export let elem_classes: string[] = [];
|
||||
export let visible = true;
|
||||
export let value: [FileData, FileData | null] | null = null;
|
||||
let old_value: [FileData, FileData | null] | null = null;
|
||||
|
||||
@ -26,11 +26,11 @@
|
||||
export let width: number | undefined;
|
||||
export let mirror_webcam: boolean;
|
||||
export let include_audio: boolean;
|
||||
export let container: boolean = false;
|
||||
export let container = false;
|
||||
export let scale: number | null = null;
|
||||
export let min_width: number | undefined = undefined;
|
||||
export let mode: "static" | "dynamic";
|
||||
export let autoplay: boolean = false;
|
||||
export let autoplay = false;
|
||||
|
||||
let _video: FileData | null = null;
|
||||
let _subtitle: FileData | null = null;
|
||||
|
251
js/app/src/components/Video/Video.test.ts
Normal file
251
js/app/src/components/Video/Video.test.ts
Normal file
@ -0,0 +1,251 @@
|
||||
import {
|
||||
test,
|
||||
describe,
|
||||
assert,
|
||||
afterEach,
|
||||
vi,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
expect
|
||||
} from "vitest";
|
||||
import { spyOn } from "tinyspy";
|
||||
import { cleanup, render } from "@gradio/tootils";
|
||||
import { setupi18n } from "../../i18n";
|
||||
|
||||
import Video from "./Video.svelte";
|
||||
import type { LoadingStatus } from "../StatusTracker/types";
|
||||
|
||||
const loading_status = {
|
||||
eta: 0,
|
||||
queue_position: 1,
|
||||
queue_size: 1,
|
||||
status: "complete" as LoadingStatus["status"],
|
||||
scroll_to_output: false,
|
||||
visible: true,
|
||||
fn_index: 0,
|
||||
show_progress: "full" as LoadingStatus["show_progress"]
|
||||
};
|
||||
|
||||
describe("Video", () => {
|
||||
beforeAll(() => {
|
||||
window.HTMLMediaElement.prototype.play = vi.fn();
|
||||
window.HTMLMediaElement.prototype.pause = vi.fn();
|
||||
});
|
||||
beforeEach(setupi18n);
|
||||
afterEach(() => cleanup());
|
||||
|
||||
test("renders provided value and label", async () => {
|
||||
const { getByTestId, queryAllByText } = await render(Video, {
|
||||
show_label: true,
|
||||
loading_status,
|
||||
mode: "dynamic",
|
||||
value: [
|
||||
{
|
||||
name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
|
||||
data: null,
|
||||
is_file: true
|
||||
}
|
||||
],
|
||||
label: "Test Label",
|
||||
root: "foo",
|
||||
root_url: null,
|
||||
streaming: false,
|
||||
pending: false,
|
||||
name: "bar",
|
||||
source: "upload"
|
||||
});
|
||||
assert.isTrue(
|
||||
getByTestId("Test Label-player").src.endsWith(
|
||||
"foo/file=https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
|
||||
)
|
||||
);
|
||||
assert(queryAllByText("Test Label").length, 1);
|
||||
});
|
||||
|
||||
test("hides label", async () => {
|
||||
const { queryAllByText } = await render(Video, {
|
||||
show_label: false,
|
||||
loading_status,
|
||||
mode: "dynamic",
|
||||
value: {
|
||||
name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
|
||||
data: null,
|
||||
is_file: true
|
||||
},
|
||||
label: "Video Component",
|
||||
root: "foo",
|
||||
root_url: null,
|
||||
streaming: false,
|
||||
pending: false,
|
||||
name: "bar",
|
||||
source: "upload"
|
||||
});
|
||||
assert(queryAllByText("Video Component").length, 0);
|
||||
});
|
||||
|
||||
test("static Video sets value", async () => {
|
||||
const { getByTestId } = await render(Video, {
|
||||
show_label: true,
|
||||
loading_status,
|
||||
mode: "static",
|
||||
value: [
|
||||
{
|
||||
name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
|
||||
data: null,
|
||||
is_file: true
|
||||
}
|
||||
],
|
||||
root: "foo",
|
||||
root_url: null,
|
||||
streaming: false,
|
||||
pending: false,
|
||||
name: "bar",
|
||||
source: "upload"
|
||||
});
|
||||
assert.isTrue(
|
||||
getByTestId("test-player").src.endsWith(
|
||||
"foo/file=https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test("when autoplay is true `media.play` should be called in static mode", async () => {
|
||||
const { getByTestId } = await render(Video, {
|
||||
show_label: true,
|
||||
loading_status,
|
||||
mode: "static",
|
||||
value: [
|
||||
{
|
||||
name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
|
||||
data: null,
|
||||
is_file: true
|
||||
}
|
||||
],
|
||||
root: "foo",
|
||||
root_url: null,
|
||||
streaming: false,
|
||||
pending: false,
|
||||
source: "microphone",
|
||||
autoplay: true
|
||||
});
|
||||
const startButton = getByTestId<HTMLAudioElement>("test-player");
|
||||
const fn = spyOn(startButton, "play");
|
||||
startButton.dispatchEvent(new Event("loadeddata"));
|
||||
assert.equal(fn.callCount, 1);
|
||||
});
|
||||
|
||||
test("when autoplay is true `media.play` should be called in dynamic mode", async () => {
|
||||
const { getByTestId } = await render(Video, {
|
||||
show_label: true,
|
||||
loading_status,
|
||||
mode: "dynamic",
|
||||
value: [
|
||||
{
|
||||
name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
|
||||
data: null,
|
||||
is_file: true
|
||||
}
|
||||
],
|
||||
root: "foo",
|
||||
root_url: null,
|
||||
streaming: false,
|
||||
pending: false,
|
||||
source: "microphone",
|
||||
autoplay: true
|
||||
});
|
||||
const startButton = getByTestId<HTMLAudioElement>("test-player");
|
||||
const fn = spyOn(startButton, "play");
|
||||
startButton.dispatchEvent(new Event("loadeddata"));
|
||||
assert.equal(fn.callCount, 1);
|
||||
});
|
||||
|
||||
test("when autoplay is true `media.play` should be called in static mode when the Video data is updated", async () => {
|
||||
const { component, getByTestId } = await render(Video, {
|
||||
show_label: true,
|
||||
loading_status,
|
||||
mode: "static",
|
||||
value: [
|
||||
{
|
||||
name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
|
||||
data: null,
|
||||
is_file: true
|
||||
}
|
||||
],
|
||||
root: "foo",
|
||||
root_url: null,
|
||||
streaming: false,
|
||||
pending: false,
|
||||
source: "microphone",
|
||||
autoplay: true
|
||||
});
|
||||
const startButton = getByTestId<HTMLAudioElement>("test-player");
|
||||
const fn = spyOn(startButton, "play");
|
||||
startButton.dispatchEvent(new Event("loadeddata"));
|
||||
component.$set({
|
||||
value: {
|
||||
name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
|
||||
data: null,
|
||||
is_file: true
|
||||
}
|
||||
});
|
||||
startButton.dispatchEvent(new Event("loadeddata"));
|
||||
assert.equal(fn.callCount, 2);
|
||||
});
|
||||
|
||||
test("when autoplay is true `media.play` should be called in dynamic mode when the Video data is updated", async () => {
|
||||
const { component, getByTestId } = await render(Video, {
|
||||
show_label: true,
|
||||
loading_status,
|
||||
mode: "dynamic",
|
||||
value: [
|
||||
{
|
||||
name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
|
||||
data: null,
|
||||
is_file: true
|
||||
}
|
||||
],
|
||||
root: "foo",
|
||||
root_url: null,
|
||||
streaming: false,
|
||||
pending: false,
|
||||
source: "microphone",
|
||||
autoplay: true
|
||||
});
|
||||
const startButton = getByTestId<HTMLAudioElement>("test-player");
|
||||
const fn = spyOn(startButton, "play");
|
||||
startButton.dispatchEvent(new Event("loadeddata"));
|
||||
component.$set({
|
||||
value: {
|
||||
name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
|
||||
data: null,
|
||||
is_file: true
|
||||
}
|
||||
});
|
||||
startButton.dispatchEvent(new Event("loadeddata"));
|
||||
assert.equal(fn.callCount, 2);
|
||||
});
|
||||
test("renders video and download button", async () => {
|
||||
const data = [
|
||||
{
|
||||
data: null,
|
||||
name: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
|
||||
is_file: true
|
||||
}
|
||||
];
|
||||
const results = await render(Video, {
|
||||
mode: "static",
|
||||
label: "video",
|
||||
show_label: true,
|
||||
value: data,
|
||||
root: "foo"
|
||||
});
|
||||
|
||||
const downloadButton = results.getAllByTestId("download-div")[0];
|
||||
expect(
|
||||
downloadButton.getElementsByTagName("a")[0].getAttribute("href")
|
||||
).toBe(`foo/file=${data[0].name}`);
|
||||
expect(
|
||||
downloadButton.getElementsByTagName("button").length
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
@ -14,17 +14,18 @@
|
||||
import { Music } from "@gradio/icons";
|
||||
// @ts-ignore
|
||||
import Range from "svelte-range-slider-pips";
|
||||
import { loaded } from "./utils";
|
||||
|
||||
import type { IBlobEvent, IMediaRecorder } from "extendable-media-recorder";
|
||||
|
||||
export let value: null | { name: string; data: string } = null;
|
||||
export let label: string;
|
||||
export let show_label: boolean = true;
|
||||
export let name: string = "";
|
||||
export let show_label = true;
|
||||
export let name = "";
|
||||
export let source: "microphone" | "upload" | "none";
|
||||
export let pending: boolean = false;
|
||||
export let streaming: boolean = false;
|
||||
export let autoplay: boolean;
|
||||
export let pending = false;
|
||||
export let streaming = false;
|
||||
export let autoplay = false;
|
||||
|
||||
// TODO: make use of this
|
||||
// export let type: "normal" | "numpy" = "normal";
|
||||
@ -33,22 +34,21 @@
|
||||
let recorder: IMediaRecorder;
|
||||
let mode = "";
|
||||
let header: Uint8Array | undefined = undefined;
|
||||
let pending_stream: Array<Uint8Array> = [];
|
||||
let submit_pending_stream_on_pending_end: boolean = false;
|
||||
let pending_stream: Uint8Array[] = [];
|
||||
let submit_pending_stream_on_pending_end = false;
|
||||
let player: HTMLAudioElement;
|
||||
let inited = false;
|
||||
let crop_values = [0, 100];
|
||||
let crop_values: [number, number] = [0, 100];
|
||||
const STREAM_TIMESLICE = 500;
|
||||
const NUM_HEADER_BYTES = 44;
|
||||
let audio_chunks: Array<Blob> = [];
|
||||
let audio_blob;
|
||||
let audio_chunks: Blob[] = [];
|
||||
let module_promises:
|
||||
| [
|
||||
Promise<typeof import("extendable-media-recorder")>,
|
||||
Promise<typeof import("extendable-media-recorder-wav-encoder")>
|
||||
];
|
||||
|
||||
function get_modules() {
|
||||
function get_modules(): void {
|
||||
module_promises = [
|
||||
import("extendable-media-recorder"),
|
||||
import("extendable-media-recorder-wav-encoder")
|
||||
@ -85,18 +85,18 @@
|
||||
}
|
||||
|
||||
const dispatch_blob = async (
|
||||
blobs: Array<Uint8Array> | Blob[],
|
||||
blobs: Uint8Array[] | Blob[],
|
||||
event: "stream" | "change" | "stop_recording"
|
||||
) => {
|
||||
let audio_blob = new Blob(blobs, { type: "audio/wav" });
|
||||
): Promise<void> => {
|
||||
let _audio_blob = new Blob(blobs, { type: "audio/wav" });
|
||||
value = {
|
||||
data: await blob_to_data_url(audio_blob),
|
||||
data: await blob_to_data_url(_audio_blob),
|
||||
name: "audio.wav"
|
||||
};
|
||||
dispatch(event, value);
|
||||
};
|
||||
|
||||
async function prepare_audio() {
|
||||
async function prepare_audio(): Promise<void> {
|
||||
let stream: MediaStream | null;
|
||||
|
||||
try {
|
||||
@ -108,9 +108,8 @@
|
||||
"Please allow access to the microphone for recording."
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (stream == null) return;
|
||||
@ -124,7 +123,7 @@
|
||||
|
||||
recorder = new MediaRecorder(stream, { mimeType: "audio/wav" });
|
||||
|
||||
async function handle_chunk(event: IBlobEvent) {
|
||||
async function handle_chunk(event: IBlobEvent): Promise<void> {
|
||||
let buffer = await event.data.arrayBuffer();
|
||||
let payload = new Uint8Array(buffer);
|
||||
if (!header) {
|
||||
@ -161,13 +160,13 @@
|
||||
$: if (submit_pending_stream_on_pending_end && pending === false) {
|
||||
submit_pending_stream_on_pending_end = false;
|
||||
if (header && pending_stream) {
|
||||
let blobParts: Array<Uint8Array> = [header].concat(pending_stream);
|
||||
let blobParts: Uint8Array[] = [header].concat(pending_stream);
|
||||
pending_stream = [];
|
||||
dispatch_blob(blobParts, "stream");
|
||||
}
|
||||
}
|
||||
|
||||
async function record() {
|
||||
async function record(): Promise<void> {
|
||||
recording = true;
|
||||
dispatch("start_recording");
|
||||
if (!inited) await prepare_audio();
|
||||
@ -185,7 +184,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
const stop = async () => {
|
||||
function stop(): void {
|
||||
recorder.stop();
|
||||
if (streaming) {
|
||||
recording = false;
|
||||
@ -193,41 +192,20 @@
|
||||
submit_pending_stream_on_pending_end = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function clear() {
|
||||
function clear(): void {
|
||||
dispatch("change");
|
||||
dispatch("clear");
|
||||
mode = "";
|
||||
value = null;
|
||||
}
|
||||
|
||||
function loaded(node: HTMLAudioElement) {
|
||||
function clamp_playback() {
|
||||
const start_time = (crop_values[0] / 100) * node.duration;
|
||||
const end_time = (crop_values[1] / 100) * node.duration;
|
||||
if (node.currentTime < start_time) {
|
||||
node.currentTime = start_time;
|
||||
}
|
||||
|
||||
if (node.currentTime > end_time) {
|
||||
node.currentTime = start_time;
|
||||
node.pause();
|
||||
}
|
||||
}
|
||||
|
||||
node.addEventListener("timeupdate", clamp_playback);
|
||||
|
||||
return {
|
||||
destroy: () => node.removeEventListener("timeupdate", clamp_playback)
|
||||
};
|
||||
}
|
||||
|
||||
function handle_change({
|
||||
detail: { values }
|
||||
}: {
|
||||
detail: { values: [number, number] };
|
||||
}) {
|
||||
}): void {
|
||||
if (!value) return;
|
||||
|
||||
dispatch("change", {
|
||||
@ -249,29 +227,19 @@
|
||||
size: number;
|
||||
is_example: boolean;
|
||||
};
|
||||
}) {
|
||||
}): void {
|
||||
value = detail;
|
||||
dispatch("change", { data: detail.data, name: detail.name });
|
||||
dispatch("upload", detail);
|
||||
}
|
||||
|
||||
function handle_ended() {
|
||||
function handle_ended(): void {
|
||||
dispatch("stop");
|
||||
dispatch("end");
|
||||
}
|
||||
|
||||
let old_val: any;
|
||||
function value_has_changed(val: any) {
|
||||
if (val === old_val) return false;
|
||||
else {
|
||||
old_val = val;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export let dragging = false;
|
||||
$: dispatch("drag", dragging);
|
||||
$: autoplay && player && value_has_changed(value?.data) && player.play();
|
||||
</script>
|
||||
|
||||
<BlockLabel
|
||||
@ -319,15 +287,15 @@
|
||||
/>
|
||||
|
||||
<audio
|
||||
use:loaded
|
||||
use:loaded={{ autoplay, crop_values }}
|
||||
controls
|
||||
bind:this={player}
|
||||
preload="metadata"
|
||||
src={value.data}
|
||||
src={value?.data}
|
||||
on:play
|
||||
on:pause
|
||||
on:ended={handle_ended}
|
||||
data-testid={`${label}-dynamic-audio`}
|
||||
data-testid={`${label}-audio`}
|
||||
/>
|
||||
|
||||
{#if mode === "edit" && player?.duration}
|
||||
|
@ -8,15 +8,16 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
import { BlockLabel } from "@gradio/atoms";
|
||||
|
||||
import { Music } from "@gradio/icons";
|
||||
|
||||
import { loaded } from "./utils";
|
||||
|
||||
export let value: null | { name: string; data: string } = null;
|
||||
export let label: string;
|
||||
export let name: string;
|
||||
export let show_label: boolean = true;
|
||||
export let show_label = true;
|
||||
export let autoplay: boolean;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
@ -33,20 +34,7 @@
|
||||
data: value?.data
|
||||
});
|
||||
|
||||
let el: HTMLAudioElement;
|
||||
|
||||
let old_val: any;
|
||||
function value_has_changed(val: any) {
|
||||
if (val === old_val) return false;
|
||||
else {
|
||||
old_val = val;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$: autoplay && el && value_has_changed(value) && el.play();
|
||||
|
||||
function handle_ended() {
|
||||
function handle_ended(): void {
|
||||
dispatch("stop");
|
||||
dispatch("end");
|
||||
}
|
||||
@ -59,14 +47,14 @@
|
||||
</Empty>
|
||||
{:else}
|
||||
<audio
|
||||
bind:this={el}
|
||||
use:loaded={{ autoplay }}
|
||||
controls
|
||||
preload="metadata"
|
||||
src={value.data}
|
||||
src={value?.data}
|
||||
on:play
|
||||
on:pause
|
||||
on:ended={handle_ended}
|
||||
data-testid={`${label}-static-audio`}
|
||||
data-testid={`${label}-audio`}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
44
js/audio/src/utils.ts
Normal file
44
js/audio/src/utils.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import type { ActionReturn } from "svelte/action";
|
||||
|
||||
interface LoadedParams {
|
||||
crop_values?: [number, number];
|
||||
autoplay?: boolean;
|
||||
}
|
||||
|
||||
export function loaded(
|
||||
node: HTMLAudioElement,
|
||||
{ crop_values, autoplay }: LoadedParams = {}
|
||||
): ActionReturn {
|
||||
function clamp_playback(): void {
|
||||
if (crop_values === undefined) return;
|
||||
|
||||
const start_time = (crop_values[0] / 100) * node.duration;
|
||||
const end_time = (crop_values[1] / 100) * node.duration;
|
||||
|
||||
if (node.currentTime < start_time) {
|
||||
node.currentTime = start_time;
|
||||
}
|
||||
|
||||
if (node.currentTime > end_time) {
|
||||
node.currentTime = start_time;
|
||||
node.pause();
|
||||
}
|
||||
}
|
||||
|
||||
async function handle_playback(): Promise<void> {
|
||||
if (!autoplay) return;
|
||||
|
||||
node.pause();
|
||||
await node.play();
|
||||
}
|
||||
|
||||
node.addEventListener("loadeddata", handle_playback);
|
||||
node.addEventListener("timeupdate", clamp_playback);
|
||||
|
||||
return {
|
||||
destroy(): void {
|
||||
node.removeEventListener("loadeddata", handle_playback);
|
||||
node.removeEventListener("timeupdate", clamp_playback);
|
||||
}
|
||||
};
|
||||
}
|
@ -6,8 +6,8 @@ import Range from "./Range.svelte";
|
||||
describe("Range", () => {
|
||||
afterEach(() => cleanup());
|
||||
|
||||
test("Release event called on blur and pointerUp", () => {
|
||||
const results = render(Range, {
|
||||
test("Release event called on blur and pointerUp", async () => {
|
||||
const results = await render(Range, {
|
||||
label: "range",
|
||||
show_label: true,
|
||||
value: 1,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { getQueriesForElement, prettyDOM } from "@testing-library/dom";
|
||||
import { tick } from "svelte";
|
||||
import type { SvelteComponentTyped } from "svelte";
|
||||
|
||||
const containerCache = new Map();
|
||||
@ -9,7 +10,7 @@ type Component<T extends SvelteComponentTyped, Props> = new (args: {
|
||||
props?: Props;
|
||||
}) => T;
|
||||
|
||||
function render<
|
||||
async function render<
|
||||
Events extends Record<string, any>,
|
||||
Props extends Record<string, any>,
|
||||
T extends SvelteComponentTyped<Props, Events>
|
||||
@ -36,6 +37,8 @@ function render<
|
||||
componentCache.delete(component);
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
return {
|
||||
container,
|
||||
component,
|
||||
|
@ -1,11 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { tick, createEventDispatcher } from "svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { Play, Pause, Maximise, Undo } from "@gradio/icons";
|
||||
import { loaded } from "./utils";
|
||||
|
||||
export let src: string;
|
||||
export let subtitle: string | null = null;
|
||||
export let mirror: boolean;
|
||||
export let autoplay: boolean;
|
||||
export let label = "test";
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
play: undefined;
|
||||
@ -14,12 +16,12 @@
|
||||
end: undefined;
|
||||
}>();
|
||||
|
||||
let time: number = 0;
|
||||
let time = 0;
|
||||
let duration: number;
|
||||
let paused: boolean = true;
|
||||
let paused = true;
|
||||
let video: HTMLVideoElement;
|
||||
|
||||
function handleMove(e: TouchEvent | MouseEvent) {
|
||||
function handleMove(e: TouchEvent | MouseEvent): void {
|
||||
if (!duration) return;
|
||||
|
||||
if (e.type === "click") {
|
||||
@ -39,7 +41,7 @@
|
||||
time = (duration * (clientX - left)) / (right - left);
|
||||
}
|
||||
|
||||
async function play_pause() {
|
||||
async function play_pause(): Promise<void> {
|
||||
if (document.fullscreenElement != video) {
|
||||
const isPlaying =
|
||||
video.currentTime > 0 &&
|
||||
@ -53,14 +55,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handle_click(e: MouseEvent) {
|
||||
function handle_click(e: MouseEvent): void {
|
||||
const { left, right } = (
|
||||
e.currentTarget as HTMLProgressElement
|
||||
).getBoundingClientRect();
|
||||
time = (duration * (e.clientX - left)) / (right - left);
|
||||
}
|
||||
|
||||
function format(seconds: number) {
|
||||
function format(seconds: number): string {
|
||||
if (isNaN(seconds) || !isFinite(seconds)) return "...";
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
@ -70,36 +72,10 @@
|
||||
return `${minutes}:${_seconds}`;
|
||||
}
|
||||
|
||||
async function checkforVideo() {
|
||||
await tick();
|
||||
|
||||
await tick();
|
||||
|
||||
var b = setInterval(async () => {
|
||||
if (video.readyState >= 3) {
|
||||
video.currentTime = 9999;
|
||||
paused = true;
|
||||
|
||||
setTimeout(async () => {
|
||||
video.currentTime = 0.0;
|
||||
}, 50);
|
||||
clearInterval(b);
|
||||
}
|
||||
}, 15);
|
||||
}
|
||||
|
||||
async function _load() {
|
||||
checkforVideo();
|
||||
}
|
||||
|
||||
$: src && _load();
|
||||
|
||||
function handle_end() {
|
||||
function handle_end(): void {
|
||||
dispatch("stop");
|
||||
dispatch("end");
|
||||
}
|
||||
|
||||
$: autoplay && video && src && video.play();
|
||||
</script>
|
||||
|
||||
<div class="wrap">
|
||||
@ -115,6 +91,8 @@
|
||||
bind:paused
|
||||
bind:this={video}
|
||||
class:mirror
|
||||
use:loaded={{ autoplay }}
|
||||
data-testid={`${label}-player`}
|
||||
>
|
||||
<track kind="captions" src={subtitle} default />
|
||||
</video>
|
||||
|
@ -9,7 +9,7 @@
|
||||
export let value: FileData | null = null;
|
||||
export let subtitle: FileData | null = null;
|
||||
export let label: string | undefined = undefined;
|
||||
export let show_label: boolean = true;
|
||||
export let show_label = true;
|
||||
export let autoplay: boolean;
|
||||
|
||||
let old_value: FileData | null = null;
|
||||
@ -56,6 +56,7 @@
|
||||
on:pause
|
||||
on:ended
|
||||
mirror={false}
|
||||
{label}
|
||||
/>
|
||||
{/key}
|
||||
<div class="download" data-testid="download-div">
|
||||
|
@ -1,35 +0,0 @@
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
import { test, describe, afterEach } from "vitest";
|
||||
import { cleanup, render } from "@gradio/tootils";
|
||||
|
||||
import StaticVideo from "./StaticVideo.svelte";
|
||||
|
||||
describe("StaticVideo", () => {
|
||||
afterEach(() => cleanup());
|
||||
|
||||
const data = {
|
||||
data: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
|
||||
name: "a.mp4"
|
||||
};
|
||||
|
||||
test("renders video and download button", () => {
|
||||
const results = render(StaticVideo, {
|
||||
label: "video",
|
||||
show_label: true,
|
||||
value: data
|
||||
});
|
||||
|
||||
//expect(results.getAllByLabelText("video")).not.toThrow();
|
||||
|
||||
const downloadButton = results.getAllByTestId("download-div")[0];
|
||||
expect(
|
||||
downloadButton.getElementsByTagName("a")[0].getAttribute("href")
|
||||
).toBe(data.data);
|
||||
expect(
|
||||
downloadButton.getElementsByTagName("button").length
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
expect(downloadButton.getElementsByTagName("button")[0]).toBeVisible();
|
||||
});
|
||||
});
|
@ -13,8 +13,8 @@
|
||||
export let subtitle: FileData | null = null;
|
||||
export let source: string;
|
||||
export let label: string | undefined = undefined;
|
||||
export let show_label: boolean = true;
|
||||
export let mirror_webcam: boolean = false;
|
||||
export let show_label = true;
|
||||
export let mirror_webcam = false;
|
||||
export let include_audio: boolean;
|
||||
export let autoplay: boolean;
|
||||
|
||||
@ -67,17 +67,19 @@
|
||||
{:else}
|
||||
<ModifyUpload on:clear={handle_clear} />
|
||||
{#if playable()}
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<Player
|
||||
{autoplay}
|
||||
src={value.data}
|
||||
subtitle={subtitle?.data}
|
||||
on:play
|
||||
on:pause
|
||||
on:stop
|
||||
on:end
|
||||
mirror={mirror_webcam && source === "webcam"}
|
||||
/>
|
||||
{#key value?.data}
|
||||
<Player
|
||||
{autoplay}
|
||||
src={value.data}
|
||||
subtitle={subtitle?.data}
|
||||
on:play
|
||||
on:pause
|
||||
on:stop
|
||||
on:end
|
||||
mirror={mirror_webcam && source === "webcam"}
|
||||
{label}
|
||||
/>
|
||||
{/key}
|
||||
{:else if value.size}
|
||||
<div class="file-name">{value.name}</div>
|
||||
<div class="file-size">
|
||||
|
@ -1,3 +1,5 @@
|
||||
import type { ActionReturn } from "svelte/action";
|
||||
|
||||
export const prettyBytes = (bytes: number): string => {
|
||||
let units = ["B", "KB", "MB", "GB", "PB"];
|
||||
let i = 0;
|
||||
@ -15,3 +17,21 @@ export const playable = (): boolean => {
|
||||
// return video_element.canPlayType(mime_type) != "";
|
||||
return true; // FIX BEFORE COMMIT - mime import causing issues
|
||||
};
|
||||
|
||||
export function loaded(
|
||||
node: HTMLVideoElement,
|
||||
{ autoplay }: { autoplay: boolean }
|
||||
): ActionReturn {
|
||||
async function handle_playback(): Promise<void> {
|
||||
if (!autoplay) return;
|
||||
await node.play();
|
||||
}
|
||||
|
||||
node.addEventListener("loadeddata", handle_playback);
|
||||
|
||||
return {
|
||||
destroy(): void {
|
||||
node.removeEventListener("loadeddata", handle_playback);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user