Make window position, frame, and grid count configurable

This commit is contained in:
Max Goodhart
2020-07-01 22:39:45 -07:00
parent fd64281676
commit a0fcc4e8a8
7 changed files with 187 additions and 76 deletions

View File

@@ -70,7 +70,11 @@ We've observed this occur in cases where file corruption is an issue. The fix ha
### The Streamwall Electron window only fits 2.5 tiles wide
It's possible that your system resolution is causing a problem. If you only broadcast at 720p, you can update the height and width in `src/constants.js` to 1280 and 720 respectively. Save your changes and restart Streamwall
Streamwall in its default settings needs enough screen space to display a 1920x1080 (1080p) window, with room for the titlebar. You can configure Streamwall to open a smaller window:
```
npm start -- --window.width=1024 --window.height=768
```
## Credits

View File

@@ -1,6 +1,19 @@
[window]
# Window dimensions
#width = 1920
#height = 1080
# Position a frameless window (useful for capturing w/ fixed screen positions)
#x = 0
#y = 0
#frameless = false
# Set the background color (useful for chroma-keying)
#background-color = "#0f0"
[grid]
#count = 3
[control]
# Address to serve control server from
address = "http://localhost:80"

View File

@@ -7,7 +7,6 @@ import { useHotkeys } from 'react-hotkeys-hook'
import { TailSpin } from 'svg-loaders-react'
import '../index.css'
import { WIDTH, HEIGHT } from '../constants'
import InstagramIcon from '../static/instagram.svg'
import FacebookIcon from '../static/facebook.svg'
@@ -16,7 +15,8 @@ import TwitchIcon from '../static/twitch.svg'
import YouTubeIcon from '../static/youtube.svg'
import SoundIcon from '../static/volume-up-solid.svg'
function Overlay({ views, streams, customStreams }) {
function Overlay({ config, views, streams, customStreams }) {
const { width, height } = config
const activeViews = views
.map(({ state, context }) => State.from(state, context))
.filter((s) => s.matches('displaying') && !s.matches('displaying.error'))
@@ -33,7 +33,12 @@ function Overlay({ views, streams, customStreams }) {
const isBlurred = viewState.matches('displaying.running.video.blurred')
const isLoading = viewState.matches('displaying.loading')
return (
<SpaceBorder pos={pos} isListening={isListening}>
<SpaceBorder
pos={pos}
windowWidth={width}
windowHeight={height}
isListening={isListening}
>
<BlurCover isBlurred={isBlurred} />
{data && (
<StreamTitle isListening={isListening}>
@@ -60,6 +65,7 @@ function Overlay({ views, streams, customStreams }) {
function App() {
const [state, setState] = useState({
config: {},
views: [],
streams: [],
customStreams: [],
@@ -75,9 +81,14 @@ function App() {
ipcRenderer.send('devtools-overlay')
})
const { views, streams, customStreams } = state
const { config, views, streams, customStreams } = state
return (
<Overlay views={views} streams={streams} customStreams={customStreams} />
<Overlay
config={config}
views={views}
streams={streams}
customStreams={customStreams}
/>
)
}
@@ -118,12 +129,12 @@ const SpaceBorder = styled.div.attrs((props) => ({
border: 0 solid black;
border-left-width: ${({ pos, borderWidth }) =>
pos.x === 0 ? 0 : borderWidth}px;
border-right-width: ${({ pos, borderWidth }) =>
pos.x + pos.width === WIDTH ? 0 : borderWidth}px;
border-right-width: ${({ pos, borderWidth, windowWidth }) =>
pos.x + pos.width === windowWidth ? 0 : borderWidth}px;
border-top-width: ${({ pos, borderWidth }) =>
pos.y === 0 ? 0 : borderWidth}px;
border-bottom-width: ${({ pos, borderWidth }) =>
pos.y + pos.height === HEIGHT ? 0 : borderWidth}px;
border-bottom-width: ${({ pos, borderWidth, windowHeight }) =>
pos.y + pos.height === windowHeight ? 0 : borderWidth}px;
box-shadow: ${({ isListening }) =>
isListening ? '0 0 10px red inset' : 'none'};
box-sizing: border-box;

View File

@@ -1,5 +0,0 @@
export const WIDTH = 1920
export const HEIGHT = 1080
export const GRID_COUNT = 3 // Note: if greater than 3, keyboard shortcuts may need reworking
export const SPACE_WIDTH = Math.floor(WIDTH / GRID_COUNT)
export const SPACE_HEIGHT = Math.floor(HEIGHT / GRID_COUNT)

View File

@@ -7,18 +7,15 @@ import { interpret } from 'xstate'
import viewStateMachine from './viewStateMachine'
import { boxesFromViewContentMap } from './geometry'
import {
WIDTH,
HEIGHT,
GRID_COUNT,
SPACE_WIDTH,
SPACE_HEIGHT,
} from '../constants'
export default class StreamWindow extends EventEmitter {
constructor({ backgroundColor = '#000' }) {
constructor(config) {
super()
this.backgroundColor = backgroundColor
const { width, height, gridCount } = config
config.spaceWidth = Math.floor(width / gridCount)
config.spaceHeight = Math.floor(height / gridCount)
this.config = config
this.win = null
this.offscreenWin = null
this.overlayView = null
@@ -27,11 +24,24 @@ export default class StreamWindow extends EventEmitter {
}
init() {
const {
width,
height,
x,
y,
frameless,
backgroundColor,
spaceWidth,
spaceHeight,
} = this.config
const win = new BrowserWindow({
title: 'Streamwall',
width: WIDTH,
height: HEIGHT,
backgroundColor: this.backgroundColor,
width,
height,
x,
y,
frame: !frameless,
backgroundColor,
useContentSize: true,
show: false,
})
@@ -63,8 +73,8 @@ export default class StreamWindow extends EventEmitter {
overlayView.setBounds({
x: 0,
y: 0,
width: WIDTH,
height: HEIGHT,
width,
height,
})
overlayView.webContents.loadFile('overlay.html')
this.overlayView = overlayView
@@ -75,7 +85,7 @@ export default class StreamWindow extends EventEmitter {
// 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.
win.removeBrowserView(view)
offscreenWin.addBrowserView(view)
view.setBounds({ x: 0, y: 0, width: SPACE_WIDTH, height: SPACE_HEIGHT })
view.setBounds({ x: 0, y: 0, width: spaceWidth, height: spaceHeight })
},
positionView: (context, event) => {
const { pos, view } = context
@@ -96,6 +106,7 @@ export default class StreamWindow extends EventEmitter {
createView() {
const { win, overlayView, viewActions } = this
const { backgroundColor } = this.config
const view = new BrowserView({
webPreferences: {
nodeIntegration: false,
@@ -104,7 +115,7 @@ export default class StreamWindow extends EventEmitter {
sandbox: true,
},
})
view.setBackgroundColor(this.backgroundColor)
view.setBackgroundColor(backgroundColor)
const machine = viewStateMachine
.withContext({
@@ -135,12 +146,9 @@ export default class StreamWindow extends EventEmitter {
}
setViews(viewContentMap) {
const { gridCount, spaceWidth, spaceHeight } = this.config
const { win, views } = this
const boxes = boxesFromViewContentMap(
GRID_COUNT,
GRID_COUNT,
viewContentMap,
)
const boxes = boxesFromViewContentMap(gridCount, gridCount, viewContentMap)
const remainingBoxes = new Set(boxes)
const unusedViews = new Set(views)
const viewsToDisplay = []
@@ -187,10 +195,10 @@ export default class StreamWindow extends EventEmitter {
for (const { box, view } of viewsToDisplay) {
const { content, x, y, w, h, spaces } = box
const pos = {
x: SPACE_WIDTH * x,
y: SPACE_HEIGHT * y,
width: SPACE_WIDTH * w,
height: SPACE_HEIGHT * h,
x: spaceWidth * x,
y: spaceHeight * y,
width: spaceWidth * w,
height: spaceHeight * h,
spaces,
}
view.send({ type: 'DISPLAY', pos, content })

View File

@@ -21,7 +21,41 @@ function parseArgs() {
.config('config', (configPath) => {
return TOML.parse(fs.readFileSync(configPath, 'utf-8'))
})
.option('background-color', {
.group(['grid.count'], 'Grid dimensions')
.option('grid.count', {
number: true,
default: 3,
})
.group(
[
'window.width',
'window.height',
'window.x',
'window.y',
'window.frameless',
'window.background-color',
],
'Window settings',
)
.option('window.x', {
number: true,
})
.option('window.y', {
number: true,
})
.option('window.width', {
number: true,
default: 1920,
})
.option('window.height', {
number: true,
default: 1080,
})
.option('window.frameless', {
boolean: true,
default: false,
})
.option('window.background-color', {
describe: 'Background color of wall (useful for chroma-keying)',
default: '#000',
})
@@ -118,7 +152,13 @@ async function main() {
})
const streamWindow = new StreamWindow({
backgroundColor: argv.backgroundColor,
gridCount: argv.grid.count,
width: argv.window.width,
height: argv.window.height,
x: argv.window.x,
y: argv.window.y,
frameless: argv.window.frameless,
backgroundColor: argv.window['background-color'],
})
streamWindow.init()
@@ -126,6 +166,11 @@ async function main() {
let streamdelayClient = null
const clientState = {
config: {
width: argv.window.width,
height: argv.window.height,
gridCount: argv.grid.count,
},
streams: [],
customStreams: [],
views: [],

View File

@@ -9,16 +9,39 @@ import styled, { css } from 'styled-components'
import { useHotkeys } from 'react-hotkeys-hook'
import '../index.css'
import { GRID_COUNT } from '../constants'
import SoundIcon from '../static/volume-up-solid.svg'
import NoVideoIcon from '../static/video-slash-solid.svg'
import ReloadIcon from '../static/redo-alt-solid.svg'
import LifeRingIcon from '../static/life-ring-regular.svg'
import WindowIcon from '../static/window-maximize-regular.svg'
const hotkeyTriggers = [
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'0',
'q',
'w',
'e',
'r',
't',
'y',
'u',
'i',
'o',
'p',
]
function App({ wsEndpoint }) {
const wsRef = useRef()
const [isConnected, setIsConnected] = useState(false)
const [config, setConfig] = useState({})
const [streams, setStreams] = useState([])
const [customStreams, setCustomStreams] = useState([])
const [stateIdxMap, setStateIdxMap] = useState(new Map())
@@ -26,6 +49,8 @@ function App({ wsEndpoint }) {
isConnected: false,
})
const { gridCount } = config
useEffect(() => {
const ws = new ReconnectingWebSocket(wsEndpoint, [], {
maxReconnectionDelay: 5000,
@@ -37,7 +62,12 @@ function App({ wsEndpoint }) {
ws.addEventListener('message', (ev) => {
const msg = JSON.parse(ev.data)
if (msg.type === 'state') {
const { streams: newStreams, views, streamdelay } = msg.state
const {
config: newConfig,
streams: newStreams,
views,
streamdelay,
} = msg.state
const newStateIdxMap = new Map()
for (const viewState of views) {
const { pos, content } = viewState.context
@@ -61,6 +91,7 @@ function App({ wsEndpoint }) {
})
}
}
setConfig(newConfig)
setStateIdxMap(newStateIdxMap)
setStreams(sortBy(newStreams, ['_id']))
setCustomStreams(newStreams.filter((s) => s._dataSource === 'custom'))
@@ -149,15 +180,18 @@ function App({ wsEndpoint }) {
)
}, [])
const handleClickId = useCallback((streamId) => {
const availableIdx = range(GRID_COUNT * GRID_COUNT).find(
(i) => !stateIdxMap.has(i),
)
if (availableIdx === undefined) {
return
}
handleSetView(availableIdx, streamId)
})
const handleClickId = useCallback(
(streamId) => {
const availableIdx = range(gridCount * gridCount).find(
(i) => !stateIdxMap.has(i),
)
if (availableIdx === undefined) {
return
}
handleSetView(availableIdx, streamId)
},
[gridCount, stateIdxMap],
)
const handleChangeCustomStream = useCallback((idx, customStream) => {
let newCustomStreams = [...customStreams]
@@ -181,25 +215,26 @@ function App({ wsEndpoint }) {
}, [])
// 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)) {
useHotkeys(
`alt+${idx + 1}`,
() => {
const isListening = stateIdxMap.get(idx)?.isListening ?? false
handleSetListening(idx, !isListening)
},
[stateIdxMap],
)
useHotkeys(
`alt+shift+${idx + 1}`,
() => {
const isBlurred = stateIdxMap.get(idx)?.isBlurred ?? false
handleSetBlurred(idx, !isBlurred)
},
[stateIdxMap],
)
}
useHotkeys(
hotkeyTriggers.map((k) => `alt+${k}`).join(','),
(ev, { key }) => {
ev.preventDefault()
const idx = hotkeyTriggers.indexOf(key[key.length - 1])
const isListening = stateIdxMap.get(idx)?.isListening ?? false
handleSetListening(idx, !isListening)
},
[stateIdxMap],
)
useHotkeys(
hotkeyTriggers.map((k) => `alt+shift+${k}`).join(','),
(ev, { key }) => {
ev.preventDefault()
const idx = hotkeyTriggers.indexOf(key[key.length - 1])
const isBlurred = stateIdxMap.get(idx)?.isBlurred ?? false
handleSetBlurred(idx, !isBlurred)
},
[stateIdxMap],
)
useHotkeys(
`alt+c`,
() => {
@@ -227,10 +262,10 @@ function App({ wsEndpoint }) {
/>
<StyledDataContainer isConnected={isConnected}>
<div>
{range(0, GRID_COUNT).map((y) => (
{range(0, gridCount).map((y) => (
<StyledGridLine>
{range(0, GRID_COUNT).map((x) => {
const idx = GRID_COUNT * y + x
{range(0, gridCount).map((x) => {
const idx = gridCount * y + x
const {
streamId = '',
isListening = false,