All checks were successful
Deploy / build-and-deploy (push) Successful in 1m43s
103 lines
2.3 KiB
TypeScript
103 lines
2.3 KiB
TypeScript
import { Howl } from "howler";
|
|
|
|
export type PlaybackState = {
|
|
isPlaying: boolean;
|
|
url: string | null;
|
|
name: string | null;
|
|
elapsed: number;
|
|
duration: number;
|
|
};
|
|
|
|
type Listener = (s: PlaybackState) => void;
|
|
|
|
const MAX_SECONDS = 6;
|
|
const FADE_MS = 500;
|
|
|
|
let state: PlaybackState = {
|
|
isPlaying: false,
|
|
url: null,
|
|
name: null,
|
|
elapsed: 0,
|
|
duration: MAX_SECONDS,
|
|
};
|
|
|
|
const listeners = new Set<Listener>();
|
|
|
|
let howlInstance: Howl | null = null;
|
|
let soundId: number | null = null;
|
|
let stopTimer: ReturnType<typeof setTimeout> | null = null;
|
|
let progressTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
function emit() {
|
|
listeners.forEach(fn => fn(state));
|
|
}
|
|
|
|
function resetTimers() {
|
|
if (stopTimer) clearTimeout(stopTimer);
|
|
if (progressTimer) clearInterval(progressTimer);
|
|
stopTimer = null;
|
|
progressTimer = null;
|
|
}
|
|
|
|
function stopSound() {
|
|
if (howlInstance && soundId !== null) {
|
|
howlInstance.stop(soundId);
|
|
}
|
|
howlInstance = null;
|
|
soundId = null;
|
|
}
|
|
|
|
export function getPlaybackState(): PlaybackState {
|
|
return state;
|
|
}
|
|
|
|
export function subscribe(fn: Listener): () => void {
|
|
listeners.add(fn);
|
|
return () => listeners.delete(fn);
|
|
}
|
|
|
|
export function playUrl(url: string, name?: string) {
|
|
stopCurrent();
|
|
|
|
const displayName = name || url.split("/").pop() || url;
|
|
|
|
howlInstance = new Howl({
|
|
src: [url],
|
|
format: url.endsWith(".mp3") ? ["mp3"] : url.endsWith(".ogg") ? ["ogg"] : ["mp3", "ogg"],
|
|
volume: 0.4,
|
|
});
|
|
|
|
soundId = howlInstance.play();
|
|
|
|
state = { isPlaying: true, url, name: displayName, elapsed: 0, duration: MAX_SECONDS };
|
|
emit();
|
|
|
|
// Progress tick
|
|
progressTimer = setInterval(() => {
|
|
if (howlInstance && soundId !== null) {
|
|
const seek = howlInstance.seek(soundId) as number;
|
|
state = { ...state, elapsed: seek };
|
|
emit();
|
|
}
|
|
}, 100);
|
|
|
|
// Cap at MAX_SECONDS with fadeout
|
|
const fadeStart = (MAX_SECONDS - FADE_MS / 1000) * 1000;
|
|
stopTimer = setTimeout(() => {
|
|
if (howlInstance && soundId !== null) {
|
|
try { howlInstance.fade(0.4, 0, FADE_MS, soundId); } catch {}
|
|
setTimeout(() => stopCurrent(), FADE_MS);
|
|
}
|
|
}, fadeStart);
|
|
|
|
// Natural end
|
|
howlInstance.on("end", () => stopCurrent());
|
|
}
|
|
|
|
export function stopCurrent() {
|
|
stopSound();
|
|
resetTimers();
|
|
state = { isPlaying: false, url: null, name: null, elapsed: 0, duration: MAX_SECONDS };
|
|
emit();
|
|
}
|