mirror of
https://github.com/streamwall/streamwall.git
synced 2026-01-31 01:12:48 -05:00
Fix view reuse bugs and create/destroy views on demand
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
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'
|
||||||
@@ -17,9 +18,10 @@ export default class StreamWindow extends EventEmitter {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
this.win = null
|
this.win = null
|
||||||
|
this.offscreenWin = null
|
||||||
this.overlayView = null
|
this.overlayView = null
|
||||||
this.views = []
|
this.views = []
|
||||||
this.viewStates = new Map()
|
this.viewActions = null
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@@ -41,6 +43,14 @@ export default class StreamWindow extends EventEmitter {
|
|||||||
})
|
})
|
||||||
this.win = win
|
this.win = win
|
||||||
|
|
||||||
|
const offscreenWin = new BrowserWindow({
|
||||||
|
show: false,
|
||||||
|
webPreferences: {
|
||||||
|
offscreen: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.offscreenWin = offscreenWin
|
||||||
|
|
||||||
const overlayView = new BrowserView({
|
const overlayView = new BrowserView({
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
@@ -56,10 +66,12 @@ export default class StreamWindow extends EventEmitter {
|
|||||||
overlayView.webContents.loadFile('overlay.html')
|
overlayView.webContents.loadFile('overlay.html')
|
||||||
this.overlayView = overlayView
|
this.overlayView = overlayView
|
||||||
|
|
||||||
const actions = {
|
this.viewActions = {
|
||||||
hideView: (context, event) => {
|
offscreenView: (context, event) => {
|
||||||
const { view } = context
|
const { view } = context
|
||||||
win.removeBrowserView(view)
|
// It appears necessary to initialize the browser view by adding it to a window and setting bounds. Otherwise, some streaming sites like Periscope will not load their videos due to the Page Visibility API being hidden.
|
||||||
|
offscreenWin.addBrowserView(view)
|
||||||
|
view.setBounds({ x: 0, y: 0, width: SPACE_WIDTH, height: SPACE_HEIGHT })
|
||||||
},
|
},
|
||||||
positionView: (context, event) => {
|
positionView: (context, event) => {
|
||||||
const { pos, view } = context
|
const { pos, view } = context
|
||||||
@@ -73,8 +85,13 @@ export default class StreamWindow extends EventEmitter {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const views = []
|
ipcMain.on('devtools-overlay', () => {
|
||||||
for (let idx = 0; idx <= 9; idx++) {
|
overlayView.webContents.openDevTools()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
createView() {
|
||||||
|
const { win, overlayView, viewActions } = this
|
||||||
const view = new BrowserView({
|
const view = new BrowserView({
|
||||||
webPreferences: { partition: 'persist:session', sandbox: true },
|
webPreferences: { partition: 'persist:session', sandbox: true },
|
||||||
})
|
})
|
||||||
@@ -87,67 +104,73 @@ export default class StreamWindow extends EventEmitter {
|
|||||||
parentWin: win,
|
parentWin: win,
|
||||||
overlayView,
|
overlayView,
|
||||||
})
|
})
|
||||||
.withConfig({ actions })
|
.withConfig({ actions: viewActions })
|
||||||
const service = interpret(machine).start()
|
const service = interpret(machine).start()
|
||||||
service.onTransition((state) => this.handleViewTransition(idx, state))
|
service.onTransition(this.emitState.bind(this))
|
||||||
|
|
||||||
views.push(service)
|
return service
|
||||||
}
|
|
||||||
this.views = views
|
|
||||||
|
|
||||||
ipcMain.on('devtools-overlay', () => {
|
|
||||||
overlayView.webContents.openDevTools()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleViewTransition(idx, state) {
|
emitState() {
|
||||||
const viewState = {
|
this.emit(
|
||||||
|
'state',
|
||||||
|
this.views.map(({ state }) => ({
|
||||||
state: state.value,
|
state: state.value,
|
||||||
context: {
|
context: {
|
||||||
url: state.context.url,
|
url: state.context.url,
|
||||||
info: state.context.info,
|
info: state.context.info,
|
||||||
pos: state.context.pos,
|
pos: state.context.pos,
|
||||||
},
|
},
|
||||||
}
|
})),
|
||||||
this.viewStates.set(idx, viewState)
|
)
|
||||||
this.emit('state', [...this.viewStates.values()])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setViews(viewURLMap) {
|
setViews(viewURLMap) {
|
||||||
const { views } = this
|
const { win, views } = this
|
||||||
const boxes = boxesFromViewURLMap(GRID_COUNT, GRID_COUNT, viewURLMap)
|
const boxes = boxesFromViewURLMap(GRID_COUNT, GRID_COUNT, viewURLMap)
|
||||||
const remainingBoxes = new Set(boxes.filter(({ url }) => url))
|
const remainingBoxes = new Set(boxes.filter(({ url }) => url))
|
||||||
|
|
||||||
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.
|
||||||
const matchers = [
|
const matchers = [
|
||||||
// First try to find a loaded view of the same URL...
|
// First try to find a loaded view of the same URL in the same space...
|
||||||
(v, url) =>
|
(v, url, spaces) =>
|
||||||
unusedViews.has(v) &&
|
|
||||||
v.state.context.url === url &&
|
v.state.context.url === url &&
|
||||||
v.state.matches('displaying.running'),
|
v.state.matches('displaying.running') &&
|
||||||
|
intersection(v.state.context.pos.spaces, spaces).length > 0,
|
||||||
|
// Then try to find a loaded view of the same URL...
|
||||||
|
(v, url) =>
|
||||||
|
v.state.context.url === url && 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) => unusedViews.has(v) && v.state.context.url === url,
|
(v, url) => v.state.context.url === url,
|
||||||
// If none could be found, try an unused view.
|
|
||||||
(v) => unusedViews.has(v),
|
|
||||||
() => {
|
|
||||||
throw new Error('could not find a usable view')
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const matcher of matchers) {
|
for (const matcher of matchers) {
|
||||||
for (const box of remainingBoxes) {
|
for (const box of remainingBoxes) {
|
||||||
const { url } = box
|
const { url, spaces } = box
|
||||||
const view = views.find((v) => matcher(v, url))
|
let foundView
|
||||||
if (view) {
|
for (const view of unusedViews) {
|
||||||
viewsToDisplay.push({ box, view })
|
if (matcher(view, url, spaces)) {
|
||||||
unusedViews.delete(view)
|
foundView = view
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundView) {
|
||||||
|
viewsToDisplay.push({ box, view: foundView })
|
||||||
|
unusedViews.delete(foundView)
|
||||||
remainingBoxes.delete(box)
|
remainingBoxes.delete(box)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const box of remainingBoxes) {
|
||||||
|
const view = this.createView()
|
||||||
|
viewsToDisplay.push({ box, view })
|
||||||
|
}
|
||||||
|
|
||||||
|
const newViews = []
|
||||||
for (const { box, view } of viewsToDisplay) {
|
for (const { box, view } of viewsToDisplay) {
|
||||||
const { url, x, y, w, h, spaces } = box
|
const { url, x, y, w, h, spaces } = box
|
||||||
const pos = {
|
const pos = {
|
||||||
@@ -158,11 +181,15 @@ export default class StreamWindow extends EventEmitter {
|
|||||||
spaces,
|
spaces,
|
||||||
}
|
}
|
||||||
view.send({ type: 'DISPLAY', pos, url })
|
view.send({ type: 'DISPLAY', pos, url })
|
||||||
|
newViews.push(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const view of unusedViews) {
|
for (const view of unusedViews) {
|
||||||
view.send('CLEAR')
|
const browserView = view.state.context.view
|
||||||
|
win.removeBrowserView(browserView)
|
||||||
|
browserView.destroy()
|
||||||
}
|
}
|
||||||
|
this.views = newViews
|
||||||
|
this.emitState()
|
||||||
}
|
}
|
||||||
|
|
||||||
setListeningView(viewIdx) {
|
setListeningView(viewIdx) {
|
||||||
|
|||||||
@@ -11,23 +11,10 @@ const viewStateMachine = Machine(
|
|||||||
info: {},
|
info: {},
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
CLEAR: 'empty',
|
|
||||||
DISPLAY: 'displaying',
|
DISPLAY: 'displaying',
|
||||||
},
|
},
|
||||||
states: {
|
states: {
|
||||||
empty: {
|
empty: {},
|
||||||
entry: assign({
|
|
||||||
pos: {},
|
|
||||||
info: {},
|
|
||||||
url: null,
|
|
||||||
}),
|
|
||||||
invoke: {
|
|
||||||
src: 'clearView',
|
|
||||||
onError: {
|
|
||||||
target: '#view.displaying.error',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
displaying: {
|
displaying: {
|
||||||
id: 'displaying',
|
id: 'displaying',
|
||||||
initial: 'loading',
|
initial: 'loading',
|
||||||
@@ -47,6 +34,7 @@ const viewStateMachine = Machine(
|
|||||||
states: {
|
states: {
|
||||||
loading: {
|
loading: {
|
||||||
initial: 'page',
|
initial: 'page',
|
||||||
|
entry: 'offscreenView',
|
||||||
states: {
|
states: {
|
||||||
page: {
|
page: {
|
||||||
invoke: {
|
invoke: {
|
||||||
@@ -78,7 +66,6 @@ const viewStateMachine = Machine(
|
|||||||
running: {
|
running: {
|
||||||
initial: 'muted',
|
initial: 'muted',
|
||||||
entry: 'positionView',
|
entry: 'positionView',
|
||||||
exit: 'hideView',
|
|
||||||
on: {
|
on: {
|
||||||
DISPLAY: {
|
DISPLAY: {
|
||||||
actions: [
|
actions: [
|
||||||
@@ -126,9 +113,6 @@ const viewStateMachine = Machine(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
clearView: async (context, event) => {
|
|
||||||
await context.view.webContents.loadURL('about:blank')
|
|
||||||
},
|
|
||||||
loadURL: async (context, event) => {
|
loadURL: async (context, event) => {
|
||||||
const { url, view } = context
|
const { url, view } = context
|
||||||
const wc = view.webContents
|
const wc = view.webContents
|
||||||
|
|||||||
Reference in New Issue
Block a user