Add Streamdelay integration

This commit is contained in:
Max Goodhart
2020-06-27 23:44:05 -07:00
parent b958f21a7d
commit d85897f68c
5 changed files with 159 additions and 3 deletions

3
package-lock.json generated
View File

@@ -12167,8 +12167,7 @@
"ws": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz",
"integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==",
"dev": true
"integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w=="
},
"xml-name-validator": {
"version": "3.0.0",

View File

@@ -28,6 +28,7 @@
"reconnecting-websocket": "^4.4.0",
"styled-components": "^5.1.1",
"svg-loaders-react": "^2.2.1",
"ws": "^7.3.0",
"xstate": "^4.10.0",
"yargs": "^15.3.1"
},

View File

@@ -0,0 +1,48 @@
import EventEmitter from 'events'
import * as url from 'url'
import WebSocket from 'ws'
import ReconnectingWebSocket from 'reconnecting-websocket'
export default class StreamdelayClient extends EventEmitter {
constructor({ endpoint, key }) {
super()
this.endpoint = endpoint
this.key = key
this.ws = null
this.status = null
}
connect() {
const wsURL = url.resolve(this.endpoint, `ws?key=${this.key}`)
const ws = (this.ws = new ReconnectingWebSocket(wsURL, [], {
WebSocket,
maxReconnectionDelay: 5000,
minReconnectionDelay: 1000 + Math.random() * 500,
reconnectionDelayGrowFactor: 1.1,
}))
ws.addEventListener('open', () => this.emitState())
ws.addEventListener('close', () => this.emitState())
ws.addEventListener('message', (ev) => {
let data
try {
data = JSON.parse(ev.data)
} catch (err) {
console.error('invalid JSON from streamdelay:', ev.data)
return
}
this.status = data.status
this.emitState()
})
}
emitState() {
this.emit('state', {
isConnected: this.ws.readyState === WebSocket.OPEN,
...this.status,
})
}
setCensored(isCensored) {
this.ws.send(JSON.stringify({ isCensored }))
}
}

View File

@@ -5,6 +5,7 @@ import { app, shell, session, BrowserWindow } from 'electron'
import { ensureValidURL } from '../util'
import { pollPublicData, StreamIDGenerator } from './data'
import StreamWindow from './StreamWindow'
import StreamdelayClient from './StreamdelayClient'
import initWebServer from './server'
async function main() {
@@ -52,6 +53,13 @@ async function main() {
describe: 'Background color of wall (useful for chroma-keying)',
default: '#000',
})
.option('streamdelay-endpoint', {
describe: 'URL of Streamdelay endpoint',
default: 'http://localhost:8404',
})
.option('streamdelay-key', {
describe: 'Streamdelay API key',
})
.help().argv
// Reject all permission requests from web content.
@@ -69,8 +77,14 @@ async function main() {
streamWindow.init()
let browseWindow = null
let streamdelayClient = null
const clientState = { streams: [], customStreams: [], views: [] }
const clientState = {
streams: [],
customStreams: [],
views: [],
streamdelay: false,
}
const getInitialState = () => clientState
let broadcastState = () => {}
const onMessage = (msg) => {
@@ -113,6 +127,8 @@ async function main() {
} else if (msg.type === 'dev-tools') {
streamWindow.openDevTools(msg.viewIdx, browseWindow.webContents)
}
} else if (msg.type === 'set-stream-censored' && streamdelayClient) {
streamdelayClient.setCensored(msg.isCensored)
}
}
@@ -134,6 +150,18 @@ async function main() {
}
}
if (argv.streamdelayKey) {
streamdelayClient = new StreamdelayClient({
endpoint: argv.streamdelayEndpoint,
key: argv.streamdelayKey,
})
streamdelayClient.on('state', (state) => {
clientState.streamdelay = state
broadcastState(clientState)
})
streamdelayClient.connect()
}
streamWindow.on('state', (viewStates) => {
clientState.views = viewStates
streamWindow.send('state', clientState)

View File

@@ -21,6 +21,7 @@ function App({ wsEndpoint }) {
const [streams, setStreams] = useState([])
const [customStreams, setCustomStreams] = useState([])
const [stateIdxMap, setStateIdxMap] = useState(new Map())
const [delayState, setDelayState] = useState(false)
const allStreams = sortBy([...streams, ...customStreams], ['_id'])
useEffect(() => {
@@ -38,6 +39,7 @@ function App({ wsEndpoint }) {
streams: newStreams,
views,
customStreams: newCustomStreams,
streamdelay,
} = msg.state
const newStateIdxMap = new Map()
const allStreams = [...newStreams, ...newCustomStreams]
@@ -66,6 +68,12 @@ function App({ wsEndpoint }) {
setStateIdxMap(newStateIdxMap)
setStreams(newStreams)
setCustomStreams(newCustomStreams)
setDelayState(
streamdelay && {
...streamdelay,
state: State.from(streamdelay.state),
},
)
} else {
console.warn('unexpected ws message', msg)
}
@@ -169,6 +177,15 @@ function App({ wsEndpoint }) {
)
})
const setStreamCensored = useCallback((isCensored) => {
wsRef.current.send(
JSON.stringify({
type: 'set-stream-censored',
isCensored,
}),
)
}, [])
// Set up keyboard shortcuts.
// Note: if GRID_COUNT > 3, there will not be keys for view indices > 9.
for (const idx of range(GRID_COUNT * GRID_COUNT)) {
@@ -189,6 +206,20 @@ function App({ wsEndpoint }) {
[stateIdxMap],
)
}
useHotkeys(
`alt+c`,
() => {
setStreamCensored(true)
},
[setStreamCensored],
)
useHotkeys(
`alt+shift+c`,
() => {
setStreamCensored(false)
},
[setStreamCensored],
)
return (
<div>
@@ -196,6 +227,12 @@ function App({ wsEndpoint }) {
<div>
connection status: {isConnected ? 'connected' : 'connecting...'}
</div>
{delayState !== false && (
<StreamDelayBox
delayState={delayState}
setStreamCensored={setStreamCensored}
/>
)}
<StyledDataContainer isConnected={isConnected}>
<div>
{range(0, 3).map((y) => (
@@ -261,6 +298,38 @@ function App({ wsEndpoint }) {
)
}
function StreamDelayBox({ delayState, setStreamCensored }) {
const handleToggleStreamCensored = useCallback(() => {
setStreamCensored(!delayState.isCensored)
}, [delayState.isCensored, setStreamCensored])
let buttonText
if (delayState.state.matches('censorship.censored.deactivating')) {
buttonText = 'Deactivating...'
} else if (delayState.isCensored) {
buttonText = 'Uncensor stream'
} else {
buttonText = 'Censor stream'
}
return (
<div>
<StyledStreamDelayBox>
<strong>Streamdelay</strong>
<span>{delayState.isConnected ? 'connected' : 'connecting...'}</span>
{delayState.isConnected && (
<span>delay: {delayState.delaySeconds}s</span>
)}
<StyledToggleButton
isActive={delayState.isCensored}
onClick={handleToggleStreamCensored}
tabIndex={1}
>
{buttonText}
</StyledToggleButton>
</StyledStreamDelayBox>
</div>
)
}
function StreamLine({
id,
row: { label, source, title, link, notes },
@@ -423,6 +492,17 @@ function CustomStreamInput({ idx, onChange, ...props }) {
)
}
const StyledStreamDelayBox = styled.div`
display: inline-flex;
margin: 5px 0;
padding: 10px;
background: #fdd;
& > * {
margin-right: 1em;
}
`
const StyledDataContainer = styled.div`
opacity: ${({ isConnected }) => (isConnected ? 1 : 0.5)};
`