my_gradio / js /audio /player /AudioPlayer.svelte
xray918's picture
Upload folder using huggingface_hub
0ad74ed verified
<script lang="ts">
import { onMount } from "svelte";
import { Music } from "@gradio/icons";
import { format_time, type I18nFormatter } from "@gradio/utils";
import WaveSurfer from "wavesurfer.js";
import { skip_audio, process_audio } from "../shared/utils";
import WaveformControls from "../shared/WaveformControls.svelte";
import { Empty } from "@gradio/atoms";
import { resolve_wasm_src } from "@gradio/wasm/svelte";
import type { FileData } from "@gradio/client";
import type { WaveformOptions } from "../shared/types";
import { createEventDispatcher } from "svelte";
import Hls from "hls.js";
export let value: null | FileData = null;
$: url = value?.url;
export let label: string;
export let i18n: I18nFormatter;
export let dispatch_blob: (
blobs: Uint8Array[] | Blob[],
event: "stream" | "change" | "stop_recording"
) => Promise<void> = () => Promise.resolve();
export let interactive = false;
export let editable = true;
export let trim_region_settings = {};
export let waveform_settings: Record<string, any>;
export let waveform_options: WaveformOptions;
export let mode = "";
export let loop: boolean;
export let handle_reset_value: () => void = () => {};
let container: HTMLDivElement;
let waveform: WaveSurfer | undefined;
let playing = false;
let timeRef: HTMLTimeElement;
let durationRef: HTMLTimeElement;
let audio_duration: number;
let trimDuration = 0;
let show_volume_slider = false;
let audio_player: HTMLAudioElement;
let stream_active = false;
const dispatch = createEventDispatcher<{
stop: undefined;
play: undefined;
pause: undefined;
edit: undefined;
end: undefined;
load: undefined;
}>();
const create_waveform = (): void => {
waveform = WaveSurfer.create({
container: container,
...waveform_settings
});
resolve_wasm_src(value?.url).then((resolved_src) => {
if (resolved_src && waveform) {
return waveform.load(resolved_src);
}
});
};
$: if (!value?.is_stream && container !== undefined && container !== null) {
if (waveform !== undefined) waveform.destroy();
container.innerHTML = "";
create_waveform();
playing = false;
}
$: waveform?.on("decode", (duration: any) => {
audio_duration = duration;
durationRef && (durationRef.textContent = format_time(duration));
});
$: waveform?.on(
"timeupdate",
(currentTime: any) =>
timeRef && (timeRef.textContent = format_time(currentTime))
);
$: waveform?.on("ready", () => {
if (!waveform_settings.autoplay) {
waveform?.stop();
} else {
waveform?.play();
}
});
$: waveform?.on("finish", () => {
if (loop) {
waveform?.play();
} else {
playing = false;
dispatch("stop");
}
});
$: waveform?.on("pause", () => {
playing = false;
dispatch("pause");
});
$: waveform?.on("play", () => {
playing = true;
dispatch("play");
});
$: waveform?.on("load", () => {
dispatch("load");
});
const handle_trim_audio = async (
start: number,
end: number
): Promise<void> => {
mode = "";
const decodedData = waveform?.getDecodedData();
if (decodedData)
await process_audio(
decodedData,
start,
end,
waveform_settings.sampleRate
).then(async (trimmedBlob: Uint8Array) => {
await dispatch_blob([trimmedBlob], "change");
waveform?.destroy();
container.innerHTML = "";
});
dispatch("edit");
};
async function load_audio(data: string): Promise<void> {
stream_active = false;
await resolve_wasm_src(data).then((resolved_src) => {
if (!resolved_src || value?.is_stream) return;
return waveform?.load(resolved_src);
});
}
$: url && load_audio(url);
function load_stream(value: FileData | null): void {
if (!value || !value.is_stream || !value.url) return;
if (!audio_player) return;
if (Hls.isSupported() && !stream_active) {
// Set config to start playback after 1 second of data received
const hls = new Hls({
maxBufferLength: 1,
maxMaxBufferLength: 1,
lowLatencyMode: true
});
hls.loadSource(value.url);
hls.attachMedia(audio_player);
hls.on(Hls.Events.MANIFEST_PARSED, function () {
if (waveform_settings.autoplay) audio_player.play();
});
hls.on(Hls.Events.ERROR, function (event, data) {
console.error("HLS error:", event, data);
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
console.error(
"Fatal network error encountered, trying to recover"
);
hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.error("Fatal media error encountered, trying to recover");
hls.recoverMediaError();
break;
default:
console.error("Fatal error, cannot recover");
hls.destroy();
break;
}
}
});
stream_active = true;
} else if (!stream_active) {
audio_player.src = value.url;
if (waveform_settings.autoplay) audio_player.play();
stream_active = true;
}
}
$: load_stream(value);
onMount(() => {
window.addEventListener("keydown", (e) => {
if (!waveform || show_volume_slider) return;
if (e.key === "ArrowRight" && mode !== "edit") {
skip_audio(waveform, 0.1);
} else if (e.key === "ArrowLeft" && mode !== "edit") {
skip_audio(waveform, -0.1);
}
});
});
</script>
<audio
class="standard-player"
class:hidden={!(value && value.is_stream)}
controls
autoplay={waveform_settings.autoplay}
on:load
bind:this={audio_player}
on:ended={() => dispatch("stop")}
on:play={() => dispatch("play")}
/>
{#if value === null}
<Empty size="small">
<Music />
</Empty>
{:else if !value.is_stream}
<div
class="component-wrapper"
data-testid={label ? "waveform-" + label : "unlabelled-audio"}
>
<div class="waveform-container">
<div
id="waveform"
bind:this={container}
style:height={container ? null : "58px"}
/>
</div>
<div class="timestamps">
<time bind:this={timeRef} id="time">0:00</time>
<div>
{#if mode === "edit" && trimDuration > 0}
<time id="trim-duration">{format_time(trimDuration)}</time>
{/if}
<time bind:this={durationRef} id="duration">0:00</time>
</div>
</div>
<!-- {#if waveform} -->
<WaveformControls
{container}
{waveform}
{playing}
{audio_duration}
{i18n}
{interactive}
{handle_trim_audio}
bind:mode
bind:trimDuration
bind:show_volume_slider
show_redo={interactive}
{handle_reset_value}
{waveform_options}
{trim_region_settings}
{editable}
/>
<!-- {/if} -->
</div>
{/if}
<style>
.component-wrapper {
padding: var(--size-3);
width: 100%;
}
:global(::part(wrapper)) {
margin-bottom: var(--size-2);
}
.timestamps {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: var(--size-1) 0;
}
#time {
color: var(--neutral-400);
}
#duration {
color: var(--neutral-400);
}
#trim-duration {
color: var(--color-accent);
margin-right: var(--spacing-sm);
}
.waveform-container {
display: flex;
align-items: center;
justify-content: center;
width: var(--size-full);
}
#waveform {
width: 100%;
height: 100%;
position: relative;
}
.standard-player {
width: 100%;
padding: var(--size-2);
}
.hidden {
display: none;
}
</style>