diff --git a/src/browser/overlay.js b/src/browser/overlay.js index 9f4d5dd..be17828 100644 --- a/src/browser/overlay.js +++ b/src/browser/overlay.js @@ -29,6 +29,9 @@ function Overlay({ config, views, streams }) { const isListening = viewState.matches( 'displaying.running.audio.listening', ) + const isBackgroundListening = viewState.matches( + 'displaying.running.audio.background', + ) const isBlurred = viewState.matches('displaying.running.video.blurred') const isLoading = viewState.matches('displaying.loading') return ( @@ -51,7 +54,7 @@ function Overlay({ config, views, streams }) { )} - {isListening && } + {(isListening || isBackgroundListening) && } )} {isLoading && } diff --git a/src/node/StreamWindow.js b/src/node/StreamWindow.js index 7b6a3f6..8beccce 100644 --- a/src/node/StreamWindow.js +++ b/src/node/StreamWindow.js @@ -257,6 +257,10 @@ export default class StreamWindow extends EventEmitter { } } + setViewBackgroundListening(viewIdx, listening) { + this.sendViewEvent(viewIdx, listening ? 'BACKGROUND' : 'UNBACKGROUND') + } + setViewBlurred(viewIdx, blurred) { this.sendViewEvent(viewIdx, blurred ? 'BLUR' : 'UNBLUR') } diff --git a/src/node/index.js b/src/node/index.js index 1e9ad66..191b2cc 100644 --- a/src/node/index.js +++ b/src/node/index.js @@ -270,6 +270,8 @@ async function main() { const onMessage = async (msg, respond) => { if (msg.type === 'set-listening-view') { streamWindow.setListeningView(msg.viewIdx) + } else if (msg.type === 'set-view-background-listening') { + streamWindow.setViewBackgroundListening(msg.viewIdx, msg.listening) } else if (msg.type === 'set-view-blurred') { streamWindow.setViewBlurred(msg.viewIdx, msg.blurred) } else if (msg.type === 'set-custom-streams') { diff --git a/src/node/viewStateMachine.js b/src/node/viewStateMachine.js index 0ff2731..5caddf5 100644 --- a/src/node/viewStateMachine.js +++ b/src/node/viewStateMachine.js @@ -141,6 +141,8 @@ const viewStateMachine = Machine( on: { MUTE: '.muted', UNMUTE: '.listening', + BACKGROUND: '.background', + UNBACKGROUND: '.muted', }, states: { muted: { @@ -149,6 +151,13 @@ const viewStateMachine = Machine( listening: { entry: 'unmuteAudio', }, + background: { + on: { + // Ignore normal audio swapping. + MUTE: {}, + }, + entry: 'unmuteAudio', + }, }, }, video: { diff --git a/src/roles.js b/src/roles.js index 1c5567e..5651d14 100644 --- a/src/roles.js +++ b/src/roles.js @@ -1,5 +1,6 @@ const operatorActions = new Set([ 'set-listening-view', + 'set-view-background-listening', 'set-view-blurred', 'set-custom-streams', 'reload-view', diff --git a/src/web/control.js b/src/web/control.js index 5e3a5ec..95a7f6d 100644 --- a/src/web/control.js +++ b/src/web/control.js @@ -15,6 +15,7 @@ import { import { State } from 'xstate' import styled, { createGlobalStyle } from 'styled-components' import { useHotkeys } from 'react-hotkeys-hook' +import Color from 'color' import '../index.css' import { idxInBox } from '../geometry' @@ -139,6 +140,9 @@ function useStreamwallConnection(wsEndpoint) { const isListening = state.matches( 'displaying.running.audio.listening', ) + const isBackgroundListening = state.matches( + 'displaying.running.audio.background', + ) const isBlurred = state.matches('displaying.running.video.blurred') for (const space of pos.spaces) { if (!newStateIdxMap.has(space)) { @@ -147,6 +151,7 @@ function useStreamwallConnection(wsEndpoint) { Object.assign(newStateIdxMap.get(space), { state, isListening, + isBackgroundListening, isBlurred, spaces: pos.spaces, }) @@ -332,6 +337,14 @@ function App({ wsEndpoint, role }) { }) }, []) + const handleSetBackgroundListening = useCallback((viewIdx, listening) => { + send({ + type: 'set-view-background-listening', + viewIdx, + listening, + }) + }, []) + const handleSetBlurred = useCallback((idx, blurred) => { send({ type: 'set-view-blurred', @@ -504,8 +517,12 @@ function App({ wsEndpoint, role }) { {range(0, gridCount).map((x) => { const idx = gridCount * y + x - const { isListening = false, isBlurred = false, state } = - stateIdxMap.get(idx) || {} + const { + isListening = false, + isBackgroundListening = false, + isBlurred = false, + state, + } = stateIdxMap.get(idx) || {} const { streamId } = sharedState.views?.[idx] || '' const isDragHighlighted = dragStart !== undefined && @@ -517,6 +534,7 @@ function App({ wsEndpoint, role }) { isError={state && state.matches('displaying.error')} isDisplaying={state && state.matches('displaying')} isListening={isListening} + isBackgroundListening={isBackgroundListening} isBlurred={isBlurred} isHighlighted={isDragHighlighted} isSwapping={idx === swapStartIdx} @@ -528,6 +546,7 @@ function App({ wsEndpoint, role }) { onBlur={handleBlurInput} onChangeSpace={handleSetView} onSetListening={handleSetListening} + onSetBackgroundListening={handleSetBackgroundListening} onSetBlurred={handleSetBlurred} onReloadView={handleReloadView} onSwapView={handleSwapView} @@ -722,6 +741,7 @@ function GridInput({ isDisplaying, isError, isListening, + isBackgroundListening, isBlurred, isHighlighted, isSwapping, @@ -732,6 +752,7 @@ function GridInput({ onFocus, onBlur, onSetListening, + onSetBackgroundListening, onSetBlurred, onReloadView, onSwapView, @@ -762,8 +783,17 @@ function GridInput({ [idx, onChangeSpace], ) const handleListeningClick = useCallback( - () => onSetListening(idx, !isListening), - [idx, onSetListening, isListening], + (ev) => + ev.shiftKey || isBackgroundListening + ? onSetBackgroundListening(idx, !isBackgroundListening) + : onSetListening(idx, !isListening), + [ + idx, + onSetListening, + onSetBackgroundListening, + isListening, + isBackgroundListening, + ], ) const handleBlurClick = useCallback(() => onSetBlurred(idx, !isBlurred), [ idx, @@ -839,7 +869,8 @@ function GridInput({ )} {roleCan(role, 'set-listening-view') && ( @@ -931,11 +962,11 @@ const StyledButton = styled.button` background: #ccc; border-radius: 5px; - ${({ isActive }) => + ${({ isActive, activeColor }) => isActive && ` - border-color: red; - background: #c77; + border-color: ${activeColor}; + background: ${Color(activeColor).desaturate(0.5).lighten(0.5)}; `}; &:focus {