mirror of
https://github.com/streamwall/streamwall.git
synced 2026-01-31 09:22:49 -05:00
Add support for displaying custom web content in views
This commit is contained in:
@@ -27,15 +27,17 @@ function Overlay({ views, streams, customStreams }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{activeViews.map((viewState) => {
|
{activeViews.map((viewState) => {
|
||||||
const { url, pos } = viewState.context
|
const { content, pos } = viewState.context
|
||||||
const data = [...streams, ...customStreams].find((d) => url === d.Link)
|
const data = [...streams, ...customStreams].find(
|
||||||
|
(d) => content.url === d.Link,
|
||||||
|
)
|
||||||
const isListening = viewState.matches('displaying.running.listening')
|
const isListening = viewState.matches('displaying.running.listening')
|
||||||
const isLoading = viewState.matches('displaying.loading')
|
const isLoading = viewState.matches('displaying.loading')
|
||||||
return (
|
return (
|
||||||
<SpaceBorder pos={pos} isListening={isListening}>
|
<SpaceBorder pos={pos} isListening={isListening}>
|
||||||
{data && (
|
{data && (
|
||||||
<StreamTitle isListening={isListening}>
|
<StreamTitle isListening={isListening}>
|
||||||
<StreamIcon url={url} />
|
<StreamIcon url={content.url} />
|
||||||
<span>
|
<span>
|
||||||
{data.hasOwnProperty('Label') ? (
|
{data.hasOwnProperty('Label') ? (
|
||||||
data.Label
|
data.Label
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import isEqual from 'lodash/isEqual'
|
||||||
import intersection from 'lodash/intersection'
|
import intersection from 'lodash/intersection'
|
||||||
import EventEmitter from 'events'
|
import EventEmitter from 'events'
|
||||||
import { BrowserView, BrowserWindow, ipcMain } from 'electron'
|
import { BrowserView, BrowserWindow, ipcMain } from 'electron'
|
||||||
import { interpret } from 'xstate'
|
import { interpret } from 'xstate'
|
||||||
|
|
||||||
import viewStateMachine from './viewStateMachine'
|
import viewStateMachine from './viewStateMachine'
|
||||||
import { boxesFromViewURLMap } from './geometry'
|
import { boxesFromViewContentMap } from './geometry'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
WIDTH,
|
WIDTH,
|
||||||
@@ -118,7 +119,7 @@ export default class StreamWindow extends EventEmitter {
|
|||||||
this.views.map(({ state }) => ({
|
this.views.map(({ state }) => ({
|
||||||
state: state.value,
|
state: state.value,
|
||||||
context: {
|
context: {
|
||||||
url: state.context.url,
|
content: state.context.content,
|
||||||
info: state.context.info,
|
info: state.context.info,
|
||||||
pos: state.context.pos,
|
pos: state.context.pos,
|
||||||
},
|
},
|
||||||
@@ -126,34 +127,38 @@ export default class StreamWindow extends EventEmitter {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setViews(viewURLMap) {
|
setViews(viewContentMap) {
|
||||||
const { win, views } = this
|
const { win, views } = this
|
||||||
const boxes = boxesFromViewURLMap(GRID_COUNT, GRID_COUNT, viewURLMap)
|
const boxes = boxesFromViewContentMap(
|
||||||
const remainingBoxes = new Set(boxes.filter(({ url }) => url))
|
GRID_COUNT,
|
||||||
|
GRID_COUNT,
|
||||||
|
viewContentMap,
|
||||||
|
)
|
||||||
|
const remainingBoxes = new Set(boxes)
|
||||||
const unusedViews = new Set(views)
|
const unusedViews = new Set(views)
|
||||||
const viewsToDisplay = []
|
const viewsToDisplay = []
|
||||||
|
|
||||||
// We try to find the best match for moving / reusing existing views to match the new positions.
|
// We try to find the best match for moving / reusing existing views to match the new positions.
|
||||||
const matchers = [
|
const matchers = [
|
||||||
// First try to find a loaded view of the same URL in the same space...
|
// First try to find a loaded view of the same URL in the same space...
|
||||||
(v, url, spaces) =>
|
(v, content, spaces) =>
|
||||||
v.state.context.url === url &&
|
isEqual(v.state.context.content, content) &&
|
||||||
v.state.matches('displaying.running') &&
|
v.state.matches('displaying.running') &&
|
||||||
intersection(v.state.context.pos.spaces, spaces).length > 0,
|
intersection(v.state.context.pos.spaces, spaces).length > 0,
|
||||||
// Then try to find a loaded view of the same URL...
|
// Then try to find a loaded view of the same URL...
|
||||||
(v, url) =>
|
(v, content) =>
|
||||||
v.state.context.url === url && v.state.matches('displaying.running'),
|
isEqual(v.state.context.content, content) &&
|
||||||
|
v.state.matches('displaying.running'),
|
||||||
// Then try view with the same URL that is still loading...
|
// Then try view with the same URL that is still loading...
|
||||||
(v, url) => v.state.context.url === url,
|
(v, content) => isEqual(v.state.context.content, content),
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const matcher of matchers) {
|
for (const matcher of matchers) {
|
||||||
for (const box of remainingBoxes) {
|
for (const box of remainingBoxes) {
|
||||||
const { url, spaces } = box
|
const { content, spaces } = box
|
||||||
let foundView
|
let foundView
|
||||||
for (const view of unusedViews) {
|
for (const view of unusedViews) {
|
||||||
if (matcher(view, url, spaces)) {
|
if (matcher(view, content, spaces)) {
|
||||||
foundView = view
|
foundView = view
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -173,7 +178,7 @@ export default class StreamWindow extends EventEmitter {
|
|||||||
|
|
||||||
const newViews = []
|
const newViews = []
|
||||||
for (const { box, view } of viewsToDisplay) {
|
for (const { box, view } of viewsToDisplay) {
|
||||||
const { url, x, y, w, h, spaces } = box
|
const { content, x, y, w, h, spaces } = box
|
||||||
const pos = {
|
const pos = {
|
||||||
x: SPACE_WIDTH * x,
|
x: SPACE_WIDTH * x,
|
||||||
y: SPACE_HEIGHT * y,
|
y: SPACE_HEIGHT * y,
|
||||||
@@ -181,7 +186,7 @@ export default class StreamWindow extends EventEmitter {
|
|||||||
height: SPACE_HEIGHT * h,
|
height: SPACE_HEIGHT * h,
|
||||||
spaces,
|
spaces,
|
||||||
}
|
}
|
||||||
view.send({ type: 'DISPLAY', pos, url })
|
view.send({ type: 'DISPLAY', pos, content })
|
||||||
newViews.push(view)
|
newViews.push(view)
|
||||||
}
|
}
|
||||||
for (const view of unusedViews) {
|
for (const view of unusedViews) {
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
export function boxesFromViewURLMap(width, height, stateURLMap) {
|
import isEqual from 'lodash/isEqual'
|
||||||
|
|
||||||
|
export function boxesFromViewContentMap(width, height, viewContentMap) {
|
||||||
const boxes = []
|
const boxes = []
|
||||||
const visited = new Set()
|
const visited = new Set()
|
||||||
|
|
||||||
|
function isPosContent(x, y, content) {
|
||||||
|
const checkIdx = width * y + x
|
||||||
|
return (
|
||||||
|
!visited.has(checkIdx) && isEqual(viewContentMap.get(checkIdx), content)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function findLargestBox(x, y) {
|
function findLargestBox(x, y) {
|
||||||
const idx = width * y + x
|
const idx = width * y + x
|
||||||
const spaces = [idx]
|
const spaces = [idx]
|
||||||
const url = stateURLMap.get(idx)
|
const content = viewContentMap.get(idx)
|
||||||
|
|
||||||
let maxY
|
let maxY
|
||||||
for (maxY = y + 1; maxY < height; maxY++) {
|
for (maxY = y + 1; maxY < height; maxY++) {
|
||||||
const checkIdx = width * maxY + x
|
if (!isPosContent(x, maxY, content)) {
|
||||||
if (visited.has(checkIdx) || stateURLMap.get(checkIdx) !== url) {
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
spaces.push(width * maxY + x)
|
spaces.push(width * maxY + x)
|
||||||
@@ -20,8 +28,7 @@ export function boxesFromViewURLMap(width, height, stateURLMap) {
|
|||||||
let cy = y
|
let cy = y
|
||||||
scan: for (cx = x + 1; cx < width; cx++) {
|
scan: for (cx = x + 1; cx < width; cx++) {
|
||||||
for (cy = y; cy < maxY; cy++) {
|
for (cy = y; cy < maxY; cy++) {
|
||||||
const checkIdx = width * cy + cx
|
if (!isPosContent(cx, cy, content)) {
|
||||||
if (visited.has(checkIdx) || stateURLMap.get(checkIdx) !== url) {
|
|
||||||
break scan
|
break scan
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,13 +39,13 @@ export function boxesFromViewURLMap(width, height, stateURLMap) {
|
|||||||
const w = cx - x
|
const w = cx - x
|
||||||
const h = maxY - y
|
const h = maxY - y
|
||||||
spaces.sort()
|
spaces.sort()
|
||||||
return { url, x, y, w, h, spaces }
|
return { content, x, y, w, h, spaces }
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let y = 0; y < width; y++) {
|
for (let y = 0; y < width; y++) {
|
||||||
for (let x = 0; x < height; x++) {
|
for (let x = 0; x < height; x++) {
|
||||||
const idx = width * y + x
|
const idx = width * y + x
|
||||||
if (visited.has(idx) || stateURLMap.get(idx) === undefined) {
|
if (visited.has(idx) || viewContentMap.get(idx) === undefined) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { boxesFromViewURLMap } from './geometry'
|
import { boxesFromViewContentMap } from './geometry'
|
||||||
|
|
||||||
function example([text]) {
|
function example([text]) {
|
||||||
return text
|
return text
|
||||||
.replace(/\s/g, '')
|
.replace(/\s/g, '')
|
||||||
.split('')
|
.split('')
|
||||||
.map((c) => (c === '.' ? undefined : c))
|
.map((c) => (c === '.' ? undefined : { url: c }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const box1 = example`
|
const box1 = example`
|
||||||
@@ -41,8 +41,8 @@ describe.each([
|
|||||||
2,
|
2,
|
||||||
box1,
|
box1,
|
||||||
[
|
[
|
||||||
{ url: 'a', x: 0, y: 0, w: 1, h: 2, spaces: [0, 2] },
|
{ content: { url: 'a' }, x: 0, y: 0, w: 1, h: 2, spaces: [0, 2] },
|
||||||
{ url: 'b', x: 1, y: 0, w: 1, h: 2, spaces: [1, 3] },
|
{ content: { url: 'b' }, x: 1, y: 0, w: 1, h: 2, spaces: [1, 3] },
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -50,8 +50,8 @@ describe.each([
|
|||||||
2,
|
2,
|
||||||
box2,
|
box2,
|
||||||
[
|
[
|
||||||
{ url: 'a', x: 0, y: 0, w: 2, h: 1, spaces: [0, 1] },
|
{ content: { url: 'a' }, x: 0, y: 0, w: 2, h: 1, spaces: [0, 1] },
|
||||||
{ url: 'b', x: 0, y: 1, w: 2, h: 1, spaces: [2, 3] },
|
{ content: { url: 'b' }, x: 0, y: 1, w: 2, h: 1, spaces: [2, 3] },
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -59,28 +59,33 @@ describe.each([
|
|||||||
3,
|
3,
|
||||||
box3,
|
box3,
|
||||||
[
|
[
|
||||||
{ url: 'a', x: 0, y: 0, w: 2, h: 2, spaces: [0, 1, 3, 4] },
|
{ content: { url: 'a' }, x: 0, y: 0, w: 2, h: 2, spaces: [0, 1, 3, 4] },
|
||||||
{ url: 'c', x: 2, y: 0, w: 1, h: 1, spaces: [2] },
|
{ content: { url: 'c' }, x: 2, y: 0, w: 1, h: 1, spaces: [2] },
|
||||||
{ url: 'a', x: 2, y: 1, w: 1, h: 1, spaces: [5] },
|
{ content: { url: 'a' }, x: 2, y: 1, w: 1, h: 1, spaces: [5] },
|
||||||
{ url: 'd', x: 0, y: 2, w: 1, h: 1, spaces: [6] },
|
{ content: { url: 'd' }, x: 0, y: 2, w: 1, h: 1, spaces: [6] },
|
||||||
{ url: 'a', x: 1, y: 2, w: 1, h: 1, spaces: [7] },
|
{ content: { url: 'a' }, x: 1, y: 2, w: 1, h: 1, spaces: [7] },
|
||||||
{ url: 'e', x: 2, y: 2, w: 1, h: 1, spaces: [8] },
|
{ content: { url: 'e' }, x: 2, y: 2, w: 1, h: 1, spaces: [8] },
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[3, 3, box4, [{ url: 'a', x: 1, y: 1, w: 2, h: 2, spaces: [4, 5, 7, 8] }]],
|
[
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
box4,
|
||||||
|
[{ content: { url: 'a' }, x: 1, y: 1, w: 2, h: 2, spaces: [4, 5, 7, 8] }],
|
||||||
|
],
|
||||||
[
|
[
|
||||||
3,
|
3,
|
||||||
3,
|
3,
|
||||||
box5,
|
box5,
|
||||||
[
|
[
|
||||||
{ url: 'a', x: 2, y: 0, w: 1, h: 3, spaces: [2, 5, 8] },
|
{ content: { url: 'a' }, x: 2, y: 0, w: 1, h: 3, spaces: [2, 5, 8] },
|
||||||
{ url: 'a', x: 1, y: 2, w: 1, h: 1, spaces: [7] },
|
{ content: { url: 'a' }, x: 1, y: 2, w: 1, h: 1, spaces: [7] },
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
])('boxesFromViewURLMap(%i, %i, %j)', (width, height, data, expected) => {
|
])('boxesFromViewContentMap(%i, %i, %j)', (width, height, data, expected) => {
|
||||||
test(`returns expected ${expected.length} boxes`, () => {
|
test(`returns expected ${expected.length} boxes`, () => {
|
||||||
const stateURLMap = new Map(data.map((v, idx) => [idx, v]))
|
const stateURLMap = new Map(data.map((v, idx) => [idx, v]))
|
||||||
const result = boxesFromViewURLMap(width, height, stateURLMap)
|
const result = boxesFromViewContentMap(width, height, stateURLMap)
|
||||||
expect(result).toStrictEqual(expected)
|
expect(result).toStrictEqual(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import isEqual from 'lodash/isEqual'
|
||||||
import { Machine, assign } from 'xstate'
|
import { Machine, assign } from 'xstate'
|
||||||
|
|
||||||
const viewStateMachine = Machine(
|
const viewStateMachine = Machine(
|
||||||
@@ -7,7 +8,7 @@ const viewStateMachine = Machine(
|
|||||||
context: {
|
context: {
|
||||||
view: null,
|
view: null,
|
||||||
pos: null,
|
pos: null,
|
||||||
url: null,
|
content: null,
|
||||||
info: {},
|
info: {},
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
@@ -20,14 +21,14 @@ const viewStateMachine = Machine(
|
|||||||
initial: 'loading',
|
initial: 'loading',
|
||||||
entry: assign({
|
entry: assign({
|
||||||
pos: (context, event) => event.pos,
|
pos: (context, event) => event.pos,
|
||||||
url: (context, event) => event.url,
|
content: (context, event) => event.content,
|
||||||
}),
|
}),
|
||||||
on: {
|
on: {
|
||||||
DISPLAY: {
|
DISPLAY: {
|
||||||
actions: assign({
|
actions: assign({
|
||||||
pos: (context, event) => event.pos,
|
pos: (context, event) => event.pos,
|
||||||
}),
|
}),
|
||||||
cond: 'urlUnchanged',
|
cond: 'contentUnchanged',
|
||||||
},
|
},
|
||||||
RELOAD: '.loading',
|
RELOAD: '.loading',
|
||||||
},
|
},
|
||||||
@@ -38,7 +39,7 @@ const viewStateMachine = Machine(
|
|||||||
states: {
|
states: {
|
||||||
page: {
|
page: {
|
||||||
invoke: {
|
invoke: {
|
||||||
src: 'loadURL',
|
src: 'loadPage',
|
||||||
onDone: {
|
onDone: {
|
||||||
target: 'video',
|
target: 'video',
|
||||||
},
|
},
|
||||||
@@ -74,7 +75,7 @@ const viewStateMachine = Machine(
|
|||||||
}),
|
}),
|
||||||
'positionView',
|
'positionView',
|
||||||
],
|
],
|
||||||
cond: 'urlUnchanged',
|
cond: 'contentUnchanged',
|
||||||
},
|
},
|
||||||
MUTE: '.muted',
|
MUTE: '.muted',
|
||||||
UNMUTE: '.listening',
|
UNMUTE: '.listening',
|
||||||
@@ -108,16 +109,17 @@ const viewStateMachine = Machine(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
guards: {
|
guards: {
|
||||||
urlUnchanged: (context, event) => {
|
contentUnchanged: (context, event) => {
|
||||||
return context.url === event.url
|
return isEqual(context.content, event.content)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
loadURL: async (context, event) => {
|
loadPage: async (context, event) => {
|
||||||
const { url, view } = context
|
const { content, view } = context
|
||||||
const wc = view.webContents
|
const wc = view.webContents
|
||||||
wc.audioMuted = true
|
wc.audioMuted = true
|
||||||
await wc.loadURL(url)
|
await wc.loadURL(content.url)
|
||||||
|
if (content.kind === 'video') {
|
||||||
wc.insertCSS(
|
wc.insertCSS(
|
||||||
`
|
`
|
||||||
* {
|
* {
|
||||||
@@ -147,9 +149,14 @@ const viewStateMachine = Machine(
|
|||||||
`,
|
`,
|
||||||
{ cssOrigin: 'user' },
|
{ cssOrigin: 'user' },
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
startVideo: async (context, event) => {
|
startVideo: async (context, event) => {
|
||||||
const wc = context.view.webContents
|
const { content, view } = context
|
||||||
|
if (content.kind !== 'video') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const wc = view.webContents
|
||||||
const info = await wc.executeJavaScript(`
|
const info = await wc.executeJavaScript(`
|
||||||
const sleep = ms => new Promise((resolve) => setTimeout(resolve, ms))
|
const sleep = ms => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
async function waitForVideo() {
|
async function waitForVideo() {
|
||||||
|
|||||||
@@ -11,26 +11,12 @@ import SoundIcon from '../static/volume-up-solid.svg'
|
|||||||
import ReloadIcon from '../static/redo-alt-solid.svg'
|
import ReloadIcon from '../static/redo-alt-solid.svg'
|
||||||
import LifeRingIcon from '../static/life-ring-regular.svg'
|
import LifeRingIcon from '../static/life-ring-regular.svg'
|
||||||
|
|
||||||
function emptyStateIdxMap() {
|
|
||||||
return new Map(
|
|
||||||
range(GRID_COUNT * GRID_COUNT).map((idx) => [
|
|
||||||
idx,
|
|
||||||
{
|
|
||||||
streamId: null,
|
|
||||||
url: null,
|
|
||||||
state: State.from({}),
|
|
||||||
isListening: false,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function App({ wsEndpoint }) {
|
function App({ wsEndpoint }) {
|
||||||
const wsRef = useRef()
|
const wsRef = useRef()
|
||||||
const [isConnected, setIsConnected] = useState(false)
|
const [isConnected, setIsConnected] = useState(false)
|
||||||
const [streams, setStreams] = useState([])
|
const [streams, setStreams] = useState([])
|
||||||
const [customStreams, setCustomStreams] = useState([])
|
const [customStreams, setCustomStreams] = useState([])
|
||||||
const [stateIdxMap, setStateIdxMap] = useState(emptyStateIdxMap())
|
const [stateIdxMap, setStateIdxMap] = useState(new Map())
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ws = new ReconnectingWebSocket(wsEndpoint, [], {
|
const ws = new ReconnectingWebSocket(wsEndpoint, [], {
|
||||||
@@ -48,20 +34,21 @@ function App({ wsEndpoint }) {
|
|||||||
views,
|
views,
|
||||||
customStreams: newCustomStreams,
|
customStreams: newCustomStreams,
|
||||||
} = msg.state
|
} = msg.state
|
||||||
const newStateIdxMap = emptyStateIdxMap()
|
const newStateIdxMap = new Map()
|
||||||
const allStreams = [...newStreams, ...newCustomStreams]
|
const allStreams = [...newStreams, ...newCustomStreams]
|
||||||
for (const viewState of views) {
|
for (const viewState of views) {
|
||||||
const { pos, url } = viewState.context
|
const { pos, content } = viewState.context
|
||||||
if (!url) {
|
const stream = allStreams.find((d) => d.Link === content.url)
|
||||||
continue
|
const streamId = stream?._id
|
||||||
}
|
|
||||||
const streamId = allStreams.find((d) => d.Link === url)?._id
|
|
||||||
const state = State.from(viewState.state)
|
const state = State.from(viewState.state)
|
||||||
const isListening = state.matches('displaying.running.listening')
|
const isListening = state.matches('displaying.running.listening')
|
||||||
for (const space of pos.spaces) {
|
for (const space of pos.spaces) {
|
||||||
|
if (!newStateIdxMap.has(space)) {
|
||||||
|
newStateIdxMap.set(space, {})
|
||||||
|
}
|
||||||
Object.assign(newStateIdxMap.get(space), {
|
Object.assign(newStateIdxMap.get(space), {
|
||||||
streamId,
|
streamId,
|
||||||
url,
|
content,
|
||||||
state,
|
state,
|
||||||
isListening,
|
isListening,
|
||||||
})
|
})
|
||||||
@@ -80,24 +67,25 @@ function App({ wsEndpoint }) {
|
|||||||
const handleSetView = useCallback(
|
const handleSetView = useCallback(
|
||||||
(idx, streamId) => {
|
(idx, streamId) => {
|
||||||
const newSpaceIdxMap = new Map(stateIdxMap)
|
const newSpaceIdxMap = new Map(stateIdxMap)
|
||||||
const url = [...streams, ...customStreams].find((d) => d._id === streamId)
|
const stream = [...streams, ...customStreams].find(
|
||||||
?.Link
|
(d) => d._id === streamId,
|
||||||
if (url) {
|
)
|
||||||
|
if (stream) {
|
||||||
|
const content = {
|
||||||
|
url: stream?.Link,
|
||||||
|
kind: stream?.Kind || 'video',
|
||||||
|
}
|
||||||
newSpaceIdxMap.set(idx, {
|
newSpaceIdxMap.set(idx, {
|
||||||
...newSpaceIdxMap.get(idx),
|
...newSpaceIdxMap.get(idx),
|
||||||
streamId,
|
streamId,
|
||||||
url,
|
content,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
newSpaceIdxMap.set(idx, {
|
newSpaceIdxMap.delete(idx)
|
||||||
...newSpaceIdxMap.get(idx),
|
|
||||||
streamId: null,
|
|
||||||
url: null,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const views = Array.from(newSpaceIdxMap, ([space, { url }]) => [
|
const views = Array.from(newSpaceIdxMap, ([space, { content }]) => [
|
||||||
space,
|
space,
|
||||||
url,
|
content,
|
||||||
])
|
])
|
||||||
wsRef.current.send(JSON.stringify({ type: 'set-views', views }))
|
wsRef.current.send(JSON.stringify({ type: 'set-views', views }))
|
||||||
},
|
},
|
||||||
@@ -155,18 +143,21 @@ function App({ wsEndpoint }) {
|
|||||||
<StyledGridLine>
|
<StyledGridLine>
|
||||||
{range(0, 3).map((x) => {
|
{range(0, 3).map((x) => {
|
||||||
const idx = 3 * y + x
|
const idx = 3 * y + x
|
||||||
const { streamId, isListening, url, state } = stateIdxMap.get(
|
const {
|
||||||
idx,
|
streamId = '',
|
||||||
)
|
isListening = false,
|
||||||
|
content = { url: '' },
|
||||||
|
state,
|
||||||
|
} = stateIdxMap.get(idx) || {}
|
||||||
return (
|
return (
|
||||||
<GridInput
|
<GridInput
|
||||||
idx={idx}
|
idx={idx}
|
||||||
url={url}
|
url={content.url}
|
||||||
onChangeSpace={handleSetView}
|
|
||||||
spaceValue={streamId}
|
spaceValue={streamId}
|
||||||
isError={state.matches('displaying.error')}
|
isError={state && state.matches('displaying.error')}
|
||||||
isDisplaying={state.matches('displaying')}
|
isDisplaying={state && state.matches('displaying')}
|
||||||
isListening={isListening}
|
isListening={isListening}
|
||||||
|
onChangeSpace={handleSetView}
|
||||||
onSetListening={handleSetListening}
|
onSetListening={handleSetListening}
|
||||||
onReloadView={handleReloadView}
|
onReloadView={handleReloadView}
|
||||||
onBrowse={handleBrowse}
|
onBrowse={handleBrowse}
|
||||||
@@ -189,13 +180,14 @@ function App({ wsEndpoint }) {
|
|||||||
Include an empty object at the end to create an extra input for a new custom stream.
|
Include an empty object at the end to create an extra input for a new custom stream.
|
||||||
We need it to be part of the array (rather than JSX below) for DOM diffing to match the key and retain focus.
|
We need it to be part of the array (rather than JSX below) for DOM diffing to match the key and retain focus.
|
||||||
*/}
|
*/}
|
||||||
{[...customStreams, { Link: '', Label: '' }].map(
|
{[...customStreams, { Link: '', Label: '', Kind: 'video' }].map(
|
||||||
({ Link, Label }, idx) => (
|
({ Link, Label, Kind }, idx) => (
|
||||||
<CustomStreamInput
|
<CustomStreamInput
|
||||||
key={idx}
|
key={idx}
|
||||||
idx={idx}
|
idx={idx}
|
||||||
Link={Link}
|
Link={Link}
|
||||||
Label={Label}
|
Label={Label}
|
||||||
|
Kind={Kind}
|
||||||
onChange={handleChangeCustomStream}
|
onChange={handleChangeCustomStream}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -311,6 +303,12 @@ function CustomStreamInput({ idx, onChange, ...props }) {
|
|||||||
},
|
},
|
||||||
[onChange],
|
[onChange],
|
||||||
)
|
)
|
||||||
|
const handleChangeKind = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
onChange(idx, { ...props, Kind: ev.target.value })
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
@@ -323,6 +321,10 @@ function CustomStreamInput({ idx, onChange, ...props }) {
|
|||||||
placeholder="Label (optional)"
|
placeholder="Label (optional)"
|
||||||
value={props.Label}
|
value={props.Label}
|
||||||
/>
|
/>
|
||||||
|
<select onChange={handleChangeKind} value={props.Kind}>
|
||||||
|
<option value="video">video</option>
|
||||||
|
<option value="web">web</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user