diff --git a/src/renderer/src/components/HiddenPlayer.tsx b/src/renderer/src/components/HiddenPlayer.tsx index 61fa3e67..4fb9d73b 100644 --- a/src/renderer/src/components/HiddenPlayer.tsx +++ b/src/renderer/src/components/HiddenPlayer.tsx @@ -1,7 +1,7 @@ import { useState, useRef } from 'react'; import { IRegion } from '../crud/useWavesurferRegions'; -import WSAudioPlayer from './WSAudioPlayer'; +import WSAudioPlayer, { WSAudioPlayerControls } from './WSAudioPlayer'; import { MediaFile } from '../model'; import { NamedRegions } from '../utils/namedSegments'; import { RequestPlay, usePlayerLogic } from '../business/player/usePlayerLogic'; @@ -20,6 +20,7 @@ export interface HiddenPlayerProps { currentSegment?: IRegion; setCurrentSegment?: (segment: IRegion | undefined, index: number) => void; playerMediafile?: MediaFile; + controlsRef?: React.RefObject; } export function HiddenPlayer(props: HiddenPlayerProps) { @@ -36,6 +37,7 @@ export function HiddenPlayer(props: HiddenPlayerProps) { currentSegmentIndex, setCurrentSegment, playerMediafile, + controlsRef, } = props; const [requestPlay, setRequestPlay] = useState({ @@ -88,6 +90,7 @@ export function HiddenPlayer(props: HiddenPlayerProps) { onCurrentSegment={onCurrentSegment} onProgress={onProgress} onDuration={onDuration} + controlsRef={controlsRef} /> ); diff --git a/src/renderer/src/components/MediaPlayer.tsx b/src/renderer/src/components/MediaPlayer.tsx index 0ede3689..032db8f9 100644 --- a/src/renderer/src/components/MediaPlayer.tsx +++ b/src/renderer/src/components/MediaPlayer.tsx @@ -25,6 +25,7 @@ import SkipPrevious from '@mui/icons-material/SkipPrevious'; import Pause from '@mui/icons-material/Pause'; import PlayArrow from '@mui/icons-material/PlayArrow'; import HiddenPlayer from './HiddenPlayer'; +import { WSAudioPlayerControls } from './WSAudioPlayer'; import { RecordKeyMap } from '@orbit/records'; const StyledTip = styled(LightTooltip)(() => ({ @@ -74,6 +75,7 @@ export function MediaPlayer(props: IProps) { const { fetchMediaUrl, mediaState } = useFetchMediaUrl(reporter); const [blobState, fetchBlob] = useFetchMediaBlob(); const audioRef = useRef(null); + const wsControlsRef = useRef(null); const playSuccess = useRef(false); const [playing, setPlayingx] = useState(false); const playingRef = useRef(false); @@ -318,7 +320,15 @@ export function MediaPlayer(props: IProps) { const handlePlayPause = () => { if (onTogglePlay) onTogglePlay(); - if (playingRef.current) stopPlay(); + // iOS Safari requires AudioContext resume to happen synchronously inside + // the user gesture. Driving playback through React state introduces async + // hops (incl. a 100ms setTimeout in usePlayerLogic) that lose that context, + // so audio toggles silently. Calling wavesurfer directly here keeps us in + // the gesture; handlePlayStatus is idempotent so the state chain catching + // up later is a no-op. + const wasPlaying = playingRef.current; + wsControlsRef.current?.togglePlay(); + if (wasPlaying) stopPlay(); else startPlayWaveSurfer(); }; @@ -428,6 +438,7 @@ export function MediaPlayer(props: IProps) { audioBlob={blobState.blob} playing={playing} setPlaying={setPlaying} + controlsRef={wsControlsRef} /> )} diff --git a/src/renderer/src/components/MediaRecord.tsx b/src/renderer/src/components/MediaRecord.tsx index bec69181..3ba08a94 100644 --- a/src/renderer/src/components/MediaRecord.tsx +++ b/src/renderer/src/components/MediaRecord.tsx @@ -10,7 +10,14 @@ import { useGlobal } from '../context/useGlobal'; import { IPassageRecordStrings, ISharedStrings } from '../model'; import { Stack, Paper, Typography } from '@mui/material'; import WSAudioPlayer, { WSAudioPlayerControls } from './WSAudioPlayer'; -import { loadBlobAsync, useMobile, waitForIt } from '../utils'; +import { + infoMsg, + loadBlobAsync, + logError, + Severity, + useMobile, + waitForIt, +} from '../utils'; import { IMediaState, MediaSt, @@ -535,20 +542,18 @@ function MediaRecord(props: IProps) { setLoading(true); reset(); - const url = await getGoodUrl(); - - if (url) { - try { - const blob = await loadBlobAsync(url); - if (blob) gotTheBlob(blob); - else blobError('Failed to load blob'); - } catch (error) { - blobError( - error instanceof Error ? error.message : 'Failed to load blob' - ); + try { + const url = await getGoodUrl(); + if (!url) { + blobError(mediaStateRef.current.error || ts.mediaError); + return; } - } else { - blobError(mediaStateRef.current.error || 'Failed to fetch media URL'); + const blob = await loadBlobAsync(url); + if (blob) gotTheBlob(blob); + else blobError(ts.mediaError); + } catch (error) { + logError(Severity.error, reporter, infoMsg(error as Error, 'media load failed')); + blobError(ts.mediaError); } }; diff --git a/src/renderer/src/components/WSAudioPlayer.tsx b/src/renderer/src/components/WSAudioPlayer.tsx index f8466513..4632b719 100644 --- a/src/renderer/src/components/WSAudioPlayer.tsx +++ b/src/renderer/src/components/WSAudioPlayer.tsx @@ -968,6 +968,11 @@ function WSAudioPlayer(props: IProps) { const handlePlayStatus = useCallback( (play: boolean) => { if (durationRef.current === 0 || recordingRef.current) return false; + const wouldReplayRegion = + play && (regionOnly || forceRegionOnly) && !!currentSegmentRef.current; + if (play === playingRef.current && !wouldReplayRegion) { + return playingRef.current; + } let nowplaying = play; if (