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 React, { useEffect, useMemo, useState } from "react";
|
||||||
import { SoundOnIcon, SoundOffIcon } from "~/components/Icon";
|
import { SoundOnIcon, SoundOffIcon } from "~/components/Icon";
|
||||||
|
|
||||||
|
type TAudioStatus = "play" | "pause" | "wait" | "end";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
autoPlay?: boolean;
|
||||||
|
onStatusChange?: (s: TAudioStatus) => void;
|
||||||
|
audio: HTMLAudioElement;
|
||||||
surah: Surah<TSurah>;
|
surah: Surah<TSurah>;
|
||||||
ayah: Ayah<TAyah>;
|
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({
|
export function AudioControl({
|
||||||
|
autoPlay = false,
|
||||||
|
onStatusChange = () => null,
|
||||||
|
audio,
|
||||||
surah,
|
surah,
|
||||||
ayah,
|
ayah,
|
||||||
onPlay = () => null,
|
|
||||||
onPlaying = () => null,
|
|
||||||
onPause = () => null,
|
|
||||||
onStall = () => null,
|
|
||||||
onEnd = () => null,
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [soundOn, setSoundOn] = useState<boolean>(false);
|
const [enabled, setEnabled] = useState<boolean>(false);
|
||||||
const audio = useMemo(() => new Audio(), []);
|
const [audioStatus, setAudioStatus] = useState<TAudioStatus>(null);
|
||||||
const turnOnSound = () => setSoundOn(true);
|
const play = (audio: HTMLAudioElement) => audio.play().catch(() => null);
|
||||||
const turnOffSound = () => setSoundOn(false);
|
const pause = (audio: HTMLAudioElement) => audio.pause();
|
||||||
const recover = () => {
|
|
||||||
if (!soundOn) return;
|
|
||||||
onStall();
|
|
||||||
audio.play().catch(() => setTimeout(recover, 50));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
audio.addEventListener("ended", () => onEnd(turnOffSound));
|
if (audio) {
|
||||||
audio.addEventListener("stalled", recover);
|
audio.src = [
|
||||||
audio.addEventListener("waiting", onStall);
|
"https://al-quran.reflectslight.io",
|
||||||
audio.addEventListener("play", onPlay);
|
"audio",
|
||||||
audio.addEventListener("playing", onPlaying);
|
"alafasy",
|
||||||
}, []);
|
surah.id,
|
||||||
|
`${ayah.id}.mp3`,
|
||||||
useEffect(() => {
|
].join("/");
|
||||||
const src = [
|
if (autoPlay) {
|
||||||
"https://al-quran.reflectslight.io",
|
play(audio);
|
||||||
"audio",
|
}
|
||||||
"alafasy",
|
|
||||||
surah.id,
|
|
||||||
`${ayah.id}.mp3`,
|
|
||||||
].join("/");
|
|
||||||
if (soundOn) {
|
|
||||||
audio.src = src;
|
|
||||||
audio.play();
|
|
||||||
} else {
|
|
||||||
audio.pause();
|
|
||||||
onPause();
|
|
||||||
}
|
}
|
||||||
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{soundOn && <SoundOnIcon onClick={turnOffSound} />}
|
{enabled && (
|
||||||
{!soundOn && <SoundOffIcon onClick={turnOnSound} />}
|
<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 })}
|
className={classNames("flex h-8 items-center", { "mb-2": rtl })}
|
||||||
>
|
>
|
||||||
{(isPaused || endOfStream) && (
|
{(isPaused || endOfStream) && (
|
||||||
<AudioControl
|
<AudioControl audio={new Audio()} surah={surah} ayah={ayah} />
|
||||||
surah={surah}
|
|
||||||
ayah={ayah}
|
|
||||||
onEnd={turnOffSound => turnOffSound()}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<span>
|
<span>
|
||||||
{t(locale, "surah")} {formatNumber(surah.id, locale)}
|
{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 classNames from "classnames";
|
||||||
import { Surah, TSurah, TAyat, TLocale } from "Quran";
|
import { Surah, TSurah, TAyat, TLocale } from "Quran";
|
||||||
import { useTheme } from "~/hooks/useTheme";
|
import { useTheme } from "~/hooks/useTheme";
|
||||||
|
@ -23,14 +23,14 @@ type Props = {
|
||||||
export function SurahStream({ surah, locale, t }: Props) {
|
export function SurahStream({ surah, locale, t }: Props) {
|
||||||
const [stream, setStream] = useState<TAyat>([]);
|
const [stream, setStream] = useState<TAyat>([]);
|
||||||
const [isPaused, setIsPaused] = useState<boolean>(false);
|
const [isPaused, setIsPaused] = useState<boolean>(false);
|
||||||
const [soundOn, setSoundOn] = useState<boolean>(false);
|
const [audioStatus, setAudioStatus] = useState<TAudioStatus>(null);
|
||||||
const [isStalled, setIsStalled] = useState<boolean>(false);
|
|
||||||
const [endOfStream, setEndOfStream] = useState<boolean>(false);
|
const [endOfStream, setEndOfStream] = useState<boolean>(false);
|
||||||
const [theme, setTheme] = useTheme();
|
const [theme, setTheme] = useTheme();
|
||||||
const readyToRender = stream.length > 0;
|
const readyToRender = stream.length > 0;
|
||||||
const ayah = stream[stream.length - 1];
|
const ayah = stream[stream.length - 1];
|
||||||
const [ms, setMs] = useState<number | null>(null);
|
const [ms, setMs] = useState<number | null>(null);
|
||||||
const ref = useRef<HTMLDivElement>();
|
const ref = useRef<HTMLDivElement>();
|
||||||
|
const audio = useMemo(() => new Audio(), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
|
@ -84,16 +84,15 @@ export function SurahStream({ surah, locale, t }: Props) {
|
||||||
{readyToRender && !endOfStream && (
|
{readyToRender && !endOfStream && (
|
||||||
<div className="sound-box flex w-14 justify-end">
|
<div className="sound-box flex w-14 justify-end">
|
||||||
<AudioControl
|
<AudioControl
|
||||||
|
autoPlay={true}
|
||||||
|
audio={audio}
|
||||||
surah={surah}
|
surah={surah}
|
||||||
ayah={ayah}
|
ayah={ayah}
|
||||||
onPlay={() => setSoundOn(true)}
|
onStatusChange={s => setAudioStatus(s)}
|
||||||
onPause={() => setSoundOn(false)}
|
|
||||||
onPlaying={() => setIsStalled(false)}
|
|
||||||
onStall={() => setIsStalled(true)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{readyToRender && !endOfStream && !isStalled && (
|
{readyToRender && !endOfStream && audioStatus !== "wait" && (
|
||||||
<Timer
|
<Timer
|
||||||
surah={surah}
|
surah={surah}
|
||||||
setStream={setStream}
|
setStream={setStream}
|
||||||
|
@ -101,13 +100,12 @@ export function SurahStream({ surah, locale, t }: Props) {
|
||||||
stream={stream}
|
stream={stream}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
isPaused={isPaused}
|
isPaused={isPaused}
|
||||||
soundOn={soundOn}
|
isStalled={audioStatus === "wait"}
|
||||||
isStalled={isStalled}
|
|
||||||
ms={ms}
|
ms={ms}
|
||||||
setMs={setMs}
|
setMs={setMs}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{readyToRender && soundOn && isStalled && <StalledIcon />}
|
{readyToRender && audioStatus === "wait" && <StalledIcon />}
|
||||||
{readyToRender && endOfStream && (
|
{readyToRender && endOfStream && (
|
||||||
<RefreshIcon onClick={() => setStream([])} />
|
<RefreshIcon onClick={() => setStream([])} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -7,8 +7,10 @@ import { SurahIndex } from "~/components/SurahIndex";
|
||||||
(function () {
|
(function () {
|
||||||
const root: HTMLElement = document.querySelector(".root")!;
|
const root: HTMLElement = document.querySelector(".root")!;
|
||||||
const locale = root.getAttribute("data-locale") as TLocale;
|
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 t = T(require("@json/t.json"));
|
||||||
|
const surahs: Surah<TSurah>[] = require("@json/surahs").map(
|
||||||
|
(e: TSurah) => new Surah(e),
|
||||||
|
);
|
||||||
ReactDOM.createRoot(root).render(
|
ReactDOM.createRoot(root).render(
|
||||||
<SurahIndex locale={locale} surahs={surahs} t={t} />,
|
<SurahIndex locale={locale} surahs={surahs} t={t} />,
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue