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
23 changes: 18 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 @@ -19,6 +20,17 @@ const setFavicon = (value) => {
Tinycon.setBubble(value)
}

const playNotificationSound = (audioFile, volume) => {
const normalizedVolume = Math.min(Math.max(Number.isFinite(volume) ? volume : 1, 0), 1)

if (normalizedVolume === 0) {
return
}

audioFile.volume = normalizedVolume
audioFile.play()
}

/**
* This component watches for various events then triggers sounds or change
* favicon to notify the user that there's something to look at **only**
Expand All @@ -27,6 +39,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 @@ -59,7 +72,7 @@ const BackgroundNotifier = () => {
useEffect(() => {
// Play a sound when enabling setting
if (!prevSoundEnabledRef.current && soundEnabled) {
neutralAudioFile.play()
playNotificationSound(neutralAudioFile, volume)
prevSoundEnabledRef.current = soundEnabled
return
}
Expand All @@ -80,18 +93,18 @@ const BackgroundNotifier = () => {
if (soundEnabled) {
const confirmed = isStatementConfirmed(comments)
if (confirmed === null) {
neutralAudioFile.play()
playNotificationSound(neutralAudioFile, volume)
} else if (confirmed) {
confirmAudioFile.play()
playNotificationSound(confirmAudioFile, volume)
} else {
refuteAudioFile.play()
playNotificationSound(refuteAudioFile, volume)
}
}
}

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

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

import { useVideoPlayback } from '../../contexts/VideoPlaybackContext'

const normalizeVolume = (volume) => {
if (!Number.isFinite(volume)) {
return null
}

return Math.min(Math.max(volume > 1 ? volume / 100 : volume, 0), 1)
}

const getNormalizedPlayerVolume = (player) => {
const internalPlayer = player?.getInternalPlayer?.()
if (!internalPlayer) {
return null
}

if (typeof internalPlayer.volume === 'number') {
return normalizeVolume(internalPlayer.volume)
}

if (typeof internalPlayer.getVolume === 'function') {
return normalizeVolume(internalPlayer.getVolume())
}

return null
}

/**
* A player component with local state for position/playing.
* Updates position when playing and seeks to position when requested.
*/
const VideoDebatePlayer = ({ url }) => {
const { forcedPosition, isPlaying, setPosition, setPlaying } = useVideoPlayback()
const { forcedPosition, isPlaying, setPlaying, setPosition, setVolume } = useVideoPlayback()
const playerRef = useRef(null)
const prevForcedPositionRef = useRef(null)
const cleanupVolumeListenerRef = useRef(null)
const lastSyncedVolumeRef = useRef(null)

const updateVolumeFromPlayer = useCallback(() => {
const volume = getNormalizedPlayerVolume(playerRef.current)
if (volume !== null && volume !== lastSyncedVolumeRef.current) {
lastSyncedVolumeRef.current = volume
setVolume(volume)
}
}, [setVolume])

const attachVolumeListener = useCallback(() => {
cleanupVolumeListenerRef.current?.()

const internalPlayer = playerRef.current?.getInternalPlayer?.()
if (
typeof internalPlayer?.volume !== 'number' ||
typeof internalPlayer?.addEventListener !== 'function' ||
typeof internalPlayer?.removeEventListener !== 'function'
) {
cleanupVolumeListenerRef.current = null
return
}

internalPlayer.addEventListener('volumechange', updateVolumeFromPlayer)
cleanupVolumeListenerRef.current = () => {
internalPlayer.removeEventListener('volumechange', updateVolumeFromPlayer)
}
}, [updateVolumeFromPlayer])

useEffect(() => {
if (
Expand All @@ -24,15 +78,35 @@ const VideoDebatePlayer = ({ url }) => {
}
}, [forcedPosition, setPlaying])

useEffect(() => {
return () => {
cleanupVolumeListenerRef.current?.()
}
}, [])

const handleReady = useCallback(() => {
updateVolumeFromPlayer()
attachVolumeListener()
}, [attachVolumeListener, updateVolumeFromPlayer])

const handleProgress = useCallback(
({ playedSeconds }) => {
setPosition(playedSeconds)
updateVolumeFromPlayer()
},
[setPosition, updateVolumeFromPlayer],
)

return (
<ReactPlayer
ref={playerRef}
className="w-full aspect-video"
url={url}
playing={isPlaying}
onReady={handleReady}
onPlay={() => setPlaying(true)}
onPause={() => setPlaying(false)}
onProgress={({ playedSeconds }) => setPosition(playedSeconds)}
onProgress={handleProgress}
width=""
height=""
controls
Expand Down
24 changes: 23 additions & 1 deletion app/contexts/VideoPlaybackContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { createContext, useCallback, useContext, useMemo, useReducer, use
const initialState = {
position: 0,
isPlaying: false,
volume: 1,
forcedPosition: { requestId: null, time: 0 },
}

Expand All @@ -18,6 +19,11 @@ const playbackReducer = (state, action) => {
...state,
isPlaying: action.payload,
}
case 'SET_VOLUME':
return {
...state,
volume: action.payload,
}
case 'FORCE_POSITION':
return {
...state,
Expand Down Expand Up @@ -56,6 +62,11 @@ export const VideoPlaybackProvider = ({ children, onUpdatePosition }) => {
dispatch({ type: 'SET_PLAYING', payload: isPlaying })
}, [])

const setVolume = useCallback((volume) => {
const normalizedVolume = Math.min(Math.max(volume, 0), 1)
dispatch({ type: 'SET_VOLUME', payload: normalizedVolume })
}, [])

const forcePosition = useCallback((time) => {
dispatch({ type: 'FORCE_POSITION', payload: time })
}, [])
Expand All @@ -64,12 +75,23 @@ export const VideoPlaybackProvider = ({ children, onUpdatePosition }) => {
() => ({
position: state.position,
isPlaying: state.isPlaying,
volume: state.volume,
forcedPosition: state.forcedPosition,
setPosition,
setPlaying,
setVolume,
forcePosition,
}),
[state.position, state.isPlaying, state.forcedPosition, setPosition, setPlaying, forcePosition],
[
state.position,
state.isPlaying,
state.volume,
state.forcedPosition,
setPosition,
setPlaying,
setVolume,
forcePosition,
],
)

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