Hoist 'audio' (HTMLAudioElement) into parent component
A new approach that gives the caller (parent) control over the HTMLAudioElement. This will hopefully provide a framework for a stable implementation that can handle multiple retries over a slow network.
This commit is contained in:
parent
34bcbcdc19
commit
d3f075e98e
4 changed files with 72 additions and 60 deletions
|
@ -3,64 +3,80 @@ import type { Surah, TSurah, Ayah, TAyah } from "Quran";
|
|||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { SoundOnIcon, SoundOffIcon } from "~/components/Icon";
|
||||
|
||||
type TAudioStatus = "play" | "pause" | "wait" | "end";
|
||||
|
||||
type Props = {
|
||||
autoPlay?: boolean;
|
||||
onStatusChange?: (s: TAudioStatus) => void;
|
||||
audio: HTMLAudioElement;
|
||||
surah: Surah<TSurah>;
|
||||
ayah: Ayah<TAyah>;
|
||||
onStall?: (e?: Event) => void;
|
||||
onPlay?: (e?: Event) => void;
|
||||
onPlaying?: (e?: Event) => void;
|
||||
onPause?: (e?: Event) => void;
|
||||
onEnd?: (turnOffSound: () => void) => void;
|
||||
};
|
||||
|
||||
export function AudioControl({
|
||||
autoPlay = false,
|
||||
onStatusChange = () => null,
|
||||
audio,
|
||||
surah,
|
||||
ayah,
|
||||
onPlay = () => null,
|
||||
onPlaying = () => null,
|
||||
onPause = () => null,
|
||||
onStall = () => null,
|
||||
onEnd = () => null,
|
||||
}: Props) {
|
||||
const [soundOn, setSoundOn] = useState<boolean>(false);
|
||||
const audio = useMemo(() => new Audio(), []);
|
||||
const turnOnSound = () => setSoundOn(true);
|
||||
const turnOffSound = () => setSoundOn(false);
|
||||
const recover = () => {
|
||||
if (!soundOn) return;
|
||||
onStall();
|
||||
audio.play().catch(() => setTimeout(recover, 50));
|
||||
};
|
||||
const [enabled, setEnabled] = useState<boolean>(false);
|
||||
const [audioStatus, setAudioStatus] = useState<TAudioStatus>(null);
|
||||
const play = (audio: HTMLAudioElement) => audio.play().catch(() => null);
|
||||
const pause = (audio: HTMLAudioElement) => audio.pause();
|
||||
|
||||
useEffect(() => {
|
||||
audio.addEventListener("ended", () => onEnd(turnOffSound));
|
||||
audio.addEventListener("stalled", recover);
|
||||
audio.addEventListener("waiting", onStall);
|
||||
audio.addEventListener("play", onPlay);
|
||||
audio.addEventListener("playing", onPlaying);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const src = [
|
||||
if (audio) {
|
||||
audio.src = [
|
||||
"https://al-quran.reflectslight.io",
|
||||
"audio",
|
||||
"alafasy",
|
||||
surah.id,
|
||||
`${ayah.id}.mp3`,
|
||||
].join("/");
|
||||
if (soundOn) {
|
||||
audio.src = src;
|
||||
audio.play();
|
||||
} else {
|
||||
audio.pause();
|
||||
onPause();
|
||||
if (autoPlay) {
|
||||
play(audio);
|
||||
}
|
||||
}, [soundOn, ayah.id]);
|
||||
}
|
||||
}, [ayah.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (audioStatus === "end") {
|
||||
setEnabled(false);
|
||||
}
|
||||
onStatusChange(audioStatus);
|
||||
}, [audioStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!audio) return;
|
||||
const onPlay = () => setAudioStatus("play");
|
||||
const onPause = () => setAudioStatus("pause");
|
||||
const onEnd = () => setAudioStatus("end");
|
||||
const onWait = () => [setAudioStatus("wait"), play(audio)];
|
||||
audio.addEventListener("play", onPlay);
|
||||
audio.addEventListener("playing", onPlay);
|
||||
audio.addEventListener("pause", onPause);
|
||||
audio.addEventListener("ended", onEnd);
|
||||
audio.addEventListener("stalled", onWait);
|
||||
audio.addEventListener("waiting", onWait);
|
||||
return () => {
|
||||
audio.removeEventListener("play", onPlay);
|
||||
audio.removeEventListener("playing", onPlay);
|
||||
audio.removeEventListener("pause", onPause);
|
||||
audio.removeEventListener("ended", onEnd);
|
||||
audio.removeEventListener("stalled", onWait);
|
||||
audio.removeEventListener("waiting", onWait);
|
||||
};
|
||||
}, [ayah.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{soundOn && <SoundOnIcon onClick={turnOffSound} />}
|
||||
{!soundOn && <SoundOffIcon onClick={turnOnSound} />}
|
||||
{enabled && (
|
||||
<SoundOnIcon onClick={() => [setEnabled(false), pause(audio)]} />
|
||||
)}
|
||||
{!enabled && (
|
||||
<SoundOffIcon onClick={() => [setEnabled(true), play(audio)]} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -45,11 +45,7 @@ export function Stream({
|
|||
className={classNames("flex h-8 items-center", { "mb-2": rtl })}
|
||||
>
|
||||
{(isPaused || endOfStream) && (
|
||||
<AudioControl
|
||||
surah={surah}
|
||||
ayah={ayah}
|
||||
onEnd={turnOffSound => turnOffSound()}
|
||||
/>
|
||||
<AudioControl audio={new Audio()} surah={surah} ayah={ayah} />
|
||||
)}
|
||||
<span>
|
||||
{t(locale, "surah")} {formatNumber(surah.id, locale)}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, useRef } from "react";
|
||||
import React, { useState, useEffect, useMemo, useRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import { Surah, TSurah, TAyat, TLocale } from "Quran";
|
||||
import { useTheme } from "~/hooks/useTheme";
|
||||
|
@ -23,14 +23,14 @@ type Props = {
|
|||
export function SurahStream({ surah, locale, t }: Props) {
|
||||
const [stream, setStream] = useState<TAyat>([]);
|
||||
const [isPaused, setIsPaused] = useState<boolean>(false);
|
||||
const [soundOn, setSoundOn] = useState<boolean>(false);
|
||||
const [isStalled, setIsStalled] = useState<boolean>(false);
|
||||
const [audioStatus, setAudioStatus] = useState<TAudioStatus>(null);
|
||||
const [endOfStream, setEndOfStream] = useState<boolean>(false);
|
||||
const [theme, setTheme] = useTheme();
|
||||
const readyToRender = stream.length > 0;
|
||||
const ayah = stream[stream.length - 1];
|
||||
const [ms, setMs] = useState<number | null>(null);
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
const audio = useMemo(() => new Audio(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
|
@ -84,16 +84,15 @@ export function SurahStream({ surah, locale, t }: Props) {
|
|||
{readyToRender && !endOfStream && (
|
||||
<div className="sound-box flex w-14 justify-end">
|
||||
<AudioControl
|
||||
autoPlay={true}
|
||||
audio={audio}
|
||||
surah={surah}
|
||||
ayah={ayah}
|
||||
onPlay={() => setSoundOn(true)}
|
||||
onPause={() => setSoundOn(false)}
|
||||
onPlaying={() => setIsStalled(false)}
|
||||
onStall={() => setIsStalled(true)}
|
||||
onStatusChange={s => setAudioStatus(s)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{readyToRender && !endOfStream && !isStalled && (
|
||||
{readyToRender && !endOfStream && audioStatus !== "wait" && (
|
||||
<Timer
|
||||
surah={surah}
|
||||
setStream={setStream}
|
||||
|
@ -101,13 +100,12 @@ export function SurahStream({ surah, locale, t }: Props) {
|
|||
stream={stream}
|
||||
locale={locale}
|
||||
isPaused={isPaused}
|
||||
soundOn={soundOn}
|
||||
isStalled={isStalled}
|
||||
isStalled={audioStatus === "wait"}
|
||||
ms={ms}
|
||||
setMs={setMs}
|
||||
/>
|
||||
)}
|
||||
{readyToRender && soundOn && isStalled && <StalledIcon />}
|
||||
{readyToRender && audioStatus === "wait" && <StalledIcon />}
|
||||
{readyToRender && endOfStream && (
|
||||
<RefreshIcon onClick={() => setStream([])} />
|
||||
)}
|
||||
|
|
|
@ -7,8 +7,10 @@ import { SurahIndex } from "~/components/SurahIndex";
|
|||
(function () {
|
||||
const root: HTMLElement = document.querySelector(".root")!;
|
||||
const locale = root.getAttribute("data-locale") as TLocale;
|
||||
const surahs: Surah<TSurah>[] = require("@json/surahs").map((e: TSurah) => new Surah(e));
|
||||
const t = T(require("@json/t.json"));
|
||||
const surahs: Surah<TSurah>[] = require("@json/surahs").map(
|
||||
(e: TSurah) => new Surah(e),
|
||||
);
|
||||
ReactDOM.createRoot(root).render(
|
||||
<SurahIndex locale={locale} surahs={surahs} t={t} />,
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue