Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions app/components/App/BackgroundNotifier.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import neutralSoundFileURL from '../../assets/sounds/background_statement_neutra
import refuteSoundFileURL from '../../assets/sounds/background_statement_refute.mp3'
import { useFocusedStatement } from '../../contexts/FocusedStatementContext'
import { useUserPreferences } from '../../contexts/UserPreferencesContext'
import { useVideoPlayback } from '../../contexts/VideoPlaybackContext'
import { isStatementConfirmed } from '../../lib/statements_utils'

const confirmAudioFile = new Audio(confirmSoundFileURL)
Expand All @@ -27,6 +28,7 @@ const setFavicon = (value) => {
const BackgroundNotifier = () => {
const { statement } = useFocusedStatement()
const { enableSoundOnBackgroundFocus: soundEnabled } = useUserPreferences()
const { volume } = useVideoPlayback()
const prevFocusedStatementIdRef = useRef(-1)
const prevSoundEnabledRef = useRef(soundEnabled)

Expand Down Expand Up @@ -57,9 +59,17 @@ const BackgroundNotifier = () => {

// Handle sound enable/disable and statement focus changes
useEffect(() => {
const playSound = (audioFile) => {
audioFile.volume = volume
audioFile.currentTime = 0
audioFile.play()?.catch(() => {
// Ignore playback failures (e.g. browser autoplay restrictions)
})
}

Comment thread
sourcery-ai[bot] marked this conversation as resolved.
// Play a sound when enabling setting
if (!prevSoundEnabledRef.current && soundEnabled) {
neutralAudioFile.play()
playSound(neutralAudioFile)
prevSoundEnabledRef.current = soundEnabled
return
}
Expand All @@ -80,18 +90,18 @@ const BackgroundNotifier = () => {
if (soundEnabled) {
const confirmed = isStatementConfirmed(comments)
if (confirmed === null) {
neutralAudioFile.play()
playSound(neutralAudioFile)
} else if (confirmed) {
confirmAudioFile.play()
playSound(confirmAudioFile)
} else {
refuteAudioFile.play()
playSound(refuteAudioFile)
}
}
}

// Always update the ref at the end
prevFocusedStatementIdRef.current = focusedStatementId
}, [focusedStatementId, soundEnabled, comments, setFavicon])
}, [focusedStatementId, soundEnabled, comments, setFavicon, volume])

return null
}
Expand Down
58 changes: 55 additions & 3 deletions app/components/VideoDebate/VideoDebatePlayer.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react'
import React, { useCallback, useEffect, useRef } from 'react'
import ReactPlayer from 'react-player'

import { useVideoPlayback } from '../../contexts/VideoPlaybackContext'
Expand All @@ -8,9 +8,11 @@ import { useVideoPlayback } from '../../contexts/VideoPlaybackContext'
* Updates position when playing and seeks to position when requested.
*/
const VideoDebatePlayer = ({ url }) => {
const { forcedPosition, isPlaying, setPosition, setPlaying } = useVideoPlayback()
const { forcedPosition, isPlaying, setPosition, setPlaying, setVolume } = useVideoPlayback()
const playerRef = useRef(null)
const prevForcedPositionRef = useRef(null)
const internalPlayerRef = useRef(null)
const volumeListenerRef = useRef(null)

useEffect(() => {
if (
Expand All @@ -24,6 +26,52 @@ const VideoDebatePlayer = ({ url }) => {
}
}, [forcedPosition, setPlaying])

// Read the current volume from the internal player and sync it to context.
// Supports both HTML5 <video> (player.volume, 0–1) and YouTube iframe API
// (player.getVolume(), 0–100). Only dispatches when the value changes.
const syncVolume = useCallback(() => {
const player = playerRef.current?.getInternalPlayer()
if (!player) return

let vol
if (typeof player.getVolume === 'function') {
vol = player.isMuted?.() ? 0 : player.getVolume() / 100
} else if (typeof player.volume === 'number') {
vol = player.muted ? 0 : player.volume
} else {
return
}

setVolume(Math.max(0, Math.min(1, vol)))
}, [setVolume])

// Keep a stable ref to the latest syncVolume so the native DOM listener
// always calls the current version without needing to re-register on each
// render (avoids the stale-closure / repeated add-remove cycle).
const syncVolumeRef = useRef(syncVolume)
syncVolumeRef.current = syncVolume

// On player ready: sync initial volume and attach a native volumechange
// listener so that volume changes while the video is paused are captured
// immediately (works for HTML5 video; YouTube provides no equivalent event).
const handleReady = useCallback(() => {
syncVolumeRef.current()
const player = playerRef.current?.getInternalPlayer()
if (player?.addEventListener) {
volumeListenerRef.current = () => syncVolumeRef.current()
player.addEventListener('volumechange', volumeListenerRef.current)
internalPlayerRef.current = player
}
}, [])

useEffect(() => {
return () => {
if (internalPlayerRef.current && volumeListenerRef.current) {
internalPlayerRef.current.removeEventListener('volumechange', volumeListenerRef.current)
}
}
}, [])

return (
<ReactPlayer
ref={playerRef}
Expand All @@ -32,7 +80,11 @@ const VideoDebatePlayer = ({ url }) => {
playing={isPlaying}
onPlay={() => setPlaying(true)}
onPause={() => setPlaying(false)}
onProgress={({ playedSeconds }) => setPosition(playedSeconds)}
onProgress={({ playedSeconds }) => {
setPosition(playedSeconds)
syncVolume()
}}
onReady={handleReady}
width=""
height=""
controls
Expand Down
19 changes: 18 additions & 1 deletion app/contexts/VideoPlaybackContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const initialState = {
position: 0,
isPlaying: false,
forcedPosition: { requestId: null, time: 0 },
volume: 1,
}

const playbackReducer = (state, action) => {
Expand All @@ -23,6 +24,11 @@ const playbackReducer = (state, action) => {
...state,
forcedPosition: { requestId: Date.now(), time: action.payload },
}
case 'SET_VOLUME':
return {
...state,
volume: action.payload,
}
default:
return state
}
Expand Down Expand Up @@ -60,16 +66,27 @@ export const VideoPlaybackProvider = ({ children, onUpdatePosition }) => {
dispatch({ type: 'FORCE_POSITION', payload: time })
}, [])

const setVolume = useCallback(
(volume) => {
if (volume !== state.volume) {
dispatch({ type: 'SET_VOLUME', payload: volume })
}
},
[state.volume],
)

const value = useMemo(
() => ({
position: state.position,
isPlaying: state.isPlaying,
forcedPosition: state.forcedPosition,
volume: state.volume,
setPosition,
setPlaying,
forcePosition,
setVolume,
}),
[state.position, state.isPlaying, state.forcedPosition, setPosition, setPlaying, forcePosition],
[state.position, state.isPlaying, state.forcedPosition, state.volume, setPosition, setPlaying, forcePosition, setVolume],
)

return <VideoPlaybackContext.Provider value={value}>{children}</VideoPlaybackContext.Provider>
Expand Down