@@ -19,6 +19,9 @@ interface VideoViewerProps {
1919 networkQuality ?: NetworkQuality ;
2020 error ?: string | null ;
2121 onReconnect ?: ( ) => void ;
22+ speakerMuted ?: boolean ;
23+ onSpeakerMutedChange ?: ( muted : boolean ) => void ;
24+ showSpeakerToggle ?: boolean ;
2225 className ?: string ;
2326}
2427
@@ -27,11 +30,25 @@ export function VideoViewer({
2730 connectionState,
2831 error,
2932 onReconnect,
33+ speakerMuted,
34+ onSpeakerMutedChange,
35+ showSpeakerToggle = true ,
3036 className = '' ,
3137} : VideoViewerProps ) {
3238 const videoRef = useRef < HTMLVideoElement > ( null ) ;
33- const [ isMuted , setIsMuted ] = useState ( false ) ;
39+ const [ localMuted , setLocalMuted ] = useState ( false ) ;
3440 const [ requiresUnmute , setRequiresUnmute ] = useState ( false ) ;
41+ const isMuted = speakerMuted ?? localMuted ;
42+
43+ const setMutedState = useCallback (
44+ ( muted : boolean ) => {
45+ onSpeakerMutedChange ?.( muted ) ;
46+ if ( speakerMuted === undefined ) {
47+ setLocalMuted ( muted ) ;
48+ }
49+ } ,
50+ [ onSpeakerMutedChange , speakerMuted ]
51+ ) ;
3552
3653 useEffect ( ( ) => {
3754 const video = videoRef . current ;
@@ -89,7 +106,7 @@ export function VideoViewer({
89106 // Unmuted play blocked — retry muted so the video at least renders
90107 console . warn ( '[VideoViewer] Unmuted autoplay blocked, retrying muted' ) ;
91108 video . muted = true ;
92- setIsMuted ( true ) ;
109+ setMutedState ( true ) ;
93110 video . play ( ) . catch ( ( err : unknown ) => {
94111 console . error ( '[VideoViewer] Failed to play even muted:' , err ) ;
95112 } ) ;
@@ -109,18 +126,18 @@ export function VideoViewer({
109126 video . removeEventListener ( 'waiting' , handleWaiting ) ;
110127 video . removeEventListener ( 'error' , handleError ) ;
111128 } ;
112- } , [ stream , isMuted ] ) ;
129+ } , [ stream , isMuted , setMutedState ] ) ;
113130
114131 const toggleMute = useCallback ( ( ) => {
115132 const video = videoRef . current ;
116133 if ( ! video ) return ;
117134
118135 video . muted = ! video . muted ;
119- setIsMuted ( video . muted ) ;
136+ setMutedState ( video . muted ) ;
120137 if ( ! video . muted ) {
121138 setRequiresUnmute ( false ) ;
122139 }
123- } , [ ] ) ;
140+ } , [ setMutedState ] ) ;
124141
125142 const hasVideoTrack = Boolean ( stream && stream . getVideoTracks ( ) . length > 0 ) ;
126143 const hasAudioTrack = Boolean ( stream && stream . getAudioTracks ( ) . length > 0 ) ;
@@ -154,6 +171,7 @@ export function VideoViewer({
154171 . play ( )
155172 . then ( ( ) => {
156173 setRequiresUnmute ( false ) ;
174+ setMutedState ( false ) ;
157175 console . log ( '[VideoViewer] Audio unmuted after user gesture' ) ;
158176 } )
159177 . catch ( ( err : unknown ) => {
@@ -239,7 +257,7 @@ export function VideoViewer({
239257 </ div >
240258
241259 { /* Speaker toggle */ }
242- { ( isStreaming || isVoiceOnly ) && hasAudioTrack && (
260+ { showSpeakerToggle && ( isStreaming || isVoiceOnly ) && hasAudioTrack && (
243261 < button
244262 type = "button"
245263 onClick = { toggleMute }
0 commit comments