Add support for TOML file data source

This commit is contained in:
Max Goodhart
2020-07-01 00:03:08 -07:00
parent cc14f2a17f
commit b2215aa42d
7 changed files with 135 additions and 64 deletions

View File

@@ -45,6 +45,14 @@ npm start -- --config="../streamwall.json"
See `config.example.toml` for an example. See `config.example.toml` for an example.
## Data sources
Streamwall can load stream data from both JSON APIs and TOML files. Data sources can be specified in a config file (see `config.example.toml` for an example) or the command line:
```
npm start -- --json-url="https://your-site/api/streams.json" --toml-file="./streams.toml"
```
## Hotkeys ## Hotkeys
The following hotkeys are available with the "control" webpage focused: The following hotkeys are available with the "control" webpage focused:

View File

@@ -13,6 +13,15 @@ address = "http://localhost:80"
password = "woke" password = "woke"
username = "woke" username = "woke"
[data]
# By default, we use the woke.net streams data source.
# You can add other URLs to fetch here:
json-url = ["https://woke.net/api/streams.json"]
# You can also specify .toml files on disk. Streamwall will reload the data whenever the file changes.
# See example.streams.toml for a sample.
#toml-file = ["./example.streams.toml"]
[cert] [cert]
# SSL certificates (optional) # SSL certificates (optional)
# If you specify an https:// URL for the "webserver" option, a certificate will be automatically generated and signed by Let's Encrypt. # If you specify an https:// URL for the "webserver" option, a certificate will be automatically generated and signed by Let's Encrypt.

42
package-lock.json generated
View File

@@ -1867,6 +1867,11 @@
} }
} }
}, },
"@repeaterjs/repeater": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.1.tgz",
"integrity": "sha512-kqdrZRvTLFfvgPTeMgj8DpREYZXU+sz4/kdX2825zMNGmiaEUzM6u7A4YBoI4WzqXX4q123VftkllAw1C/4rMg=="
},
"@sindresorhus/is": { "@sindresorhus/is": {
"version": "0.14.0", "version": "0.14.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@@ -2509,7 +2514,6 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
"integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
"dev": true,
"requires": { "requires": {
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"picomatch": "^2.0.4" "picomatch": "^2.0.4"
@@ -2956,11 +2960,9 @@
"dev": true "dev": true
}, },
"binary-extensions": { "binary-extensions": {
"version": "2.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
"integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ=="
"dev": true,
"optional": true
}, },
"bluebird": { "bluebird": {
"version": "3.7.2", "version": "3.7.2",
@@ -3339,8 +3341,6 @@
"version": "3.4.0", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz",
"integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==", "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==",
"dev": true,
"optional": true,
"requires": { "requires": {
"anymatch": "~3.1.1", "anymatch": "~3.1.1",
"braces": "~3.0.2", "braces": "~3.0.2",
@@ -3356,8 +3356,6 @@
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"optional": true,
"requires": { "requires": {
"fill-range": "^7.0.1" "fill-range": "^7.0.1"
} }
@@ -3366,8 +3364,6 @@
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"optional": true,
"requires": { "requires": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
} }
@@ -3375,16 +3371,12 @@
"is-number": { "is-number": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
"dev": true,
"optional": true
}, },
"to-regex-range": { "to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"optional": true,
"requires": { "requires": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
} }
@@ -5331,7 +5323,6 @@
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
"dev": true,
"optional": true "optional": true
}, },
"function-bind": { "function-bind": {
@@ -5412,7 +5403,6 @@
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
"dev": true,
"requires": { "requires": {
"is-glob": "^4.0.1" "is-glob": "^4.0.1"
} }
@@ -5912,8 +5902,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"optional": true,
"requires": { "requires": {
"binary-extensions": "^2.0.0" "binary-extensions": "^2.0.0"
} }
@@ -5998,8 +5986,7 @@
"is-extglob": { "is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
"dev": true
}, },
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "3.0.0", "version": "3.0.0",
@@ -6021,7 +6008,6 @@
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"dev": true,
"requires": { "requires": {
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
} }
@@ -8972,8 +8958,7 @@
"normalize-path": { "normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
"dev": true
}, },
"normalize-url": { "normalize-url": {
"version": "4.5.0", "version": "4.5.0",
@@ -9418,8 +9403,7 @@
"picomatch": { "picomatch": {
"version": "2.2.2", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
"dev": true
}, },
"pify": { "pify": {
"version": "3.0.0", "version": "3.0.0",
@@ -9934,8 +9918,6 @@
"version": "3.4.0", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
"integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
"dev": true,
"optional": true,
"requires": { "requires": {
"picomatch": "^2.2.1" "picomatch": "^2.2.1"
} }

View File

@@ -13,6 +13,8 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@repeaterjs/repeater": "^3.0.1",
"chokidar": "^3.4.0",
"ejs": "^3.1.3", "ejs": "^3.1.3",
"electron": "^9.0.4", "electron": "^9.0.4",
"koa": "^2.12.1", "koa": "^2.12.1",

View File

@@ -1,5 +1,10 @@
import { once } from 'events'
import { promises as fsPromises } from 'fs'
import { promisify } from 'util' import { promisify } from 'util'
import { Repeater } from '@repeaterjs/repeater'
import TOML from '@iarna/toml'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import chokidar from 'chokidar'
const sleep = promisify(setTimeout) const sleep = promisify(setTimeout)
@@ -7,14 +12,13 @@ function filterLive(data) {
return data.filter(({ status }) => status === 'Live' || status === 'Unknown') return data.filter(({ status }) => status === 'Live' || status === 'Unknown')
} }
export async function* pollPublicData() { export async function* pollDataURL(url) {
const publicDataURL = 'https://woke.net/api/streams.json'
const refreshInterval = 5 * 1000 const refreshInterval = 5 * 1000
let lastData = [] let lastData = []
while (true) { while (true) {
let data = [] let data = []
try { try {
const resp = await fetch(publicDataURL) const resp = await fetch(url)
data = await resp.json() data = await resp.json()
} catch (err) { } catch (err) {
console.warn('error loading stream data', err) console.warn('error loading stream data', err)
@@ -32,17 +36,52 @@ export async function* pollPublicData() {
} }
} }
export async function* watchDataFile(path) {
const watcher = chokidar.watch(path)
while (true) {
let data
try {
const text = await fsPromises.readFile(path)
data = TOML.parse(text)
} catch (err) {
console.warn('error reading data file', err)
}
if (data) {
yield data.streams || []
}
await once(watcher, 'change')
}
}
export async function* markDataSource(dataSource, name) {
for await (const streamList of dataSource) {
for (const s of streamList) {
s._dataSource = name
}
yield streamList
}
}
export async function* combineDataSources(dataSources) {
for await (const streamLists of Repeater.latest(dataSources)) {
yield [].concat(...streamLists)
}
}
export class StreamIDGenerator { export class StreamIDGenerator {
constructor(parent) { constructor() {
this.idMap = new Map(parent ? parent.idMap : null) this.idMap = new Map()
this.idSet = new Set(this.idMap.values()) this.idSet = new Set()
} }
process(streams) { process(streams) {
const { idMap, idSet } = this const { idMap, idSet } = this
const localIdMap = new Map(idMap)
const localIdSet = new Set(idSet)
for (const stream of streams) { for (const stream of streams) {
const { link, source, label } = stream const { link, source, label, _dataSource } = stream
if (!idMap.has(link)) { if (!localIdMap.has(link)) {
let counter = 0 let counter = 0
let newId let newId
const normalizedText = (source || label || link) const normalizedText = (source || label || link)
@@ -54,11 +93,20 @@ export class StreamIDGenerator {
const counterPart = counter === 0 && textPart ? '' : counter const counterPart = counter === 0 && textPart ? '' : counter
newId = `${textPart}${counterPart}` newId = `${textPart}${counterPart}`
counter++ counter++
} while (idSet.has(newId)) } while (localIdSet.has(newId))
idMap.set(link, newId)
idSet.add(newId) localIdMap.set(link, newId)
localIdSet.add(newId)
// Custom stream ids are not persisted so that editing them doesn't create a bunch of unused ids.
const persistId = _dataSource !== 'custom'
if (persistId) {
idMap.set(link, newId)
idSet.add(newId)
}
} }
stream._id = idMap.get(link)
stream._id = localIdMap.get(link)
} }
return streams return streams
} }

View File

@@ -1,10 +1,17 @@
import fs from 'fs' import fs from 'fs'
import yargs from 'yargs' import yargs from 'yargs'
import TOML from '@iarna/toml' import TOML from '@iarna/toml'
import { Repeater } from '@repeaterjs/repeater'
import { app, shell, session, BrowserWindow } from 'electron' import { app, shell, session, BrowserWindow } from 'electron'
import { ensureValidURL } from '../util' import { ensureValidURL } from '../util'
import { pollPublicData, StreamIDGenerator } from './data' import {
pollDataURL,
watchDataFile,
StreamIDGenerator,
markDataSource,
combineDataSources,
} from './data'
import StreamWindow from './StreamWindow' import StreamWindow from './StreamWindow'
import StreamdelayClient from './StreamdelayClient' import StreamdelayClient from './StreamdelayClient'
import initWebServer from './server' import initWebServer from './server'
@@ -18,6 +25,18 @@ function parseArgs() {
describe: 'Background color of wall (useful for chroma-keying)', describe: 'Background color of wall (useful for chroma-keying)',
default: '#000', default: '#000',
}) })
.group(['data.json-url', 'data.toml-file'], 'Datasources')
.option('data.json-url', {
describe: 'Fetch streams from the specified URL(s)',
array: true,
default: ['https://woke.net/api/streams.json'],
})
.option('data.toml-file', {
describe: 'Fetch streams from the specified file(s)',
normalize: true,
array: true,
default: [],
})
.group( .group(
[ [
'control.username', 'control.username',
@@ -92,6 +111,11 @@ async function main() {
}) })
const idGen = new StreamIDGenerator() const idGen = new StreamIDGenerator()
let updateCustomStreams
const customStreamData = new Repeater(async (push) => {
await push([])
updateCustomStreams = push
})
const streamWindow = new StreamWindow({ const streamWindow = new StreamWindow({
backgroundColor: argv.backgroundColor, backgroundColor: argv.backgroundColor,
@@ -117,10 +141,7 @@ async function main() {
} else if (msg.type === 'set-view-blurred') { } else if (msg.type === 'set-view-blurred') {
streamWindow.setViewBlurred(msg.viewIdx, msg.blurred) streamWindow.setViewBlurred(msg.viewIdx, msg.blurred)
} else if (msg.type === 'set-custom-streams') { } else if (msg.type === 'set-custom-streams') {
const customIDGen = new StreamIDGenerator(idGen) updateCustomStreams(msg.streams)
clientState.customStreams = customIDGen.process(msg.streams)
streamWindow.send('state', clientState)
broadcastState(clientState)
} else if (msg.type === 'reload-view') { } else if (msg.type === 'reload-view') {
streamWindow.reloadView(msg.viewIdx) streamWindow.reloadView(msg.viewIdx)
} else if (msg.type === 'browse' || msg.type === 'dev-tools') { } else if (msg.type === 'browse' || msg.type === 'dev-tools') {
@@ -190,7 +211,17 @@ async function main() {
broadcastState(clientState) broadcastState(clientState)
}) })
for await (const rawStreams of pollPublicData()) { const dataSources = [
...argv.data['json-url'].map((url) =>
markDataSource(pollDataURL(url), 'json-url'),
),
...argv.data['toml-file'].map((path) =>
markDataSource(watchDataFile(path), 'toml-file'),
),
markDataSource(customStreamData, 'custom'),
]
for await (const rawStreams of combineDataSources(dataSources)) {
const streams = idGen.process(rawStreams) const streams = idGen.process(rawStreams)
clientState.streams = streams clientState.streams = streams
streamWindow.send('state', clientState) streamWindow.send('state', clientState)

View File

@@ -25,7 +25,6 @@ function App({ wsEndpoint }) {
const [delayState, setDelayState] = useState({ const [delayState, setDelayState] = useState({
isConnected: false, isConnected: false,
}) })
const allStreams = sortBy([...streams, ...customStreams], ['_id'])
useEffect(() => { useEffect(() => {
const ws = new ReconnectingWebSocket(wsEndpoint, [], { const ws = new ReconnectingWebSocket(wsEndpoint, [], {
@@ -38,17 +37,11 @@ function App({ wsEndpoint }) {
ws.addEventListener('message', (ev) => { ws.addEventListener('message', (ev) => {
const msg = JSON.parse(ev.data) const msg = JSON.parse(ev.data)
if (msg.type === 'state') { if (msg.type === 'state') {
const { const { streams: newStreams, views, streamdelay } = msg.state
streams: newStreams,
views,
customStreams: newCustomStreams,
streamdelay,
} = msg.state
const newStateIdxMap = new Map() const newStateIdxMap = new Map()
const allStreams = [...newStreams, ...newCustomStreams]
for (const viewState of views) { for (const viewState of views) {
const { pos, content } = viewState.context const { pos, content } = viewState.context
const stream = allStreams.find((d) => d.link === content.url) const stream = newStreams.find((d) => d.link === content.url)
const streamId = stream?._id const streamId = stream?._id
const state = State.from(viewState.state) const state = State.from(viewState.state)
const isListening = state.matches( const isListening = state.matches(
@@ -69,8 +62,8 @@ function App({ wsEndpoint }) {
} }
} }
setStateIdxMap(newStateIdxMap) setStateIdxMap(newStateIdxMap)
setStreams(newStreams) setStreams(sortBy(newStreams, ['_id']))
setCustomStreams(newCustomStreams) setCustomStreams(newStreams.filter((s) => s._dataSource === 'custom'))
setDelayState( setDelayState(
streamdelay.isConnected && { streamdelay.isConnected && {
...streamdelay, ...streamdelay,
@@ -87,9 +80,7 @@ function App({ wsEndpoint }) {
const handleSetView = useCallback( const handleSetView = useCallback(
(idx, streamId) => { (idx, streamId) => {
const newSpaceIdxMap = new Map(stateIdxMap) const newSpaceIdxMap = new Map(stateIdxMap)
const stream = [...streams, ...customStreams].find( const stream = streams.find((d) => d._id === streamId)
(d) => d._id === streamId,
)
if (stream) { if (stream) {
const content = { const content = {
url: stream?.link, url: stream?.link,
@@ -109,7 +100,7 @@ function App({ wsEndpoint }) {
]) ])
wsRef.current.send(JSON.stringify({ type: 'set-views', views })) wsRef.current.send(JSON.stringify({ type: 'set-views', views }))
}, },
[streams, customStreams, stateIdxMap], [streams, stateIdxMap],
) )
const handleSetListening = useCallback((idx, listening) => { const handleSetListening = useCallback((idx, listening) => {
@@ -270,7 +261,7 @@ function App({ wsEndpoint }) {
</div> </div>
<div> <div>
{isConnected {isConnected
? allStreams.map((row) => ( ? streams.map((row) => (
<StreamLine id={row._id} row={row} onClickId={handleClickId} /> <StreamLine id={row._id} row={row} onClickId={handleClickId} />
)) ))
: 'loading...'} : 'loading...'}