mirror of
https://github.com/streamwall/streamwall.git
synced 2026-01-31 09:22:49 -05:00
Add Streamdelay integration
This commit is contained in:
3
package-lock.json
generated
3
package-lock.json
generated
@@ -12167,8 +12167,7 @@
|
|||||||
"ws": {
|
"ws": {
|
||||||
"version": "7.3.0",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz",
|
||||||
"integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==",
|
"integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"xml-name-validator": {
|
"xml-name-validator": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"reconnecting-websocket": "^4.4.0",
|
"reconnecting-websocket": "^4.4.0",
|
||||||
"styled-components": "^5.1.1",
|
"styled-components": "^5.1.1",
|
||||||
"svg-loaders-react": "^2.2.1",
|
"svg-loaders-react": "^2.2.1",
|
||||||
|
"ws": "^7.3.0",
|
||||||
"xstate": "^4.10.0",
|
"xstate": "^4.10.0",
|
||||||
"yargs": "^15.3.1"
|
"yargs": "^15.3.1"
|
||||||
},
|
},
|
||||||
|
|||||||
48
src/node/StreamdelayClient.js
Normal file
48
src/node/StreamdelayClient.js
Normal 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 }))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { app, shell, session, BrowserWindow } from 'electron'
|
|||||||
import { ensureValidURL } from '../util'
|
import { ensureValidURL } from '../util'
|
||||||
import { pollPublicData, StreamIDGenerator } from './data'
|
import { pollPublicData, StreamIDGenerator } from './data'
|
||||||
import StreamWindow from './StreamWindow'
|
import StreamWindow from './StreamWindow'
|
||||||
|
import StreamdelayClient from './StreamdelayClient'
|
||||||
import initWebServer from './server'
|
import initWebServer from './server'
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -52,6 +53,13 @@ async function main() {
|
|||||||
describe: 'Background color of wall (useful for chroma-keying)',
|
describe: 'Background color of wall (useful for chroma-keying)',
|
||||||
default: '#000',
|
default: '#000',
|
||||||
})
|
})
|
||||||
|
.option('streamdelay-endpoint', {
|
||||||
|
describe: 'URL of Streamdelay endpoint',
|
||||||
|
default: 'http://localhost:8404',
|
||||||
|
})
|
||||||
|
.option('streamdelay-key', {
|
||||||
|
describe: 'Streamdelay API key',
|
||||||
|
})
|
||||||
.help().argv
|
.help().argv
|
||||||
|
|
||||||
// Reject all permission requests from web content.
|
// Reject all permission requests from web content.
|
||||||
@@ -69,8 +77,14 @@ async function main() {
|
|||||||
streamWindow.init()
|
streamWindow.init()
|
||||||
|
|
||||||
let browseWindow = null
|
let browseWindow = null
|
||||||
|
let streamdelayClient = null
|
||||||
|
|
||||||
const clientState = { streams: [], customStreams: [], views: [] }
|
const clientState = {
|
||||||
|
streams: [],
|
||||||
|
customStreams: [],
|
||||||
|
views: [],
|
||||||
|
streamdelay: false,
|
||||||
|
}
|
||||||
const getInitialState = () => clientState
|
const getInitialState = () => clientState
|
||||||
let broadcastState = () => {}
|
let broadcastState = () => {}
|
||||||
const onMessage = (msg) => {
|
const onMessage = (msg) => {
|
||||||
@@ -113,6 +127,8 @@ async function main() {
|
|||||||
} else if (msg.type === 'dev-tools') {
|
} else if (msg.type === 'dev-tools') {
|
||||||
streamWindow.openDevTools(msg.viewIdx, browseWindow.webContents)
|
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) => {
|
streamWindow.on('state', (viewStates) => {
|
||||||
clientState.views = viewStates
|
clientState.views = viewStates
|
||||||
streamWindow.send('state', clientState)
|
streamWindow.send('state', clientState)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ function App({ wsEndpoint }) {
|
|||||||
const [streams, setStreams] = useState([])
|
const [streams, setStreams] = useState([])
|
||||||
const [customStreams, setCustomStreams] = useState([])
|
const [customStreams, setCustomStreams] = useState([])
|
||||||
const [stateIdxMap, setStateIdxMap] = useState(new Map())
|
const [stateIdxMap, setStateIdxMap] = useState(new Map())
|
||||||
|
const [delayState, setDelayState] = useState(false)
|
||||||
const allStreams = sortBy([...streams, ...customStreams], ['_id'])
|
const allStreams = sortBy([...streams, ...customStreams], ['_id'])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -38,6 +39,7 @@ function App({ wsEndpoint }) {
|
|||||||
streams: newStreams,
|
streams: newStreams,
|
||||||
views,
|
views,
|
||||||
customStreams: newCustomStreams,
|
customStreams: newCustomStreams,
|
||||||
|
streamdelay,
|
||||||
} = msg.state
|
} = msg.state
|
||||||
const newStateIdxMap = new Map()
|
const newStateIdxMap = new Map()
|
||||||
const allStreams = [...newStreams, ...newCustomStreams]
|
const allStreams = [...newStreams, ...newCustomStreams]
|
||||||
@@ -66,6 +68,12 @@ function App({ wsEndpoint }) {
|
|||||||
setStateIdxMap(newStateIdxMap)
|
setStateIdxMap(newStateIdxMap)
|
||||||
setStreams(newStreams)
|
setStreams(newStreams)
|
||||||
setCustomStreams(newCustomStreams)
|
setCustomStreams(newCustomStreams)
|
||||||
|
setDelayState(
|
||||||
|
streamdelay && {
|
||||||
|
...streamdelay,
|
||||||
|
state: State.from(streamdelay.state),
|
||||||
|
},
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
console.warn('unexpected ws message', msg)
|
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.
|
// Set up keyboard shortcuts.
|
||||||
// Note: if GRID_COUNT > 3, there will not be keys for view indices > 9.
|
// Note: if GRID_COUNT > 3, there will not be keys for view indices > 9.
|
||||||
for (const idx of range(GRID_COUNT * GRID_COUNT)) {
|
for (const idx of range(GRID_COUNT * GRID_COUNT)) {
|
||||||
@@ -189,6 +206,20 @@ function App({ wsEndpoint }) {
|
|||||||
[stateIdxMap],
|
[stateIdxMap],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
useHotkeys(
|
||||||
|
`alt+c`,
|
||||||
|
() => {
|
||||||
|
setStreamCensored(true)
|
||||||
|
},
|
||||||
|
[setStreamCensored],
|
||||||
|
)
|
||||||
|
useHotkeys(
|
||||||
|
`alt+shift+c`,
|
||||||
|
() => {
|
||||||
|
setStreamCensored(false)
|
||||||
|
},
|
||||||
|
[setStreamCensored],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -196,6 +227,12 @@ function App({ wsEndpoint }) {
|
|||||||
<div>
|
<div>
|
||||||
connection status: {isConnected ? 'connected' : 'connecting...'}
|
connection status: {isConnected ? 'connected' : 'connecting...'}
|
||||||
</div>
|
</div>
|
||||||
|
{delayState !== false && (
|
||||||
|
<StreamDelayBox
|
||||||
|
delayState={delayState}
|
||||||
|
setStreamCensored={setStreamCensored}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<StyledDataContainer isConnected={isConnected}>
|
<StyledDataContainer isConnected={isConnected}>
|
||||||
<div>
|
<div>
|
||||||
{range(0, 3).map((y) => (
|
{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({
|
function StreamLine({
|
||||||
id,
|
id,
|
||||||
row: { label, source, title, link, notes },
|
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`
|
const StyledDataContainer = styled.div`
|
||||||
opacity: ${({ isConnected }) => (isConnected ? 1 : 0.5)};
|
opacity: ${({ isConnected }) => (isConnected ? 1 : 0.5)};
|
||||||
`
|
`
|
||||||
|
|||||||
Reference in New Issue
Block a user