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:
pngwn 2023-06-28 19:37:21 +01:00 committed by GitHub
parent 58c6d68f20
commit 1650e1d383
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 570 additions and 210 deletions

View File

@ -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:

View File

@ -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}

View File

@ -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()

View File

@ -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);

View File

@ -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);
});
});

View File

@ -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);

View File

@ -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"]],

View File

@ -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",

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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;

View 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);
});
});

View File

@ -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}

View File

@ -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
View 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);
}
};
}

View File

@ -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,

View File

@ -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,

View File

@ -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>

View File

@ -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">

View File

@ -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();
});
});

View File

@ -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">

View File

@ -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);
}
};
}