diff --git a/CHANGELOG.md b/CHANGELOG.md index faa8785250..53b55196e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: diff --git a/demo/audio_component/run.ipynb b/demo/audio_component/run.ipynb index c2709434e0..14bb564cf8 100644 --- a/demo/audio_component/run.ipynb +++ b/demo/audio_component/run.ipynb @@ -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} \ No newline at end of file +{"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} \ No newline at end of file diff --git a/demo/audio_component/run.py b/demo/audio_component/run.py index e6ea63ad9c..ac522bb8a9 100644 --- a/demo/audio_component/run.py +++ b/demo/audio_component/run.py @@ -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() \ No newline at end of file diff --git a/js/app/src/components/Audio/Audio.svelte b/js/app/src/components/Audio/Audio.svelte index 1ffc50ee0b..5395905fd4 100644 --- a/js/app/src/components/Audio/Audio.svelte +++ b/js/app/src/components/Audio/Audio.svelte @@ -18,9 +18,9 @@ error: string; }>(); - export let elem_id: string = ""; - export let elem_classes: Array = []; - 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); diff --git a/js/app/src/components/Audio/Audio.test.ts b/js/app/src/components/Audio/Audio.test.ts index 4bd7dd9a88..35b238f053 100644 --- a/js/app/src/components/Audio/Audio.test.ts +++ b/js/app/src/components/Audio/Audio.test.ts @@ -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("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("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("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("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); + }); }); diff --git a/js/app/src/components/Button/Button.test.ts b/js/app/src/components/Button/Button.test.ts index 422c856cd8..9d4bd221fa 100644 --- a/js/app/src/components/Button/Button.test.ts +++ b/js/app/src/components/Button/Button.test.ts @@ -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); diff --git a/js/app/src/components/Chatbot/Chatbot.test.ts b/js/app/src/components/Chatbot/Chatbot.test.ts index 6b125cc610..337ae292ad 100644 --- a/js/app/src/components/Chatbot/Chatbot.test.ts +++ b/js/app/src/components/Chatbot/Chatbot.test.ts @@ -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"]], diff --git a/js/app/src/components/ColorPicker/ColorPicker.test.ts b/js/app/src/components/ColorPicker/ColorPicker.test.ts index 8c53cd0abd..a896efc8cc 100644 --- a/js/app/src/components/ColorPicker/ColorPicker.test.ts +++ b/js/app/src/components/ColorPicker/ColorPicker.test.ts @@ -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", diff --git a/js/app/src/components/Gallery/Gallery.test.ts b/js/app/src/components/Gallery/Gallery.test.ts index 22a1f4ee51..3f7ae7a720 100644 --- a/js/app/src/components/Gallery/Gallery.test.ts +++ b/js/app/src/components/Gallery/Gallery.test.ts @@ -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 diff --git a/js/app/src/components/Radio/Radio.test.ts b/js/app/src/components/Radio/Radio.test.ts index 748fb109f9..0e0abff632 100644 --- a/js/app/src/components/Radio/Radio.test.ts +++ b/js/app/src/components/Radio/Radio.test.ts @@ -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, diff --git a/js/app/src/components/Textbox/Textbox.test.ts b/js/app/src/components/Textbox/Textbox.test.ts index 55df164941..a47774ef0e 100644 --- a/js/app/src/components/Textbox/Textbox.test.ts +++ b/js/app/src/components/Textbox/Textbox.test.ts @@ -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, diff --git a/js/app/src/components/Video/Video.svelte b/js/app/src/components/Video/Video.svelte index 5972d47fa3..e80e1ca70b 100644 --- a/js/app/src/components/Video/Video.svelte +++ b/js/app/src/components/Video/Video.svelte @@ -10,9 +10,9 @@ import type { LoadingStatus } from "../StatusTracker/types"; import { _ } from "svelte-i18n"; - export let elem_id: string = ""; - export let elem_classes: Array = []; - 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; diff --git a/js/app/src/components/Video/Video.test.ts b/js/app/src/components/Video/Video.test.ts new file mode 100644 index 0000000000..31236a28ca --- /dev/null +++ b/js/app/src/components/Video/Video.test.ts @@ -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("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("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("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("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); + }); +}); diff --git a/js/audio/src/Audio.svelte b/js/audio/src/Audio.svelte index 31558241d0..b9a967a56a 100644 --- a/js/audio/src/Audio.svelte +++ b/js/audio/src/Audio.svelte @@ -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 = []; - 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 = []; - let audio_blob; + let audio_chunks: Blob[] = []; let module_promises: | [ Promise, Promise ]; - 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 | Blob[], + blobs: Uint8Array[] | Blob[], event: "stream" | "change" | "stop_recording" - ) => { - let audio_blob = new Blob(blobs, { type: "audio/wav" }); + ): Promise => { + 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 { 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 { 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 = [header].concat(pending_stream); + let blobParts: Uint8Array[] = [header].concat(pending_stream); pending_stream = []; dispatch_blob(blobParts, "stream"); } } - async function record() { + async function record(): Promise { 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();