diff --git a/backend/db.js b/backend/db.js index 43a71a2..4eaf108 100644 --- a/backend/db.js +++ b/backend/db.js @@ -1,4 +1,4 @@ -var { callModel: Call } = require("./models/call"); +var Call = require("./models/call"); var Event = require("./models/event"); var Podcast = require("./models/podcast"); diff --git a/frontend/src/Call/components/MediaPlayer.js b/frontend/src/Call/components/MediaPlayer.js index 681d938..912aada 100644 --- a/frontend/src/Call/components/MediaPlayer.js +++ b/frontend/src/Call/components/MediaPlayer.js @@ -11,6 +11,7 @@ import { Button } from "semantic-ui-react"; import ReactAudioPlayer from 'react-audio-player' +import { useWavesurfer } from './WaveSurfer' //'@wavesurfer/react' import WavesurferPlayer from '@wavesurfer/react' import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.js'; @@ -22,21 +23,55 @@ import "./MediaPlayer.css"; const MediaPlayer = (props) => { + const containerRef = useRef(null) const audioRef = React.createRef(); const call = props.call; const [sourceIndex, setSourceIndex] = useState(0); - const [wavesurfer, setWavesurfer] = useState(null) - const [isPlaying, setIsPlaying] = useState(false) + //const [wavesurfer, setWavesurfer] = useState(null) + //const [isPlaying, setIsPlaying] = useState(false) const [playTime, setPlayTime] = useState(0); const playSilence = props.playSilence; const parentHandlePlayPause = props.onPlayPause const regionsPlugin = useMemo(() => RegionsPlugin.create(), []); const plugins = useMemo(() => [regionsPlugin], [regionsPlugin]); + // + const { wavesurfer, isReady, isPlaying, currentTime, hasFinished } = useWavesurfer({ + container: containerRef, + waveColor: "#E81B39", + height: 25, + barWidth: 3, + barGap: 3, + barRadius: 6, + plugins: plugins, + backend: 'MediaElementWebAudio', + responsive: true, + //autoplay: true, + }) + + // useEffect(() => { + // if (wavesurfer) { + // wavesurfer.on('finish', props.onEnded); + // } + // }, [wavesurfer]) useEffect(() => { setSourceIndex(0); - + if ('mediaSession' in navigator) { navigator.mediaSession.metadata = new MediaMetadata({ title: "Waiting for Call...", @@ -68,53 +103,50 @@ const MediaPlayer = (props) => { }, [playSilence]); useEffect(() => { + if (hasFinished) { + props.onEnded(); + } + }, [hasFinished]); + + useEffect(() => { + if (call && wavesurfer) { + wavesurfer.load(call.url); + } setSourceIndex(0); }, [call]); + useEffect(() => { - const onReady = (ws) => { - setWavesurfer(ws) - setIsPlaying(false) - regionsPlugin.clearRegions(); - if (call) { - call.srcList.forEach(src => { - regionsPlugin.addRegion({ - start: src.pos, - color: "rgba(128, 128, 128, 1.0)", - drag: false, - resize: false - }); - }); - } - } - - const onPlay = () => { - setIsPlaying(true); - parentHandlePlayPause(true); - - } - - const onPause = () => { - setIsPlaying(false); - parentHandlePlayPause(false); - } - const onPlayPause = () => { - wavesurfer && wavesurfer.playPause() - } - - const updatePlayProgress = () => { - - if (wavesurfer && wavesurfer.isPlaying()) { - var totalTime = wavesurfer.getDuration(), - currentTime = wavesurfer.getCurrentTime(), - remainingTime = totalTime - currentTime; - - if (!isPlaying) { - setIsPlaying(true); + if (isReady && wavesurfer) { + wavesurfer.play() + //regionsPlugin.clearRegions(); + if (call) { + // call.srcList.forEach(src => { + // regionsPlugin.addRegion({ + // start: src.pos, + // color: "rgba(128, 128, 128, 1.0)", + // drag: false, + // resize: false + // }); + // }); } - //console.log("totalTime: " + totalTime + " currentTime: " + currentTime + " remainingTime: " + remainingTime); + } + }, [isReady]); + + useEffect(() => { + if (isPlaying) { + parentHandlePlayPause(true); + } else { + parentHandlePlayPause(false); + } + }, [isPlaying]); + + useEffect(() => { + if (wavesurfer && wavesurfer.isPlaying()) { + //console.log("totalTime: " + totalTime + " currentTime: " + currentTime + " remainingTime: " + remainingTime); + // this checks to see if it should display the next Source ID if (call && ((call.srcList.length - 1) >= (sourceIndex + 1)) && (currentTime > call.srcList[sourceIndex + 1].pos)) { setSourceIndex(sourceIndex + 1); @@ -123,6 +155,10 @@ const MediaPlayer = (props) => { setPlayTime(Math.floor(currentTime)); } + }, [currentTime]); + + const onPlayPause = () => { + wavesurfer && wavesurfer.playPause() } let playEnabled = { "disabled": true } @@ -143,29 +179,15 @@ const MediaPlayer = (props) => {
- { - isPlaying - ? () - : () - } + { + isPlaying + ? () + : () + }
+
-
diff --git a/frontend/src/Call/components/WaveSurfer.js b/frontend/src/Call/components/WaveSurfer.js new file mode 100644 index 0000000..4232c5d --- /dev/null +++ b/frontend/src/Call/components/WaveSurfer.js @@ -0,0 +1,200 @@ +/** + * A React component for wavesurfer.js + * + * Usage: + * + * import WavesurferPlayer from '@wavesurfer/react' + * + * console.log('Ready!', wavesurfer)} + * /> + */ + +import { set } from 'date-fns' +import { useState, useMemo, useEffect, useRef, memo } from 'react' +import WaveSurfer from 'wavesurfer.js' + +/** + * Use wavesurfer instance + */ +function useWavesurferInstance(containerRef, options) { + const [wavesurfer, setWavesurfer] = useState(null) + // Flatten options object to an array of keys and values to compare them deeply in the hook deps + const flatOptions = useMemo(() => Object.entries(options).flat(), [options]) + + // Create a wavesurfer instance + useEffect(() => { + if (!containerRef.current) return + + const ws = WaveSurfer.create({ + ...options, + container: containerRef.current, + }) + + setWavesurfer(ws) + + return () => { + ws.destroy() + } + }, [containerRef, ...flatOptions]) + + return wavesurfer +} + +/** + * Use wavesurfer state + */ +function useWavesurferState(wavesurfer) { + const [isReady, setIsReady] = useState(false) + const [isPlaying, setIsPlaying] = useState(false) + const [currentTime, setCurrentTime] = useState(0) + const [hasFinished, setHasFinished] = useState(false) + + useEffect(() => { + if (!wavesurfer) return + + const unsubscribeFns = [ + wavesurfer.on('load', () => { + setIsReady(false) + setIsPlaying(false) + setCurrentTime(0) + }), + + wavesurfer.on('ready', () => { + setIsReady(true) + setIsPlaying(false) + setHasFinished(false) + setCurrentTime(0) + }), + + wavesurfer.on('finish', () => { + setHasFinished(true) + }), + + wavesurfer.on('play', () => { + setIsPlaying(true) + }), + + wavesurfer.on('pause', () => { + setIsPlaying(false) + }), + + wavesurfer.on('timeupdate', () => { + setCurrentTime(wavesurfer.getCurrentTime()) + }), + + wavesurfer.on('destroy', () => { + setIsReady(false) + setIsPlaying(false) + }), + ] + + return () => { + unsubscribeFns.forEach((fn) => fn()) + } + }, [wavesurfer]) + + return useMemo( + () => ({ + isReady, + isPlaying, + currentTime, + hasFinished + }), + [isPlaying, currentTime, isReady, hasFinished], + ) +} + +const EVENT_PROP_RE = /^on([A-Z])/ +const isEventProp = (key) => EVENT_PROP_RE.test(key) +const getEventName = (key) => key.replace(EVENT_PROP_RE, (_, $1) => $1.toLowerCase()) + +/** + * Parse props into wavesurfer options and events + */ +function useWavesurferProps(props) { + // Props starting with `on` are wavesurfer events, e.g. `onReady` + // The rest of the props are wavesurfer options + return useMemo(() => { + const allOptions = { ...props } + const allEvents = { ...props } + + for (const key in allOptions) { + if (isEventProp(key)) { + delete allOptions[key] + } else { + delete allEvents[key] + } + } + return [allOptions, allEvents] + }, [props]) +} + +/** + * Subscribe to wavesurfer events + */ +function useWavesurferEvents(wavesurfer, events) { + const flatEvents = useMemo(() => Object.entries(events).flat(), [events]) + + // Subscribe to events + useEffect(() => { + if (!wavesurfer) return + + const eventEntries = Object.entries(events) + if (!eventEntries.length) return + + const unsubscribeFns = eventEntries.map(([name, handler]) => { + const event = getEventName(name) + return wavesurfer.on(event, (...args) => handler(wavesurfer, ...args)) + }) + + return () => { + unsubscribeFns.forEach((fn) => fn()) + } + }, [wavesurfer, ...flatEvents]) +} + +/** + * Wavesurfer player component + * @see https://wavesurfer.xyz/docs/modules/wavesurfer + */ +const WavesurferPlayer = memo((props) => { + const containerRef = useRef(null) + const [options, events] = useWavesurferProps(props) + const wavesurfer = useWavesurferInstance(containerRef, options) + useWavesurferEvents(wavesurfer, events) + + // Create a container div + return
+}) + +export default WavesurferPlayer + +/** + * React hook for wavesurfer.js + * + * ``` + * import { useWavesurfer } from '@wavesurfer/react' + * + * const App = () => { + * const containerRef = useRef(null) + * + * const { wavesurfer, isReady, isPlaying, currentTime } = useWavesurfer({ + * container: containerRef, + * url: '/my-server/audio.ogg', + * waveColor: 'purple', + * height: 100', + * }) + * + * return
+ * } + * ``` + */ +export function useWavesurfer({ container, ...options }) { + const wavesurfer = useWavesurferInstance(container, options) + const state = useWavesurferState(wavesurfer) + return useMemo(() => ({ ...state, wavesurfer }), [state, wavesurfer]) +} \ No newline at end of file