From d3e93ec39b467604ba0d074f35d774e37fe1e3cb Mon Sep 17 00:00:00 2001 From: an-lee Date: Wed, 19 Jun 2024 23:15:34 +0800 Subject: [PATCH] Fix multiple players playing (#688) * only one player playing in Square page * refactor player * rename component * refactor post recording --- enjoy/src/renderer/components/misc/index.ts | 1 + .../components/misc/universal-player.tsx | 57 ++++++ .../components/misc/wavesurfer-player.tsx | 64 ++++++- .../renderer/components/posts/post-audio.tsx | 22 +-- .../components/posts/post-recording.tsx | 166 ++---------------- 5 files changed, 142 insertions(+), 168 deletions(-) create mode 100644 enjoy/src/renderer/components/misc/universal-player.tsx diff --git a/enjoy/src/renderer/components/misc/index.ts b/enjoy/src/renderer/components/misc/index.ts index a53590e2..2c4f547d 100644 --- a/enjoy/src/renderer/components/misc/index.ts +++ b/enjoy/src/renderer/components/misc/index.ts @@ -8,5 +8,6 @@ export * from "./github-login-form"; export * from "./mixin-login-form"; export * from "./no-records-found"; export * from "./page-placeholder"; +export * from "./universal-player"; export * from "./sidebar"; export * from "./wavesurfer-player"; diff --git a/enjoy/src/renderer/components/misc/universal-player.tsx b/enjoy/src/renderer/components/misc/universal-player.tsx new file mode 100644 index 00000000..00bcabd0 --- /dev/null +++ b/enjoy/src/renderer/components/misc/universal-player.tsx @@ -0,0 +1,57 @@ +import { + MediaPlayer, + MediaProvider, + type MediaPlayerInstance, +} from "@vidstack/react"; +import { + DefaultAudioLayout, + defaultLayoutIcons, +} from "@vidstack/react/player/layouts/default"; +import { useEffect, useRef, useState } from "react"; +import { v4 as uuidv4 } from "uuid"; + +export const UniversalPlayer = (props: { + src: string; + onTimeUpdate?: (time: number) => void; + onError?: (error: any) => void; +}) => { + const { src, onTimeUpdate, onError } = props; + const mediaPlayer = useRef(null); + const [uuid] = useState(uuidv4()); + + useEffect(() => { + if (!mediaPlayer.current) return; + + const onOtherPlayerPlay = (event: CustomEvent) => { + if (event.detail.uuid !== uuid) { + mediaPlayer.current!.pause(); + } + }; + + document.addEventListener("play", onOtherPlayerPlay); + + return () => { + document.removeEventListener("play", onOtherPlayerPlay); + }; + }, [uuid, mediaPlayer.current]); + + return ( + { + onTimeUpdate(currentTime); + }} + src={src} + onError={(err) => onError(err)} + onPlay={() => { + const event = new CustomEvent("play", { + detail: { uuid }, + }); + document.dispatchEvent(event); + }} + > + + + + ); +}; diff --git a/enjoy/src/renderer/components/misc/wavesurfer-player.tsx b/enjoy/src/renderer/components/misc/wavesurfer-player.tsx index b9387511..f2070628 100644 --- a/enjoy/src/renderer/components/misc/wavesurfer-player.tsx +++ b/enjoy/src/renderer/components/misc/wavesurfer-player.tsx @@ -1,10 +1,16 @@ -import { renderPitchContour, secondsToTimestamp } from "@renderer/lib/utils"; +import { + cn, + renderPitchContour, + secondsToTimestamp, +} from "@renderer/lib/utils"; import { extractFrequencies } from "@/utils"; import { useIntersectionObserver } from "@uidotdev/usehooks"; import { useCallback, useEffect, useRef, useState } from "react"; import WaveSurfer from "wavesurfer.js"; import { Button, Skeleton } from "@renderer/components/ui"; -import { PauseIcon, PlayIcon } from "lucide-react"; +import { PauseIcon, PlayIcon, XCircleIcon } from "lucide-react"; +import { v4 as uuidv4 } from "uuid"; +import { t } from "i18next"; export const WavesurferPlayer = (props: { id: string; @@ -13,8 +19,20 @@ export const WavesurferPlayer = (props: { currentTime?: number; setCurrentTime?: (currentTime: number) => void; onError?: (error: Error) => void; + wavesurferOptions?: any; + pitchContourOptions?: any; + className?: string; }) => { - const { id, src, height = 80, onError, setCurrentTime } = props; + const { + id, + src, + height = 80, + onError, + setCurrentTime, + wavesurferOptions, + pitchContourOptions, + className = "", + } = props; const [initialized, setInitialized] = useState(false); const [isPlaying, setIsPlaying] = useState(false); const [wavesurfer, setWavesurfer] = useState(null); @@ -23,6 +41,7 @@ export const WavesurferPlayer = (props: { threshold: 1, }); const [duration, setDuration] = useState(0); + const [error, setError] = useState(null); const onPlayClick = useCallback(() => { if (!wavesurfer) return; @@ -50,6 +69,7 @@ export const WavesurferPlayer = (props: { minPxPerSec: 100, waveColor: "#ddd", progressColor: "rgba(0, 0, 0, 0.25)", + ...wavesurferOptions, }); setWavesurfer(ws); @@ -58,9 +78,12 @@ export const WavesurferPlayer = (props: { useEffect(() => { if (!wavesurfer) return; + const uuid = uuidv4(); const subscriptions = [ wavesurfer.on("play", () => { setIsPlaying(true); + const customEvent = new CustomEvent("play", { detail: { uuid } }); + document.dispatchEvent(customEvent); }), wavesurfer.on("pause", () => { setIsPlaying(false); @@ -83,6 +106,7 @@ export const WavesurferPlayer = (props: { data, cubicInterpolationMode: "monotone", pointRadius: 1, + ...pitchContourOptions, }, ], }); @@ -90,16 +114,45 @@ export const WavesurferPlayer = (props: { setInitialized(true); }), wavesurfer.on("error", (err: Error) => { + setError(err.message); onError(err); }), ]; + const onOtherPlayerPlay = (event: CustomEvent) => { + if (!wavesurfer) return; + if (event.detail.uuid === uuid) return; + + wavesurfer.pause(); + }; + + document.addEventListener("play", onOtherPlayerPlay); + return () => { subscriptions.forEach((unsub) => unsub()); wavesurfer?.destroy(); + document.removeEventListener("play", onOtherPlayerPlay); }; }, [wavesurfer]); + if (error) { + return ( +
+
+ +
+
+ {error} +
+
+ +
+
+ ); + } + return ( <>
@@ -110,7 +163,10 @@ export const WavesurferPlayer = (props: {
{!initialized && (
diff --git a/enjoy/src/renderer/components/posts/post-audio.tsx b/enjoy/src/renderer/components/posts/post-audio.tsx index 097e25d3..c54c06ed 100644 --- a/enjoy/src/renderer/components/posts/post-audio.tsx +++ b/enjoy/src/renderer/components/posts/post-audio.tsx @@ -1,16 +1,11 @@ -import { useEffect, useState, useContext } from "react"; +import { useEffect, useState, useContext, useRef } from "react"; import { AppSettingsProviderContext } from "@renderer/context"; import { Button } from "@renderer/components/ui"; -import { MediaPlayer, MediaProvider } from "@vidstack/react"; -import { - DefaultAudioLayout, - defaultLayoutIcons, -} from "@vidstack/react/player/layouts/default"; import { STORAGE_WORKER_ENDPOINTS } from "@/constants"; import { TimelineEntry } from "echogarden/dist/utilities/Timeline.d.js"; import { t } from "i18next"; import { XCircleIcon } from "lucide-react"; -import { WavesurferPlayer } from "../misc"; +import { UniversalPlayer, WavesurferPlayer } from "@renderer/components"; export const PostAudio = (props: { audio: Partial; @@ -75,16 +70,13 @@ export const PostAudio = (props: { onError={(err) => setError(err.message)} /> ) : ( - { - setCurrentTime(_currentTime); - }} + { + setCurrentTime(time); + }} onError={(err) => setError(err.message)} - > - - - + /> )} {currentTranscription && ( diff --git a/enjoy/src/renderer/components/posts/post-recording.tsx b/enjoy/src/renderer/components/posts/post-recording.tsx index 82f9d40b..7f2556aa 100644 --- a/enjoy/src/renderer/components/posts/post-recording.tsx +++ b/enjoy/src/renderer/components/posts/post-recording.tsx @@ -1,13 +1,4 @@ -import { useEffect, useState, useRef, useCallback, useContext } from "react"; -import { renderPitchContour } from "@renderer/lib/utils"; -import { extractFrequencies } from "@/utils"; -import WaveSurfer from "wavesurfer.js"; -import { Button, Skeleton } from "@renderer/components/ui"; -import { PlayIcon, PauseIcon } from "lucide-react"; -import { useIntersectionObserver } from "@uidotdev/usehooks"; -import { secondsToTimestamp } from "@renderer/lib/utils"; -import { t } from "i18next"; -import { XCircleIcon } from "lucide-react"; +import { useEffect, useState, useContext } from "react"; import { AppSettingsProviderContext } from "@renderer/context"; import { WavesurferPlayer } from "@renderer/components"; @@ -17,21 +8,8 @@ export const PostRecording = (props: { }) => { const { webApi } = useContext(AppSettingsProviderContext); const { recording, height = 80 } = props; - const [initialized, setInitialized] = useState(false); - const [isPlaying, setIsPlaying] = useState(false); - const [wavesurfer, setWavesurfer] = useState(null); - const containerRef = useRef(); - const [ref, entry] = useIntersectionObserver({ - threshold: 1, - }); - const [duration, setDuration] = useState(0); - const [error, setError] = useState(null); const [segment, setSegment] = useState(null); - const onPlayClick = useCallback(() => { - wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play(); - }, [wavesurfer]); - const fetchSegment = async () => { if (segment) return; @@ -49,136 +27,26 @@ export const PostRecording = (props: { }; useEffect(() => { - // use the intersection observer to only create the wavesurfer instance - // when the player is visible - if (!entry?.isIntersecting) return; - if (!recording.src) return; - if (wavesurfer) return; - if (error) return; - - const ws = WaveSurfer.create({ - container: containerRef.current, - url: recording.src, - height, - barWidth: 1, - cursorWidth: 0, - autoCenter: true, - autoScroll: true, - dragToSeek: true, - hideScrollbar: true, - minPxPerSec: 100, - waveColor: "rgba(0, 0, 0, 0.25)", - progressColor: "rgba(0, 0, 0, 0.5)", - }); - - setWavesurfer(ws); - fetchSegment(); - - return () => { - setWavesurfer(null); - }; - }, [recording.src, entry, error]); - - useEffect(() => { - if (!wavesurfer) return; - - const subscriptions = [ - wavesurfer.on("play", () => { - setIsPlaying(true); - }), - wavesurfer.on("pause", () => { - setIsPlaying(false); - }), - wavesurfer.on("ready", () => { - setDuration(wavesurfer.getDuration()); - const peaks = wavesurfer.getDecodedData().getChannelData(0); - const sampleRate = wavesurfer.options.sampleRate; - const data = extractFrequencies({ peaks, sampleRate }); - setTimeout(() => { - renderPitchContour({ - wrapper: wavesurfer.getWrapper(), - canvasId: `pitch-contour-${recording.id}-canvas`, - labels: new Array(data.length).fill(""), - datasets: [ - { - data, - cubicInterpolationMode: "monotone", - pointRadius: 1, - borderColor: "#fb6f92", - pointBorderColor: "#fb6f92", - pointBackgroundColor: "#ff8fab", - }, - ], - }); - }, 1000); - setInitialized(true); - }), - wavesurfer.on("error", (err: Error) => { - setError(err.message); - }), - ]; - - return () => { - subscriptions.forEach((unsub) => unsub()); - wavesurfer?.destroy(); - }; - }, [wavesurfer]); - - if (error) { - return ( -
-
- -
-
- {error} -
-
- -
-
- ); - } + }, [recording.src]); return (
-
- - {secondsToTimestamp(duration)} - -
- -
- {!initialized && ( -
- - - -
- )} - -
- -
- -
-
+ {recording.referenceText && (