diff --git a/apps/client/src/assets/sounds/buzzer.mp3 b/apps/client/src/assets/sounds/buzzer.mp3 new file mode 100644 index 0000000000..3c1db38941 Binary files /dev/null and b/apps/client/src/assets/sounds/buzzer.mp3 differ diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 43fb648f0e..b3df9f1556 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -176,6 +176,14 @@ export const useTimer = () => { return useRuntimeStore(featureSelector); }; +export const useTimerPhase = () => { + const featureSelector = (state: RuntimeStore) => ({ + phase: state.timer.phase, + }); + + return useRuntimeStore(featureSelector); +}; + export const useClock = () => { const featureSelector = (state: RuntimeStore) => ({ clock: state.clock, diff --git a/apps/client/src/features/viewers/timer/Timer.tsx b/apps/client/src/features/viewers/timer/Timer.tsx index 44f9b96671..be03b6dd60 100644 --- a/apps/client/src/features/viewers/timer/Timer.tsx +++ b/apps/client/src/features/viewers/timer/Timer.tsx @@ -1,4 +1,6 @@ +import { useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; +import { usePrevious } from '@mantine/hooks'; import { AnimatePresence, motion } from 'framer-motion'; import { CustomFields, @@ -12,12 +14,14 @@ import { ViewSettings, } from 'ontime-types'; +import sound from '../../../assets/sounds/buzzer.mp3'; import { overrideStylesURL } from '../../../common/api/constants'; import { FitText } from '../../../common/components/fit-text/FitText'; import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar'; import TitleCard from '../../../common/components/title-card/TitleCard'; import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; +import { useTimerPhase } from '../../../common/hooks/useSocket'; import { useWindowTitle } from '../../../common/hooks/useWindowTitle'; import { ViewExtendedTimer } from '../../../common/models/TimeManager.type'; import { formatTime, getDefaultFormat } from '../../../common/utils/time'; @@ -59,12 +63,28 @@ interface TimerProps { viewSettings: ViewSettings; } +const usePhaseEvent = () => { + const { phase } = useTimerPhase(); + const previousValue = usePrevious(phase); + + const audio = useMemo(() => new Audio(sound), []); + + if (previousValue !== TimerPhase.None && previousValue !== phase && phase === TimerPhase.Overtime) { + try { + audio.play(); + } catch (error) { + console.error('Audio playback prevented', error); + } + } +}; + export default function Timer(props: TimerProps) { const { auxTimer, customFields, eventNow, eventNext, isMirrored, message, settings, time, viewSettings } = props; const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); const { getLocalizedString } = useTranslation(); const [searchParams] = useSearchParams(); + usePhaseEvent(); useWindowTitle('Timer');