From dd719629f182d28fa44a79daeaf59531b379ea96 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Mon, 6 Apr 2020 13:06:04 +0200 Subject: [PATCH 01/24] Add empty overlay menu for source preview to house webcam selection --- src/ui/studio/video-setup/index.js | 3 -- src/ui/studio/video-setup/preview.js | 71 +++++++++++++++++++++------- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/ui/studio/video-setup/index.js b/src/ui/studio/video-setup/index.js index 9f493d60..6f4c7212 100644 --- a/src/ui/studio/video-setup/index.js +++ b/src/ui/studio/video-setup/index.js @@ -156,7 +156,6 @@ export default function VideoSetup(props) { title = t('sources-video-user-selected'); hideActionButtons = !state.userStream && state.userAllowed !== false; body = , dimensions: () => dimensionsOf(inputs[0].stream), - extraHeight: SUBBOX_HEIGHT, }]; break; case 2: @@ -28,12 +26,10 @@ export function SourcePreview({ reselectSource, warnings, inputs }) { { body: , dimensions: () => dimensionsOf(inputs[0].stream), - extraHeight: SUBBOX_HEIGHT, }, { body: , dimensions: () => dimensionsOf(inputs[1].stream), - extraHeight: SUBBOX_HEIGHT, }, ]; break; @@ -57,15 +53,12 @@ function StreamPreview({ input, text }) { return ( - - {text} - {track && `: ${width}×${height}`} - + ); } -export const PreviewVideo = ({ allowed, stream, unexpectedEnd, ...props }) => { +const PreviewVideo = ({ allowed, stream, unexpectedEnd, ...props }) => { const resizeVideoBox = useVideoBoxResize(); const videoRef = useRef(); @@ -124,13 +117,55 @@ export const PreviewVideo = ({ allowed, stream, unexpectedEnd, ...props }) => { ); }; -export const UnshareButton = ({ handleClick }) => { - const { t } = useTranslation(); +const StreamSettings = () => { + const [isExpanded, setIsExpanded] = useState(false); return ( - +
+
+
setIsExpanded(old => !old)} + sx={{ + display: 'inline-block', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + color: 'white', + p: '6px', + m: 2, + fontSize: '30px', + lineHeight: '1em', + borderRadius: '10px', + cursor: 'pointer', + '&:hover': { + backgroundColor: 'rgba(0, 0, 0, 0.7)', + }, + '&:hover > svg': { + transform: isExpanded ? 'none' : 'rotate(45deg)', + }, + }} + > + +
+
+
+ +
+
); }; From a3f2fa6d18ce52930cbe2329cf2eb1f0b1d1cc17 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Fri, 17 Apr 2020 19:31:41 +0200 Subject: [PATCH 02/24] Add video device selector for user source --- src/i18n/locales/en.json | 1 + src/studio-state.js | 6 +++ src/ui/studio/capturer.js | 3 +- src/ui/studio/video-setup/index.js | 19 +++++-- src/ui/studio/video-setup/preview.js | 81 +++++++++++++++++++++++++--- src/util.js | 2 +- 6 files changed, 98 insertions(+), 14 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 9ee5b52d..6b4829e2 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -46,6 +46,7 @@ "sources-video-none-available": "Your browser/device does not support capturing display or camera video :-(", "sources-video-reselect-source": "Reselect source(s)", "sources-video-no-cam-detected": "No camera detected", + "sources-video-device": "Device", "sources-audio-question": "Record Audio?", "sources-audio-microphone": "Microphone", diff --git a/src/studio-state.js b/src/studio-state.js index db7eb472..8b77d8ff 100644 --- a/src/studio-state.js +++ b/src/studio-state.js @@ -22,6 +22,8 @@ export const STATE_ERROR = 'error'; const initialState = () => ({ + mediaDevices: [], + audioAllowed: null, audioStream: null, audioUnexpectedEnd: false, @@ -57,6 +59,10 @@ const initialState = () => ({ const reducer = (state, action) => { switch (action.type) { + case 'UPDATE_MEDIA_DEVICES': + console.log(action.payload); + return { ...state, mediaDevices: action.payload }; + case 'CHOOSE_AUDIO': return { ...state, audioChoice: action.payload }; diff --git a/src/ui/studio/capturer.js b/src/ui/studio/capturer.js index 32c21283..a33d4aaa 100644 --- a/src/ui/studio/capturer.js +++ b/src/ui/studio/capturer.js @@ -55,7 +55,7 @@ export async function startDisplayCapture(dispatch, settings) { } } -export async function startUserCapture(dispatch, settings) { +export async function startUserCapture(dispatch, settings, videoConstraints) { const maxFps = settings.camera?.maxFps ? { frameRate: { max: settings.camera.maxFps } } : {}; @@ -68,6 +68,7 @@ export async function startUserCapture(dispatch, settings) { facingMode: 'user', ...maxFps, ...maxHeight, + ...videoConstraints, }, audio: false, }; diff --git a/src/ui/studio/video-setup/index.js b/src/ui/studio/video-setup/index.js index 6f4c7212..d214a751 100644 --- a/src/ui/studio/video-setup/index.js +++ b/src/ui/studio/video-setup/index.js @@ -55,6 +55,7 @@ export default function VideoSetup(props) { const clickUser = async () => { setActiveSource(VIDEO_SOURCE_USER); await startUserCapture(dispatch, settings); + await queryMediaDevices(dispatch); }; const clickDisplay = async () => { setActiveSource(VIDEO_SOURCE_DISPLAY); @@ -63,7 +64,10 @@ export default function VideoSetup(props) { const clickBoth = async () => { setActiveSource(VIDEO_SOURCE_BOTH); await startUserCapture(dispatch, settings); - await startDisplayCapture(dispatch, settings); + await Promise.all([ + queryMediaDevices(dispatch), + startDisplayCapture(dispatch, settings), + ]); }; const reselectSource = () => { @@ -158,7 +162,7 @@ export default function VideoSetup(props) { body = { }; const Spacer = (rest) =>
; + +const queryMediaDevices = async (dispatch) => { + const devices = await navigator.mediaDevices.enumerateDevices(); + dispatch({ type: 'UPDATE_MEDIA_DEVICES', payload: devices }); +}; diff --git a/src/ui/studio/video-setup/preview.js b/src/ui/studio/video-setup/preview.js index 835da78d..51de6391 100644 --- a/src/ui/studio/video-setup/preview.js +++ b/src/ui/studio/video-setup/preview.js @@ -10,6 +10,14 @@ import { faExclamationTriangle, faTimes, faCog } from '@fortawesome/free-solid-s import { VideoBox, useVideoBoxResize } from '../elements.js'; import { dimensionsOf } from '../../../util.js'; +import { useDispatch, useStudioState } from '../../../studio-state'; +import { useSettings } from '../../../settings'; +import { + startDisplayCapture, + startUserCapture, + stopDisplayCapture, + stopUserCapture +} from '../capturer'; export function SourcePreview({ warnings, inputs }) { @@ -17,18 +25,18 @@ export function SourcePreview({ warnings, inputs }) { switch (inputs.length) { case 1: children = [{ - body: , + body: , dimensions: () => dimensionsOf(inputs[0].stream), }]; break; case 2: children = [ { - body: , + body: , dimensions: () => dimensionsOf(inputs[0].stream), }, { - body: , + body: , dimensions: () => dimensionsOf(inputs[1].stream), }, ]; @@ -53,7 +61,7 @@ function StreamPreview({ input, text }) { return ( - + ); } @@ -117,7 +125,7 @@ const PreviewVideo = ({ allowed, stream, unexpectedEnd, ...props }) => { ); }; -const StreamSettings = () => { +const StreamSettings = ({ isDesktop }) => { const [isExpanded, setIsExpanded] = useState(false); return ( @@ -162,10 +170,69 @@ const StreamSettings = () => { height: isExpanded ? '100px' : 0, transition: 'height 0.2s', overflow: 'hidden', - backgroundColor: 'rgba(255, 255, 255, 0.9)', + backgroundColor: 'rgba(255, 255, 255, 0.95)', + fontSize: '18px', }}> - +
+ + + { !isDesktop && } + +
+ blabla +
); }; + +const UserSettings = () => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const settings = useSettings(); + const state = useStudioState(); + + const currentDeviceId = state.userStream?.getVideoTracks()?.[0]?.getSettings()?.deviceId; + let devices = []; + for (const d of state.mediaDevices) { + // Only intersted in video inputs + if (d.kind !== 'videoinput') { + continue; + } + + // If we already have a device with that device ID, we ignore it. + if (devices.some(od => od.deviceId === d.deviceId)) { + continue; + } + + devices.push(d); + } + + const changeDevice = id => { + stopUserCapture(state.userStream, dispatch); + startUserCapture(dispatch, settings, { deviceId: { exact: id }}); + }; + + return + + {t('sources-video-device')}: + + + + + + Aspect ratio: + TODO + + ; +}; diff --git a/src/util.js b/src/util.js index d9d43126..73e39d79 100644 --- a/src/util.js +++ b/src/util.js @@ -95,7 +95,7 @@ export const userHasWebcam = async () => { return false; } - const devices = await navigator.mediaDevices.enumerateDevices() + const devices = await navigator.mediaDevices.enumerateDevices(); return devices.some(d => d.kind === 'videoinput'); } From c67eb1284ea8a40f742d1ec22d42ab81d547fd72 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Mon, 20 Apr 2020 11:40:46 +0200 Subject: [PATCH 03/24] Add studio specific prefix to local storage key I just noticed that we should do that to avoid clash with other things deployed on the same domain. --- src/ui/studio/save-creation/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/studio/save-creation/index.js b/src/ui/studio/save-creation/index.js index efcc9955..63a5005b 100644 --- a/src/ui/studio/save-creation/index.js +++ b/src/ui/studio/save-creation/index.js @@ -27,7 +27,7 @@ import FormField from './form-field'; import RecordingPreview from './recording-preview'; -const LAST_PRESENTER_KEY = 'lastPresenter'; +const LAST_PRESENTER_KEY = 'ocStudioLastPresenter'; const Input = props => ; From 8a4eb786ebf82b45affaf6a11a28b43b9f1b27e5 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Mon, 20 Apr 2020 12:25:14 +0200 Subject: [PATCH 04/24] Store last used video device ID in local storage and prefer that device --- src/ui/studio/video-setup/index.js | 8 ++++++-- src/ui/studio/video-setup/preview.js | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/ui/studio/video-setup/index.js b/src/ui/studio/video-setup/index.js index d214a751..26bfca88 100644 --- a/src/ui/studio/video-setup/index.js +++ b/src/ui/studio/video-setup/index.js @@ -31,6 +31,8 @@ import { ActionButtons } from '../elements'; import { SourcePreview } from './preview'; +export const LAST_VIDEO_DEVICE_KEY = 'ocStudioLastVideoDevice'; + export default function VideoSetup(props) { const { t } = useTranslation(); @@ -51,10 +53,12 @@ export default function VideoSetup(props) { const setActiveSource = s => dispatch({ type: 'CHOOSE_VIDEO', payload: s }); + const lastDeviceId = window.localStorage.getItem(LAST_VIDEO_DEVICE_KEY); + const userConstraints = lastDeviceId ? { deviceId: { ideal: lastDeviceId }} : {}; const clickUser = async () => { setActiveSource(VIDEO_SOURCE_USER); - await startUserCapture(dispatch, settings); + await startUserCapture(dispatch, settings, userConstraints); await queryMediaDevices(dispatch); }; const clickDisplay = async () => { @@ -63,7 +67,7 @@ export default function VideoSetup(props) { }; const clickBoth = async () => { setActiveSource(VIDEO_SOURCE_BOTH); - await startUserCapture(dispatch, settings); + await startUserCapture(dispatch, settings, userConstraints); await Promise.all([ queryMediaDevices(dispatch), startDisplayCapture(dispatch, settings), diff --git a/src/ui/studio/video-setup/preview.js b/src/ui/studio/video-setup/preview.js index 51de6391..c472e97f 100644 --- a/src/ui/studio/video-setup/preview.js +++ b/src/ui/studio/video-setup/preview.js @@ -18,6 +18,7 @@ import { stopDisplayCapture, stopUserCapture } from '../capturer'; +import { LAST_VIDEO_DEVICE_KEY } from './index.js'; export function SourcePreview({ warnings, inputs }) { @@ -208,6 +209,12 @@ const UserSettings = () => { devices.push(d); } + useEffect(() => { + if (currentDeviceId) { + window.localStorage.setItem(LAST_VIDEO_DEVICE_KEY, currentDeviceId); + } + }); + const changeDevice = id => { stopUserCapture(state.userStream, dispatch); startUserCapture(dispatch, settings, { deviceId: { exact: id }}); From 86c979071ef745c7dc29c5069ff7656c875c4b2a Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Mon, 20 Apr 2020 18:07:13 +0200 Subject: [PATCH 05/24] Add aspect ratio selector for the camera stream Having tested this with a number of browser and devices, it looks like this doesn't work most of the time. Right now we request it with `ideal`, but even with `exact` it doesn't seem to make a difference. So I assume we will disable that again as otherwise users are too easily confused. However, most code in this commit is also used for the quality selector. --- src/i18n/locales/de.json | 2 + src/i18n/locales/en.json | 1 + src/ui/studio/video-setup/index.js | 61 ++++++++++++++++++++-- src/ui/studio/video-setup/preview.js | 76 ++++++++++++++++++++-------- src/util.js | 3 ++ 5 files changed, 119 insertions(+), 24 deletions(-) diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 86854926..952e8f0d 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -42,6 +42,8 @@ "sources-scenario-user": "Kamera", "sources-display": "Bildschirm", "sources-user": "Kamera", + "sources-video-device": "Kamera", + "sources-video-aspect-ratio-auto": "auto", "source-display-not-allowed-title": "Zugriff auf Bildschirm nicht möglich", "source-display-not-allowed-text": "Versuchen Sie, die Quelle neu auszuwählen. Falls das nicht funkioniert, laden Sie entweder diese Seite neu oder erlauben Sie dieser Seite manuell Zugriff auf Ihren Bildschirm (typischerweise über einen Button neben der Adressleiste Ihres Browsers)", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 6b4829e2..12328a73 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -47,6 +47,7 @@ "sources-video-reselect-source": "Reselect source(s)", "sources-video-no-cam-detected": "No camera detected", "sources-video-device": "Device", + "sources-video-aspect-ratio-auto": "auto", "sources-audio-question": "Record Audio?", "sources-audio-microphone": "Microphone", diff --git a/src/ui/studio/video-setup/index.js b/src/ui/studio/video-setup/index.js index 26bfca88..98c9aa25 100644 --- a/src/ui/studio/video-setup/index.js +++ b/src/ui/studio/video-setup/index.js @@ -5,7 +5,7 @@ import { jsx } from 'theme-ui'; import { faChalkboard, faChalkboardTeacher, faUser } from '@fortawesome/free-solid-svg-icons'; import { Container, Flex, Heading, Text } from '@theme-ui/components'; import { Styled } from 'theme-ui'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -18,6 +18,7 @@ import { VIDEO_SOURCE_NONE, } from '../../../studio-state'; import { useSettings } from '../../../settings'; +import { deviceIdOf } from '../../../util'; import Notification from '../../notification'; @@ -31,7 +32,8 @@ import { ActionButtons } from '../elements'; import { SourcePreview } from './preview'; -export const LAST_VIDEO_DEVICE_KEY = 'ocStudioLastVideoDevice'; +const LAST_VIDEO_DEVICE_KEY = 'ocStudioLastVideoDevice'; +const CAMERA_ASPECT_RATIO_KEY = 'ocStudioCameraAspectRatio'; export default function VideoSetup(props) { const { t } = useTranslation(); @@ -53,8 +55,57 @@ export default function VideoSetup(props) { const setActiveSource = s => dispatch({ type: 'CHOOSE_VIDEO', payload: s }); - const lastDeviceId = window.localStorage.getItem(LAST_VIDEO_DEVICE_KEY); - const userConstraints = lastDeviceId ? { deviceId: { ideal: lastDeviceId }} : {}; + // Handle user preferences regarding the camera stream. Defaults are loaded + // from local storage. + const [cameraPreferences, setVideoPreferences] = useState({ + deviceId: window.localStorage.getItem(LAST_VIDEO_DEVICE_KEY), + aspectRatio: window.localStorage.getItem(CAMERA_ASPECT_RATIO_KEY) || 'auto', + }); + + // Creates a valid constraints object from the user preferences. The mapping + // is as follows: + // + // - deviceId: falsy (-> ignored) or string (-> passed on) + // - aspectRatio: '16:9' or '4:3' (-> passed on), ignored on any other value + const prefsToConstraints = prefs => ({ + ...(prefs.deviceId && { deviceId: { ideal: prefs.deviceId }}), + ...(() => { + switch (prefs.aspectRatio) { + case '4:3': return { aspectRatio: { ideal: 4 / 3 }}; + case '16:9': return { aspectRatio: { ideal: 16 / 9 }}; + default: return {}; + } + })(), + }); + const userConstraints = prefsToConstraints(cameraPreferences); + + // Callback passed to the components that allow the user to change the + // preferences. `newPrefs` is merged into the existing preferences, the + // resulting preferences are set and the webcam stream is re-requested with + // the updated constraints. + const updateCameraPrefs = newPrefs => { + // Merge and update preferences. + const merged = { ...cameraPreferences, ...newPrefs }; + const constraints = prefsToConstraints(merged); + setVideoPreferences(merged); + + // Update preferences in local storage. We don't update the device ID + // though, as that is done below in the `useEffect` invocation. + window.localStorage.setItem(CAMERA_ASPECT_RATIO_KEY, merged.aspectRatio); + + // Apply new preferences by rerequesting camera stream. + stopUserCapture(userStream, dispatch); + startUserCapture(dispatch, settings, constraints); + }; + + // Store the camera device ID in local storage. We do this here, as we also + // want to remember the device the user initially selected. + useEffect(() => { + const cameraDeviceId = deviceIdOf(userStream); + if (cameraDeviceId) { + window.localStorage.setItem(LAST_VIDEO_DEVICE_KEY, cameraDeviceId); + } + }); const clickUser = async () => { setActiveSource(VIDEO_SOURCE_USER); @@ -171,6 +222,7 @@ export default function VideoSetup(props) { allowed: state.userAllowed, unexpectedEnd: state.userUnexpectedEnd, }]} + {...{ updateCameraPrefs, cameraPreferences }} />; break; @@ -208,6 +260,7 @@ export default function VideoSetup(props) { unexpectedEnd: state.userUnexpectedEnd, }, ]} + {...{ updateCameraPrefs, cameraPreferences }} />; break; default: diff --git a/src/ui/studio/video-setup/preview.js b/src/ui/studio/video-setup/preview.js index c472e97f..cf6fedd8 100644 --- a/src/ui/studio/video-setup/preview.js +++ b/src/ui/studio/video-setup/preview.js @@ -9,7 +9,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faExclamationTriangle, faTimes, faCog } from '@fortawesome/free-solid-svg-icons'; import { VideoBox, useVideoBoxResize } from '../elements.js'; -import { dimensionsOf } from '../../../util.js'; +import { deviceIdOf, dimensionsOf } from '../../../util.js'; import { useDispatch, useStudioState } from '../../../studio-state'; import { useSettings } from '../../../settings'; import { @@ -21,23 +21,23 @@ import { import { LAST_VIDEO_DEVICE_KEY } from './index.js'; -export function SourcePreview({ warnings, inputs }) { +export function SourcePreview({ warnings, inputs, updateCameraPrefs, cameraPreferences }) { let children; switch (inputs.length) { case 1: children = [{ - body: , + body: , dimensions: () => dimensionsOf(inputs[0].stream), }]; break; case 2: children = [ { - body: , + body: , dimensions: () => dimensionsOf(inputs[0].stream), }, { - body: , + body: , dimensions: () => dimensionsOf(inputs[1].stream), }, ]; @@ -54,7 +54,7 @@ export function SourcePreview({ warnings, inputs }) { ); } -function StreamPreview({ input, text }) { +function StreamPreview({ input, text, updateCameraPrefs, cameraPreferences }) { const stream = input.stream; const track = stream?.getVideoTracks()?.[0]; const { width, height } = track?.getSettings() ?? {}; @@ -62,7 +62,7 @@ function StreamPreview({ input, text }) { return ( - + ); } @@ -126,7 +126,7 @@ const PreviewVideo = ({ allowed, stream, unexpectedEnd, ...props }) => { ); }; -const StreamSettings = ({ isDesktop }) => { +const StreamSettings = ({ isDesktop, updateCameraPrefs, cameraPreferences }) => { const [isExpanded, setIsExpanded] = useState(false); return ( @@ -177,7 +177,7 @@ const StreamSettings = ({ isDesktop }) => {
- { !isDesktop && } + { !isDesktop && }
blabla @@ -187,13 +187,13 @@ const StreamSettings = ({ isDesktop }) => { ); }; -const UserSettings = () => { +const UserSettings = ({ updateCameraPrefs, cameraPreferences }) => { const { t } = useTranslation(); const dispatch = useDispatch(); const settings = useSettings(); const state = useStudioState(); - const currentDeviceId = state.userStream?.getVideoTracks()?.[0]?.getSettings()?.deviceId; + const currentDeviceId = deviceIdOf(state.userStream); let devices = []; for (const d of state.mediaDevices) { // Only intersted in video inputs @@ -209,15 +209,35 @@ const UserSettings = () => { devices.push(d); } - useEffect(() => { - if (currentDeviceId) { - window.localStorage.setItem(LAST_VIDEO_DEVICE_KEY, currentDeviceId); - } - }); + const changeDevice = id => updateCameraPrefs({ deviceId: id }); + const changeAspectRatio = ratio => updateCameraPrefs({ aspectRatio: ratio }); + - const changeDevice = id => { - stopUserCapture(state.userStream, dispatch); - startUserCapture(dispatch, settings, { deviceId: { exact: id }}); + const ArRadioBox = ({ id, value, checked }) => { + return + changeAspectRatio(e.target.value)} + sx={{ + display: 'none', + '&+label': { + border: theme => `2px solid ${theme.colors.gray[0]}`, + p: '1px 4px', + borderRadius: '6px', + mx: 1, + }, + '&:checked+label': { + bg: 'gray.0', + color: 'white', + }, + }} + /> + + ; }; return @@ -239,7 +259,23 @@ const UserSettings = () => { Aspect ratio: - TODO + + cameraPreferences.aspectRatio !== x)} + /> + + + ; }; diff --git a/src/util.js b/src/util.js index 73e39d79..ffff3c6f 100644 --- a/src/util.js +++ b/src/util.js @@ -42,6 +42,9 @@ export const dimensionsOf = stream => { return [width, height]; }; +// Returns the devide ID of the video track of the given stream. +export const deviceIdOf = stream => stream?.getVideoTracks()?.[0]?.getSettings()?.deviceId; + // Converts the MIME type into a file extension. export const mimeToExt = mime => { if (mime) { From 5e1537f6347dc346b82308bdbc242810ca0c36ac Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Mon, 20 Apr 2020 18:27:46 +0200 Subject: [PATCH 06/24] Restructure and cleanup video source setup code --- src/ui/studio/video-setup/index.js | 211 +++++++++++++-------------- src/ui/studio/video-setup/preview.js | 127 ++++++++-------- 2 files changed, 161 insertions(+), 177 deletions(-) diff --git a/src/ui/studio/video-setup/index.js b/src/ui/studio/video-setup/index.js index 98c9aa25..67a6609e 100644 --- a/src/ui/studio/video-setup/index.js +++ b/src/ui/studio/video-setup/index.js @@ -35,28 +35,20 @@ import { SourcePreview } from './preview'; const LAST_VIDEO_DEVICE_KEY = 'ocStudioLastVideoDevice'; const CAMERA_ASPECT_RATIO_KEY = 'ocStudioCameraAspectRatio'; -export default function VideoSetup(props) { + +export default function VideoSetup({ nextStep, userHasWebcam }) { const { t } = useTranslation(); const settings = useSettings(); const dispatch = useDispatch(); const state = useStudioState(); - const { - displayStream, - userStream, - displaySupported, - userSupported, - videoChoice: activeSource, - } = state; - + const { displayStream, userStream, videoChoice: activeSource } = state; const hasStreams = displayStream || userStream; - const anySupported = displaySupported || userSupported; - const bothSupported = displaySupported && userSupported; - const setActiveSource = s => dispatch({ type: 'CHOOSE_VIDEO', payload: s }); // Handle user preferences regarding the camera stream. Defaults are loaded - // from local storage. + // from local storage. That's also why we don't need to uplift this into + // studio state. const [cameraPreferences, setVideoPreferences] = useState({ deviceId: window.localStorage.getItem(LAST_VIDEO_DEVICE_KEY), aspectRatio: window.localStorage.getItem(CAMERA_ASPECT_RATIO_KEY) || 'auto', @@ -107,31 +99,12 @@ export default function VideoSetup(props) { } }); - const clickUser = async () => { - setActiveSource(VIDEO_SOURCE_USER); - await startUserCapture(dispatch, settings, userConstraints); - await queryMediaDevices(dispatch); - }; - const clickDisplay = async () => { - setActiveSource(VIDEO_SOURCE_DISPLAY); - await startDisplayCapture(dispatch, settings); - }; - const clickBoth = async () => { - setActiveSource(VIDEO_SOURCE_BOTH); - await startUserCapture(dispatch, settings, userConstraints); - await Promise.all([ - queryMediaDevices(dispatch), - startDisplayCapture(dispatch, settings), - ]); - }; - const reselectSource = () => { setActiveSource(VIDEO_SOURCE_NONE); stopUserCapture(state.userStream, dispatch); stopDisplayCapture(state.displayStream, dispatch); }; - const userHasWebcam = props.userHasWebcam; const nextDisabled = activeSource === VIDEO_SOURCE_NONE || activeSource === VIDEO_SOURCE_BOTH ? (!displayStream || !userStream) : !hasStreams; @@ -163,52 +136,28 @@ export default function VideoSetup(props) { let hideActionButtons; let title; let body; + + const userInput = { + isDesktop: false, + stream: state.userStream, + allowed: state.userAllowed, + prefs: cameraPreferences, + updatePrefs: updateCameraPrefs, + unexpectedEnd: state.userUnexpectedEnd, + }; + const displayInput = { + isDesktop: true, + stream: state.displayStream, + allowed: state.displayAllowed, + prefs: null, // TODO + updatePrefs: null, // TODO + unexpectedEnd: state.displayUnexpectedEnd, + }; switch (activeSource) { case VIDEO_SOURCE_NONE: title = t('sources-video-question'); hideActionButtons = true; - if (anySupported) { - body = - - :not(:last-of-type)': { - mb: [3, 0], - mr: [0, 3], - }, - }} - > - { displaySupported && } - { bothSupported && } - { userSupported && } - - - ; - } else { - body = {t('sources-video-none-available')}; - } + body = ; break; case VIDEO_SOURCE_USER: @@ -216,13 +165,7 @@ export default function VideoSetup(props) { hideActionButtons = !state.userStream && state.userAllowed !== false; body = ; break; @@ -231,12 +174,7 @@ export default function VideoSetup(props) { hideActionButtons = !state.displayStream && state.displayAllowed !== false; body = ; break; @@ -246,25 +184,12 @@ export default function VideoSetup(props) { || (!state.displayStream && state.displayAllowed !== false); body = ; break; default: - return

Something went very wrong

; + console.error('bug: active source has an unexpected value'); + return

Something went very wrong (internal error) :-(

; }; const hideReselectSource = hideActionButtons @@ -280,7 +205,7 @@ export default function VideoSetup(props) { }} > - {title} + { title } { body } @@ -288,20 +213,88 @@ export default function VideoSetup(props) { { activeSource !== VIDEO_SOURCE_NONE &&
} { activeSource !== VIDEO_SOURCE_NONE && props.nextStep(), - disabled: nextDisabled, - }} + next={hideActionButtons ? null : { onClick: () => nextStep(), disabled: nextDisabled }} prev={hideReselectSource ? null : { onClick: reselectSource, disabled: false, label: 'sources-video-reselect-source', }} - />} + /> } ); } +const SourceSelection = ({ setActiveSource, userConstraints, userHasWebcam }) => { + const { t } = useTranslation(); + + const settings = useSettings(); + const dispatch = useDispatch(); + const state = useStudioState(); + const { displaySupported, userSupported } = state; + + const clickUser = async () => { + setActiveSource(VIDEO_SOURCE_USER); + await startUserCapture(dispatch, settings, userConstraints); + await queryMediaDevices(dispatch); + }; + const clickDisplay = async () => { + setActiveSource(VIDEO_SOURCE_DISPLAY); + await startDisplayCapture(dispatch, settings); + }; + const clickBoth = async () => { + setActiveSource(VIDEO_SOURCE_BOTH); + await startUserCapture(dispatch, settings, userConstraints); + await Promise.all([ + queryMediaDevices(dispatch), + startDisplayCapture(dispatch, settings), + ]); + }; + + + if (!displaySupported && !userSupported) { + return {t('sources-video-none-available')}; + } + + return + + :not(:last-of-type)': { + mb: [3, 0], + mr: [0, 3], + }, + }} + > + { displaySupported && } + { displaySupported && userSupported && } + { userSupported && } + + + ; +}; + const OptionButton = ({ icon, label, onClick, disabledText = false }) => { const disabled = disabledText !== false; diff --git a/src/ui/studio/video-setup/preview.js b/src/ui/studio/video-setup/preview.js index cf6fedd8..c8dc48ee 100644 --- a/src/ui/studio/video-setup/preview.js +++ b/src/ui/studio/video-setup/preview.js @@ -4,40 +4,34 @@ import { jsx } from 'theme-ui'; import { Fragment, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Card, Text, Spinner } from '@theme-ui/components'; +import { Card, Spinner } from '@theme-ui/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faExclamationTriangle, faTimes, faCog } from '@fortawesome/free-solid-svg-icons'; import { VideoBox, useVideoBoxResize } from '../elements.js'; import { deviceIdOf, dimensionsOf } from '../../../util.js'; -import { useDispatch, useStudioState } from '../../../studio-state'; -import { useSettings } from '../../../settings'; -import { - startDisplayCapture, - startUserCapture, - stopDisplayCapture, - stopUserCapture -} from '../capturer'; -import { LAST_VIDEO_DEVICE_KEY } from './index.js'; +import { useStudioState } from '../../../studio-state'; -export function SourcePreview({ warnings, inputs, updateCameraPrefs, cameraPreferences }) { +// Shows the preview for one or two input streams. The previews also show +// preferences allowing the user to change the webcam and the like. +export const SourcePreview = ({ warnings, inputs }) => { let children; switch (inputs.length) { case 1: children = [{ - body: , + body: , dimensions: () => dimensionsOf(inputs[0].stream), }]; break; case 2: children = [ { - body: , + body: , dimensions: () => dimensionsOf(inputs[0].stream), }, { - body: , + body: , dimensions: () => dimensionsOf(inputs[1].stream), }, ]; @@ -54,20 +48,15 @@ export function SourcePreview({ warnings, inputs, updateCameraPrefs, cameraPrefe ); } -function StreamPreview({ input, text, updateCameraPrefs, cameraPreferences }) { - const stream = input.stream; - const track = stream?.getVideoTracks()?.[0]; - const { width, height } = track?.getSettings() ?? {}; +const StreamPreview = ({ input, text }) => ( + + + + +); - return ( - - - - - ); -} - -const PreviewVideo = ({ allowed, stream, unexpectedEnd, ...props }) => { +const PreviewVideo = ({ input }) => { + const { allowed, stream, unexpectedEnd } = input; const resizeVideoBox = useVideoBoxResize(); const videoRef = useRef(); @@ -126,7 +115,8 @@ const PreviewVideo = ({ allowed, stream, unexpectedEnd, ...props }) => { ); }; -const StreamSettings = ({ isDesktop, updateCameraPrefs, cameraPreferences }) => { +const StreamSettings = ({ input }) => { + const { isDesktop, updatePrefs, prefs } = input; const [isExpanded, setIsExpanded] = useState(false); return ( @@ -177,7 +167,7 @@ const StreamSettings = ({ isDesktop, updateCameraPrefs, cameraPreferences }) =>
- { !isDesktop && } + { !isDesktop && }
blabla @@ -187,10 +177,8 @@ const StreamSettings = ({ isDesktop, updateCameraPrefs, cameraPreferences }) => ); }; -const UserSettings = ({ updateCameraPrefs, cameraPreferences }) => { +const UserSettings = ({ updatePrefs, prefs }) => { const { t } = useTranslation(); - const dispatch = useDispatch(); - const settings = useSettings(); const state = useStudioState(); const currentDeviceId = deviceIdOf(state.userStream); @@ -209,36 +197,8 @@ const UserSettings = ({ updateCameraPrefs, cameraPreferences }) => { devices.push(d); } - const changeDevice = id => updateCameraPrefs({ deviceId: id }); - const changeAspectRatio = ratio => updateCameraPrefs({ aspectRatio: ratio }); - - - const ArRadioBox = ({ id, value, checked }) => { - return - changeAspectRatio(e.target.value)} - sx={{ - display: 'none', - '&+label': { - border: theme => `2px solid ${theme.colors.gray[0]}`, - p: '1px 4px', - borderRadius: '6px', - mx: 1, - }, - '&:checked+label': { - bg: 'gray.0', - color: 'white', - }, - }} - /> - - ; - }; + const changeDevice = id => updatePrefs({ deviceId: id }); + const changeAspectRatio = ratio => updatePrefs({ aspectRatio: ratio }); return @@ -260,22 +220,53 @@ const UserSettings = ({ updateCameraPrefs, cameraPreferences }) => { Aspect ratio: - cameraPreferences.aspectRatio !== x)} + name="aspectRatio" + onChange={changeAspectRatio} + checked={['4:3', '16:9'].every(x => prefs.aspectRatio !== x)} /> - - ; }; + +// A styled radio input which looks like a button. +const RadioButton = ({ id, value, checked, name, onChange }) => { + return + onChange(e.target.value)} + {...{ id, value, checked, name }} + sx={{ + display: 'none', + '&+label': { + border: theme => `2px solid ${theme.colors.gray[0]}`, + p: '1px 4px', + borderRadius: '6px', + mx: 1, + }, + '&:checked+label': { + bg: 'gray.0', + color: 'white', + }, + }} + /> + + ; +}; From a39e0469b9154f007d0ad6e6cf601ecb711fa257 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Tue, 21 Apr 2020 11:25:08 +0200 Subject: [PATCH 07/24] Fix aspect ratio translation --- src/i18n/locales/de.json | 1 + src/i18n/locales/en.json | 1 + src/ui/studio/video-setup/preview.js | 7 ++++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 952e8f0d..ce7dcb12 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -43,6 +43,7 @@ "sources-display": "Bildschirm", "sources-user": "Kamera", "sources-video-device": "Kamera", + "sources-video-aspect-ratio": "Seitenverhältnis", "sources-video-aspect-ratio-auto": "auto", "source-display-not-allowed-title": "Zugriff auf Bildschirm nicht möglich", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 12328a73..281f5202 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -47,6 +47,7 @@ "sources-video-reselect-source": "Reselect source(s)", "sources-video-no-cam-detected": "No camera detected", "sources-video-device": "Device", + "sources-video-aspect-ratio": "Aspect ratio", "sources-video-aspect-ratio-auto": "auto", "sources-audio-question": "Record Audio?", diff --git a/src/ui/studio/video-setup/preview.js b/src/ui/studio/video-setup/preview.js index c8dc48ee..4b4ec078 100644 --- a/src/ui/studio/video-setup/preview.js +++ b/src/ui/studio/video-setup/preview.js @@ -218,12 +218,13 @@ const UserSettings = ({ updatePrefs, prefs }) => { - Aspect ratio: + {t('sources-video-aspect-ratio')}: prefs.aspectRatio !== x)} /> @@ -247,7 +248,7 @@ const UserSettings = ({ updatePrefs, prefs }) => { }; // A styled radio input which looks like a button. -const RadioButton = ({ id, value, checked, name, onChange }) => { +const RadioButton = ({ id, value, checked, name, onChange, label }) => { return { }, }} /> - + ; }; From b5ff419b89c4fcc90746ce1d3336adbe8f4ac553 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Tue, 21 Apr 2020 13:30:47 +0200 Subject: [PATCH 08/24] Replace fixed magic height by actual height of preferences box --- src/ui/studio/video-setup/preview.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ui/studio/video-setup/preview.js b/src/ui/studio/video-setup/preview.js index 4b4ec078..b5ea2998 100644 --- a/src/ui/studio/video-setup/preview.js +++ b/src/ui/studio/video-setup/preview.js @@ -118,6 +118,7 @@ const PreviewVideo = ({ input }) => { const StreamSettings = ({ input }) => { const { isDesktop, updatePrefs, prefs } = input; const [isExpanded, setIsExpanded] = useState(false); + const expandedHeight = useRef(null); return (
{
-
+
{ if (r) { expandedHeight.current = `${r.offsetHeight}px`; } }} + sx={{ p: 1, border: theme => `1px solid ${theme.colors.gray[0]}` }} + > { !isDesktop && } From a1d009435fd557843a58a11a0b3a644e000b2f06 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Tue, 21 Apr 2020 15:07:16 +0200 Subject: [PATCH 09/24] Add current stream information if the video preferences are shown --- src/ui/studio/video-setup/preview.js | 32 ++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/ui/studio/video-setup/preview.js b/src/ui/studio/video-setup/preview.js index b5ea2998..9a6c963c 100644 --- a/src/ui/studio/video-setup/preview.js +++ b/src/ui/studio/video-setup/preview.js @@ -49,7 +49,7 @@ export const SourcePreview = ({ warnings, inputs }) => { } const StreamPreview = ({ input, text }) => ( - + @@ -120,7 +120,24 @@ const StreamSettings = ({ input }) => { const [isExpanded, setIsExpanded] = useState(false); const expandedHeight = useRef(null); - return ( + return +
+ + + +
{ { !isDesktop && }
- blabla
- ); + ; +}; + +const StreamInfo = ({ stream }) => { + const s = stream?.getVideoTracks()?.[0]?.getSettings(); + const sizeInfo = (s && s.width && s.height) ? `${s.width}×${s.height}` : ''; + const fpsInfo = (s && s.frameRate) ? `${s.frameRate} fps` : ''; + + return s ? [sizeInfo, fpsInfo].join(', ') : '...'; }; const UserSettings = ({ updatePrefs, prefs }) => { From 64aaf91cc7902ca23a1f5442beaefbbc8f4e3611 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Tue, 21 Apr 2020 18:03:58 +0200 Subject: [PATCH 10/24] Add quality preferences for both video streams This feature is still not finished. Open tasks/questions: - Should the setting passed as `ideal`? Firefox does some strange stuff with this. Passing as `exact` doesn't work with desktop capture in Firefox or Chrome. We could pass it as `max` and/or `min` but... that seems wrong. - We need to check the `maxHeight` in settings. Still unclear how we communicate this to the user. - It would be beneficial if you could somehow check which quality settings won't work. And/or better signal to the user if a quality setting worked. --- src/i18n/locales/de.json | 2 ++ src/i18n/locales/en.json | 2 ++ src/studio-state.js | 1 - src/ui/studio/capturer.js | 3 +- src/ui/studio/video-setup/index.js | 52 ++++++++++++++++++++++------ src/ui/studio/video-setup/preview.js | 36 ++++++++++++++++++- 6 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index ce7dcb12..a1a7f44a 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -45,6 +45,8 @@ "sources-video-device": "Kamera", "sources-video-aspect-ratio": "Seitenverhältnis", "sources-video-aspect-ratio-auto": "auto", + "sources-video-quality": "Qualität", + "sources-video-quality-auto": "auto", "source-display-not-allowed-title": "Zugriff auf Bildschirm nicht möglich", "source-display-not-allowed-text": "Versuchen Sie, die Quelle neu auszuwählen. Falls das nicht funkioniert, laden Sie entweder diese Seite neu oder erlauben Sie dieser Seite manuell Zugriff auf Ihren Bildschirm (typischerweise über einen Button neben der Adressleiste Ihres Browsers)", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 281f5202..71f9413f 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -49,6 +49,8 @@ "sources-video-device": "Device", "sources-video-aspect-ratio": "Aspect ratio", "sources-video-aspect-ratio-auto": "auto", + "sources-video-quality": "Quality", + "sources-video-quality-auto": "auto", "sources-audio-question": "Record Audio?", "sources-audio-microphone": "Microphone", diff --git a/src/studio-state.js b/src/studio-state.js index 8b77d8ff..14175889 100644 --- a/src/studio-state.js +++ b/src/studio-state.js @@ -60,7 +60,6 @@ const initialState = () => ({ const reducer = (state, action) => { switch (action.type) { case 'UPDATE_MEDIA_DEVICES': - console.log(action.payload); return { ...state, mediaDevices: action.payload }; case 'CHOOSE_AUDIO': diff --git a/src/ui/studio/capturer.js b/src/ui/studio/capturer.js index a33d4aaa..1b96793d 100644 --- a/src/ui/studio/capturer.js +++ b/src/ui/studio/capturer.js @@ -21,7 +21,7 @@ export async function startAudioCapture(dispatch, deviceId = null) { } } -export async function startDisplayCapture(dispatch, settings) { +export async function startDisplayCapture(dispatch, settings, videoConstraints = {}) { const maxFps = settings.display?.maxFps ? { frameRate: { max: settings.display.maxFps } } : {}; @@ -34,6 +34,7 @@ export async function startDisplayCapture(dispatch, settings) { cursor: 'always', ...maxFps, ...maxHeight, + ...videoConstraints }, audio: false, }; diff --git a/src/ui/studio/video-setup/index.js b/src/ui/studio/video-setup/index.js index 67a6609e..552d5c6f 100644 --- a/src/ui/studio/video-setup/index.js +++ b/src/ui/studio/video-setup/index.js @@ -34,7 +34,8 @@ import { SourcePreview } from './preview'; const LAST_VIDEO_DEVICE_KEY = 'ocStudioLastVideoDevice'; const CAMERA_ASPECT_RATIO_KEY = 'ocStudioCameraAspectRatio'; - +const CAMERA_QUALITY_KEY = 'ocStudioCameraQuality'; +const DISPLAY_QUALITY_KEY = 'ocStudioDisplayQuality'; export default function VideoSetup({ nextStep, userHasWebcam }) { const { t } = useTranslation(); @@ -49,9 +50,14 @@ export default function VideoSetup({ nextStep, userHasWebcam }) { // Handle user preferences regarding the camera stream. Defaults are loaded // from local storage. That's also why we don't need to uplift this into // studio state. - const [cameraPreferences, setVideoPreferences] = useState({ + const [cameraPreferences, setCameraPreferences] = useState({ deviceId: window.localStorage.getItem(LAST_VIDEO_DEVICE_KEY), aspectRatio: window.localStorage.getItem(CAMERA_ASPECT_RATIO_KEY) || 'auto', + quality: window.localStorage.getItem(CAMERA_QUALITY_KEY) || 'auto', + }); + + const [displayPreferences, setDisplayPreferences] = useState({ + quality: window.localStorage.getItem(DISPLAY_QUALITY_KEY) || 'auto', }); // Creates a valid constraints object from the user preferences. The mapping @@ -68,8 +74,12 @@ export default function VideoSetup({ nextStep, userHasWebcam }) { default: return {}; } })(), + ...(() => { + const mapping = { '480p': 480, '720p': 720, '1080p': 1080, '1440p': 1440, '2160p': 2160 }; + const height = mapping[prefs.quality]; + return height ? { height: { ideal: height }} : {}; + })(), }); - const userConstraints = prefsToConstraints(cameraPreferences); // Callback passed to the components that allow the user to change the // preferences. `newPrefs` is merged into the existing preferences, the @@ -79,11 +89,12 @@ export default function VideoSetup({ nextStep, userHasWebcam }) { // Merge and update preferences. const merged = { ...cameraPreferences, ...newPrefs }; const constraints = prefsToConstraints(merged); - setVideoPreferences(merged); + setCameraPreferences(merged); // Update preferences in local storage. We don't update the device ID // though, as that is done below in the `useEffect` invocation. window.localStorage.setItem(CAMERA_ASPECT_RATIO_KEY, merged.aspectRatio); + window.localStorage.setItem(CAMERA_QUALITY_KEY, merged.quality); // Apply new preferences by rerequesting camera stream. stopUserCapture(userStream, dispatch); @@ -99,6 +110,20 @@ export default function VideoSetup({ nextStep, userHasWebcam }) { } }); + const updateDisplayPrefs = newPrefs => { + // Merge and update preferences. + const merged = { ...displayPreferences, ...newPrefs }; + const constraints = prefsToConstraints(merged); + setDisplayPreferences(merged); + + // Update preferences in local storage. + window.localStorage.setItem(DISPLAY_QUALITY_KEY, merged.quality); + + // Apply new preferences by rerequesting display stream. + stopDisplayCapture(displayStream, dispatch); + startDisplayCapture(dispatch, settings, constraints); + }; + const reselectSource = () => { setActiveSource(VIDEO_SOURCE_NONE); stopUserCapture(state.userStream, dispatch); @@ -149,15 +174,22 @@ export default function VideoSetup({ nextStep, userHasWebcam }) { isDesktop: true, stream: state.displayStream, allowed: state.displayAllowed, - prefs: null, // TODO - updatePrefs: null, // TODO + prefs: displayPreferences, + updatePrefs: updateDisplayPrefs, unexpectedEnd: state.displayUnexpectedEnd, }; switch (activeSource) { case VIDEO_SOURCE_NONE: + const userConstraints = prefsToConstraints(cameraPreferences); + const displayConstraints = prefsToConstraints(displayPreferences); title = t('sources-video-question'); hideActionButtons = true; - body = ; + body = ; break; case VIDEO_SOURCE_USER: @@ -224,7 +256,7 @@ export default function VideoSetup({ nextStep, userHasWebcam }) { ); } -const SourceSelection = ({ setActiveSource, userConstraints, userHasWebcam }) => { +const SourceSelection = ({ setActiveSource, userConstraints, displayConstraints, userHasWebcam }) => { const { t } = useTranslation(); const settings = useSettings(); @@ -239,14 +271,14 @@ const SourceSelection = ({ setActiveSource, userConstraints, userHasWebcam }) => }; const clickDisplay = async () => { setActiveSource(VIDEO_SOURCE_DISPLAY); - await startDisplayCapture(dispatch, settings); + await startDisplayCapture(dispatch, settings, displayConstraints); }; const clickBoth = async () => { setActiveSource(VIDEO_SOURCE_BOTH); await startUserCapture(dispatch, settings, userConstraints); await Promise.all([ queryMediaDevices(dispatch), - startDisplayCapture(dispatch, settings), + startDisplayCapture(dispatch, settings, displayConstraints), ]); }; diff --git a/src/ui/studio/video-setup/preview.js b/src/ui/studio/video-setup/preview.js index 9a6c963c..897ac82f 100644 --- a/src/ui/studio/video-setup/preview.js +++ b/src/ui/studio/video-setup/preview.js @@ -181,7 +181,6 @@ const StreamSettings = ({ input }) => { overflow: 'hidden', backgroundColor: 'rgba(255, 255, 255, 0.95)', fontSize: '18px', - }}>
{ { !isDesktop && } +
@@ -207,6 +207,40 @@ const StreamInfo = ({ stream }) => { return s ? [sizeInfo, fpsInfo].join(', ') : '...'; }; +const UniveralSettings = ({ isDesktop, updatePrefs, prefs }) => { + const { t } = useTranslation(); + + const changeQuality = quality => updatePrefs({ quality }); + const qualities = ['480p', '720p', '1080p', '1440p', '2160p']; + const kind = isDesktop ? 'desktop' : 'user'; + + return + + {t('sources-video-quality')}: + + prefs.quality !== q)} + /> + { + qualities.map(q => ) + } + + + ; +}; + const UserSettings = ({ updatePrefs, prefs }) => { const { t } = useTranslation(); const state = useStudioState(); From 9e2493a3755d2c3edd08e78a09dd8f92a6d37de9 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Tue, 21 Apr 2020 18:57:39 +0200 Subject: [PATCH 11/24] Provide some feedback to the user whether a video preference worked When requesting specific aspect ratios or resolutions, sometimes the browser can't stick to that request and returns a video stream ignoring those contraints. This commit colors the preference buttons green or yellow depending on whether the browser could satisfy the request. --- src/ui/studio/video-setup/preview.js | 65 +++++++++++++++++++--------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/src/ui/studio/video-setup/preview.js b/src/ui/studio/video-setup/preview.js index 897ac82f..00ae75eb 100644 --- a/src/ui/studio/video-setup/preview.js +++ b/src/ui/studio/video-setup/preview.js @@ -116,7 +116,7 @@ const PreviewVideo = ({ input }) => { }; const StreamSettings = ({ input }) => { - const { isDesktop, updatePrefs, prefs } = input; + const { isDesktop, updatePrefs, prefs, stream } = input; const [isExpanded, setIsExpanded] = useState(false); const expandedHeight = useRef(null); @@ -190,7 +190,7 @@ const StreamSettings = ({ input }) => { { !isDesktop && } - +
@@ -207,13 +207,20 @@ const StreamInfo = ({ stream }) => { return s ? [sizeInfo, fpsInfo].join(', ') : '...'; }; -const UniveralSettings = ({ isDesktop, updatePrefs, prefs }) => { +const UniveralSettings = ({ isDesktop, updatePrefs, prefs, stream }) => { const { t } = useTranslation(); const changeQuality = quality => updatePrefs({ quality }); const qualities = ['480p', '720p', '1080p', '1440p', '2160p']; const kind = isDesktop ? 'desktop' : 'user'; + const [, currentHeight] = dimensionsOf(stream); + let fitState; + if (currentHeight && qualities.includes(prefs.quality)) { + const expectedHeight = parseInt(prefs.quality); + fitState = expectedHeight === currentHeight ? 'ok': 'warn'; + } + return {t('sources-video-quality')}: @@ -234,6 +241,7 @@ const UniveralSettings = ({ isDesktop, updatePrefs, prefs }) => { name={`quality-${kind}`} onChange={changeQuality} checked={prefs.quality === q} + state={fitState} />) } @@ -261,6 +269,21 @@ const UserSettings = ({ updatePrefs, prefs }) => { devices.push(d); } + const ars = ['4:3', '16:9']; + const [width, height] = dimensionsOf(state.userStream); + let arState; + if (width && height && ars.includes(prefs.aspectRatio)) { + const currentAr = width / height; + const [w, h] = prefs.aspectRatio.split(':'); + const expectedAr = Number(w) / Number(h) + + // We have some range we accept as "good". You never know with these + // floats... + arState = (expectedAr * 0.97 < currentAr && currentAr < expectedAr / 0.97) + ? 'ok' + : 'warn'; + } + const changeDevice = id => updatePrefs({ deviceId: id }); const changeAspectRatio = ratio => updatePrefs({ aspectRatio: ratio }); @@ -290,29 +313,30 @@ const UserSettings = ({ updatePrefs, prefs }) => { name="aspectRatio" label={t('sources-video-aspect-ratio-auto')} onChange={changeAspectRatio} - checked={['4:3', '16:9'].every(x => prefs.aspectRatio !== x)} - /> - - prefs.aspectRatio !== x)} /> + { + ars.map(ar => ) + } ; }; // A styled radio input which looks like a button. -const RadioButton = ({ id, value, checked, name, onChange, label }) => { +const RadioButton = ({ id, value, checked, name, onChange, label, state }) => { + const stateColorMap = { + 'warn': '#ffe300', + 'ok': '#51d18f', + }; + return { }, '&:checked+label': { bg: 'gray.0', - color: 'white', + color: state ? stateColorMap[state] : 'white', + fontWeight: 'bold', }, }} /> From ede8c4f7fec74daa9d4817e638370b564366f6c4 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Wed, 22 Apr 2020 10:09:10 +0200 Subject: [PATCH 12/24] Fix lint warning --- src/ui/studio/video-setup/preview.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/studio/video-setup/preview.js b/src/ui/studio/video-setup/preview.js index 00ae75eb..dac5cb95 100644 --- a/src/ui/studio/video-setup/preview.js +++ b/src/ui/studio/video-setup/preview.js @@ -318,6 +318,7 @@ const UserSettings = ({ updatePrefs, prefs }) => { { ars.map(ar => Date: Wed, 22 Apr 2020 14:59:34 +0200 Subject: [PATCH 13/24] Pull code related to video stream preferences into own file This restructuring removes some duplicate code and is generally easier to understand. --- src/ui/studio/video-setup/index.js | 124 ++------- src/ui/studio/video-setup/prefs.js | 374 +++++++++++++++++++++++++++ src/ui/studio/video-setup/preview.js | 258 +----------------- 3 files changed, 397 insertions(+), 359 deletions(-) create mode 100644 src/ui/studio/video-setup/prefs.js diff --git a/src/ui/studio/video-setup/index.js b/src/ui/studio/video-setup/index.js index 552d5c6f..ad4ee68c 100644 --- a/src/ui/studio/video-setup/index.js +++ b/src/ui/studio/video-setup/index.js @@ -5,7 +5,7 @@ import { jsx } from 'theme-ui'; import { faChalkboard, faChalkboardTeacher, faUser } from '@fortawesome/free-solid-svg-icons'; import { Container, Flex, Heading, Text } from '@theme-ui/components'; import { Styled } from 'theme-ui'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -18,7 +18,6 @@ import { VIDEO_SOURCE_NONE, } from '../../../studio-state'; import { useSettings } from '../../../settings'; -import { deviceIdOf } from '../../../util'; import Notification from '../../notification'; @@ -30,107 +29,24 @@ import { } from '../capturer'; import { ActionButtons } from '../elements'; import { SourcePreview } from './preview'; +import { loadCameraPrefs, loadDisplayPrefs, prefsToConstraints } from './prefs'; -const LAST_VIDEO_DEVICE_KEY = 'ocStudioLastVideoDevice'; -const CAMERA_ASPECT_RATIO_KEY = 'ocStudioCameraAspectRatio'; -const CAMERA_QUALITY_KEY = 'ocStudioCameraQuality'; -const DISPLAY_QUALITY_KEY = 'ocStudioDisplayQuality'; - export default function VideoSetup({ nextStep, userHasWebcam }) { const { t } = useTranslation(); - const settings = useSettings(); const dispatch = useDispatch(); const state = useStudioState(); const { displayStream, userStream, videoChoice: activeSource } = state; const hasStreams = displayStream || userStream; - const setActiveSource = s => dispatch({ type: 'CHOOSE_VIDEO', payload: s }); - - // Handle user preferences regarding the camera stream. Defaults are loaded - // from local storage. That's also why we don't need to uplift this into - // studio state. - const [cameraPreferences, setCameraPreferences] = useState({ - deviceId: window.localStorage.getItem(LAST_VIDEO_DEVICE_KEY), - aspectRatio: window.localStorage.getItem(CAMERA_ASPECT_RATIO_KEY) || 'auto', - quality: window.localStorage.getItem(CAMERA_QUALITY_KEY) || 'auto', - }); - - const [displayPreferences, setDisplayPreferences] = useState({ - quality: window.localStorage.getItem(DISPLAY_QUALITY_KEY) || 'auto', - }); - - // Creates a valid constraints object from the user preferences. The mapping - // is as follows: - // - // - deviceId: falsy (-> ignored) or string (-> passed on) - // - aspectRatio: '16:9' or '4:3' (-> passed on), ignored on any other value - const prefsToConstraints = prefs => ({ - ...(prefs.deviceId && { deviceId: { ideal: prefs.deviceId }}), - ...(() => { - switch (prefs.aspectRatio) { - case '4:3': return { aspectRatio: { ideal: 4 / 3 }}; - case '16:9': return { aspectRatio: { ideal: 16 / 9 }}; - default: return {}; - } - })(), - ...(() => { - const mapping = { '480p': 480, '720p': 720, '1080p': 1080, '1440p': 1440, '2160p': 2160 }; - const height = mapping[prefs.quality]; - return height ? { height: { ideal: height }} : {}; - })(), - }); - - // Callback passed to the components that allow the user to change the - // preferences. `newPrefs` is merged into the existing preferences, the - // resulting preferences are set and the webcam stream is re-requested with - // the updated constraints. - const updateCameraPrefs = newPrefs => { - // Merge and update preferences. - const merged = { ...cameraPreferences, ...newPrefs }; - const constraints = prefsToConstraints(merged); - setCameraPreferences(merged); - - // Update preferences in local storage. We don't update the device ID - // though, as that is done below in the `useEffect` invocation. - window.localStorage.setItem(CAMERA_ASPECT_RATIO_KEY, merged.aspectRatio); - window.localStorage.setItem(CAMERA_QUALITY_KEY, merged.quality); - - // Apply new preferences by rerequesting camera stream. - stopUserCapture(userStream, dispatch); - startUserCapture(dispatch, settings, constraints); - }; - - // Store the camera device ID in local storage. We do this here, as we also - // want to remember the device the user initially selected. - useEffect(() => { - const cameraDeviceId = deviceIdOf(userStream); - if (cameraDeviceId) { - window.localStorage.setItem(LAST_VIDEO_DEVICE_KEY, cameraDeviceId); - } - }); - - const updateDisplayPrefs = newPrefs => { - // Merge and update preferences. - const merged = { ...displayPreferences, ...newPrefs }; - const constraints = prefsToConstraints(merged); - setDisplayPreferences(merged); - - // Update preferences in local storage. - window.localStorage.setItem(DISPLAY_QUALITY_KEY, merged.quality); - - // Apply new preferences by rerequesting display stream. - stopDisplayCapture(displayStream, dispatch); - startDisplayCapture(dispatch, settings, constraints); - }; + const setActiveSource = s => dispatch({ type: 'CHOOSE_VIDEO', payload: s }); const reselectSource = () => { setActiveSource(VIDEO_SOURCE_NONE); - stopUserCapture(state.userStream, dispatch); - stopDisplayCapture(state.displayStream, dispatch); + stopUserCapture(userStream, dispatch); + stopDisplayCapture(displayStream, dispatch); }; - const nextDisabled = activeSource === VIDEO_SOURCE_NONE || activeSource === VIDEO_SOURCE_BOTH ? (!displayStream || !userStream) : !hasStreams; @@ -157,31 +73,27 @@ export default function VideoSetup({ nextStep, userHasWebcam }) { ); - // The body depends on which source is currently selected. - let hideActionButtons; - let title; - let body; - const userInput = { isDesktop: false, - stream: state.userStream, + stream: userStream, allowed: state.userAllowed, - prefs: cameraPreferences, - updatePrefs: updateCameraPrefs, unexpectedEnd: state.userUnexpectedEnd, }; const displayInput = { isDesktop: true, - stream: state.displayStream, + stream: displayStream, allowed: state.displayAllowed, - prefs: displayPreferences, - updatePrefs: updateDisplayPrefs, unexpectedEnd: state.displayUnexpectedEnd, }; + + // The body depends on which source is currently selected. + let hideActionButtons; + let title; + let body; switch (activeSource) { case VIDEO_SOURCE_NONE: - const userConstraints = prefsToConstraints(cameraPreferences); - const displayConstraints = prefsToConstraints(displayPreferences); + const userConstraints = prefsToConstraints(loadCameraPrefs()); + const displayConstraints = prefsToConstraints(loadDisplayPrefs()); title = t('sources-video-question'); hideActionButtons = true; body = { + const deviceConstraint = prefs.deviceId + && { deviceId: { [exactDevice ? 'exact' : 'ideal']: prefs.deviceId }}; + + const aspectRatio = parseAspectRatio(prefs.aspectRatio); + const aspectRatioConstraint = aspectRatio && { aspectRatio: { ideal: aspectRatio }}; + + const height = parseQuality(prefs.quality); + const heightConstraint = height && { height: { ideal: height }}; + + return { + ...deviceConstraint, + ...aspectRatioConstraint, + ...heightConstraint, + }; +}; + +// All aspect ratios the user can choose from. +export const ASPECT_RATIOS = ['4:3', '16:9']; + +// Converts the given aspect ratio label (one of the elements in +// `ASPECT_RATIOS`) into the numerical ratio, e.g. 4/3 = 1.333. If the argument +// is not a valid label, `null` is returned. +export const parseAspectRatio = label => { + const mapping = { + '4:3': 4 / 3, + '16:9': 16 / 9, + }; + + return mapping[label] || null; +} + +// Converts the given quality label to the actual height as number. If the +// argument is not a valid quality label (e.g. '720p'), `null` is returned. +export const parseQuality = label => { + if (!/^[0-9]+p$/.test(label)) { + return null; + } + + return parseInt(label); +}; + +// Local storage keys +const LAST_VIDEO_DEVICE_KEY = 'ocStudioLastVideoDevice'; +const CAMERA_ASPECT_RATIO_KEY = 'ocStudioCameraAspectRatio'; +const CAMERA_QUALITY_KEY = 'ocStudioCameraQuality'; +const DISPLAY_QUALITY_KEY = 'ocStudioDisplayQuality'; + +// Loads the initial camera preferences from local storage. +export const loadCameraPrefs = () => ({ + deviceId: window.localStorage.getItem(LAST_VIDEO_DEVICE_KEY), + aspectRatio: window.localStorage.getItem(CAMERA_ASPECT_RATIO_KEY) || 'auto', + quality: window.localStorage.getItem(CAMERA_QUALITY_KEY) || 'auto', +}); + +// Loads the initial display preferences from local storage. +export const loadDisplayPrefs = () => ({ + quality: window.localStorage.getItem(DISPLAY_QUALITY_KEY) || 'auto', +}); + + +export const StreamSettings = ({ isDesktop, stream }) => { + const dispatch = useDispatch(); + const settings = useSettings(); + + // The current preferences and the callback to update them. + const prefs = isDesktop ? loadDisplayPrefs() : loadCameraPrefs(); + const updatePrefs = newPrefs => { + // Merge and update preferences. + const merged = { ...prefs, ...newPrefs }; + const constraints = prefsToConstraints(merged); + + // Update preferences in local storage and re-request stream. The latter + // will cause the rerender. + if (isDesktop) { + window.localStorage.setItem(DISPLAY_QUALITY_KEY, merged.quality); + + stopDisplayCapture(stream, dispatch); + startDisplayCapture(dispatch, settings, constraints); + } else { + window.localStorage.setItem(LAST_VIDEO_DEVICE_KEY, merged.deviceId); + window.localStorage.setItem(CAMERA_ASPECT_RATIO_KEY, merged.aspectRatio); + window.localStorage.setItem(CAMERA_QUALITY_KEY, merged.quality); + + stopUserCapture(stream, dispatch); + startUserCapture(dispatch, settings, constraints); + } + }; + + // Store the camera device ID in local storage. We also do this here, as we + // also want to remember the device the user initially selected in the browser + // popup. + useEffect(() => { + const cameraDeviceId = deviceIdOf(stream); + if (!isDesktop && cameraDeviceId) { + window.localStorage.setItem(LAST_VIDEO_DEVICE_KEY, cameraDeviceId); + } + }); + + // State about expanding and hiding the settings. + const [isExpanded, setIsExpanded] = useState(false); + const expandedHeight = useRef(null); + + return +
+ + + +
+
+
+
setIsExpanded(old => !old)} + sx={{ + display: 'inline-block', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + color: 'white', + p: '6px', + m: 2, + fontSize: '30px', + lineHeight: '1em', + borderRadius: '10px', + cursor: 'pointer', + '&:hover': { + backgroundColor: 'rgba(0, 0, 0, 0.7)', + }, + '&:hover > svg': { + transform: isExpanded ? 'none' : 'rotate(45deg)', + }, + }} + > + +
+
+
+
{ if (r) { expandedHeight.current = `${r.offsetHeight}px`; } }} + sx={{ p: 1, border: theme => `1px solid ${theme.colors.gray[0]}` }} + > + + + { !isDesktop && } + + +
+
+
+
+
; +}; + +const StreamInfo = ({ stream }) => { + const s = stream?.getVideoTracks()?.[0]?.getSettings(); + const sizeInfo = (s && s.width && s.height) ? `${s.width}×${s.height}` : ''; + const fpsInfo = (s && s.frameRate) ? `${s.frameRate} fps` : ''; + + return s ? [sizeInfo, fpsInfo].join(', ') : '...'; +}; + +const UniveralSettings = ({ isDesktop, updatePrefs, prefs, stream }) => { + const { t } = useTranslation(); + + const changeQuality = quality => updatePrefs({ quality }); + const qualities = ['480p', '720p', '1080p', '1440p', '2160p']; + const kind = isDesktop ? 'desktop' : 'user'; + + const [, currentHeight] = dimensionsOf(stream); + let fitState; + if (currentHeight && qualities.includes(prefs.quality)) { + const expectedHeight = parseInt(prefs.quality); + fitState = expectedHeight === currentHeight ? 'ok': 'warn'; + } + + return + + {t('sources-video-quality')}: + + prefs.quality !== q)} + /> + { + qualities.map(q => ) + } + + + ; +}; + +const UserSettings = ({ updatePrefs, prefs }) => { + const { t } = useTranslation(); + const state = useStudioState(); + + const currentDeviceId = deviceIdOf(state.userStream); + let devices = []; + for (const d of state.mediaDevices) { + // Only intersted in video inputs + if (d.kind !== 'videoinput') { + continue; + } + + // If we already have a device with that device ID, we ignore it. + if (devices.some(od => od.deviceId === d.deviceId)) { + continue; + } + + devices.push(d); + } + + const [width, height] = dimensionsOf(state.userStream); + let arState; + if (width && height && ASPECT_RATIOS.includes(prefs.aspectRatio)) { + const currentAr = width / height; + const expectedAr = parseAspectRatio(prefs.aspectRatio); + + // We have some range we accept as "good". You never know with these + // floats... + arState = (expectedAr * 0.97 < currentAr && currentAr < expectedAr / 0.97) + ? 'ok' + : 'warn'; + } + + const changeDevice = id => updatePrefs({ deviceId: id }); + const changeAspectRatio = ratio => updatePrefs({ aspectRatio: ratio }); + + return + + {t('sources-video-device')}: + + + + + + {t('sources-video-aspect-ratio')}: + + prefs.aspectRatio !== x)} + /> + { + ASPECT_RATIOS.map(ar => ) + } + + + ; +}; + +// A styled radio input which looks like a button. +const RadioButton = ({ id, value, checked, name, onChange, label, state }) => { + const stateColorMap = { + 'warn': '#ffe300', + 'ok': '#51d18f', + }; + + return + onChange(e.target.value)} + {...{ id, value, checked, name }} + sx={{ + display: 'none', + '&+label': { + border: theme => `2px solid ${theme.colors.gray[0]}`, + p: '1px 4px', + borderRadius: '6px', + mx: 1, + }, + '&:checked+label': { + bg: 'gray.0', + color: state ? stateColorMap[state] : 'white', + fontWeight: 'bold', + }, + }} + /> + + ; +}; diff --git a/src/ui/studio/video-setup/preview.js b/src/ui/studio/video-setup/preview.js index dac5cb95..b40da25b 100644 --- a/src/ui/studio/video-setup/preview.js +++ b/src/ui/studio/video-setup/preview.js @@ -2,15 +2,14 @@ /** @jsx jsx */ import { jsx } from 'theme-ui'; -import { Fragment, useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { Fragment, useEffect, useRef } from 'react'; import { Card, Spinner } from '@theme-ui/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faExclamationTriangle, faTimes, faCog } from '@fortawesome/free-solid-svg-icons'; +import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { VideoBox, useVideoBoxResize } from '../elements.js'; -import { deviceIdOf, dimensionsOf } from '../../../util.js'; -import { useStudioState } from '../../../studio-state'; +import { dimensionsOf } from '../../../util.js'; +import { StreamSettings } from './prefs'; // Shows the preview for one or two input streams. The previews also show @@ -51,7 +50,7 @@ export const SourcePreview = ({ warnings, inputs }) => { const StreamPreview = ({ input, text }) => ( - + ); @@ -114,250 +113,3 @@ const PreviewVideo = ({ input }) => { /> ); }; - -const StreamSettings = ({ input }) => { - const { isDesktop, updatePrefs, prefs, stream } = input; - const [isExpanded, setIsExpanded] = useState(false); - const expandedHeight = useRef(null); - - return -
- - - -
-
-
-
setIsExpanded(old => !old)} - sx={{ - display: 'inline-block', - backgroundColor: 'rgba(0, 0, 0, 0.5)', - color: 'white', - p: '6px', - m: 2, - fontSize: '30px', - lineHeight: '1em', - borderRadius: '10px', - cursor: 'pointer', - '&:hover': { - backgroundColor: 'rgba(0, 0, 0, 0.7)', - }, - '&:hover > svg': { - transform: isExpanded ? 'none' : 'rotate(45deg)', - }, - }} - > - -
-
-
-
{ if (r) { expandedHeight.current = `${r.offsetHeight}px`; } }} - sx={{ p: 1, border: theme => `1px solid ${theme.colors.gray[0]}` }} - > - - - { !isDesktop && } - - -
-
-
-
-
; -}; - -const StreamInfo = ({ stream }) => { - const s = stream?.getVideoTracks()?.[0]?.getSettings(); - const sizeInfo = (s && s.width && s.height) ? `${s.width}×${s.height}` : ''; - const fpsInfo = (s && s.frameRate) ? `${s.frameRate} fps` : ''; - - return s ? [sizeInfo, fpsInfo].join(', ') : '...'; -}; - -const UniveralSettings = ({ isDesktop, updatePrefs, prefs, stream }) => { - const { t } = useTranslation(); - - const changeQuality = quality => updatePrefs({ quality }); - const qualities = ['480p', '720p', '1080p', '1440p', '2160p']; - const kind = isDesktop ? 'desktop' : 'user'; - - const [, currentHeight] = dimensionsOf(stream); - let fitState; - if (currentHeight && qualities.includes(prefs.quality)) { - const expectedHeight = parseInt(prefs.quality); - fitState = expectedHeight === currentHeight ? 'ok': 'warn'; - } - - return - - {t('sources-video-quality')}: - - prefs.quality !== q)} - /> - { - qualities.map(q => ) - } - - - ; -}; - -const UserSettings = ({ updatePrefs, prefs }) => { - const { t } = useTranslation(); - const state = useStudioState(); - - const currentDeviceId = deviceIdOf(state.userStream); - let devices = []; - for (const d of state.mediaDevices) { - // Only intersted in video inputs - if (d.kind !== 'videoinput') { - continue; - } - - // If we already have a device with that device ID, we ignore it. - if (devices.some(od => od.deviceId === d.deviceId)) { - continue; - } - - devices.push(d); - } - - const ars = ['4:3', '16:9']; - const [width, height] = dimensionsOf(state.userStream); - let arState; - if (width && height && ars.includes(prefs.aspectRatio)) { - const currentAr = width / height; - const [w, h] = prefs.aspectRatio.split(':'); - const expectedAr = Number(w) / Number(h) - - // We have some range we accept as "good". You never know with these - // floats... - arState = (expectedAr * 0.97 < currentAr && currentAr < expectedAr / 0.97) - ? 'ok' - : 'warn'; - } - - const changeDevice = id => updatePrefs({ deviceId: id }); - const changeAspectRatio = ratio => updatePrefs({ aspectRatio: ratio }); - - return - - {t('sources-video-device')}: - - - - - - {t('sources-video-aspect-ratio')}: - - prefs.aspectRatio !== x)} - /> - { - ars.map(ar => ) - } - - - ; -}; - -// A styled radio input which looks like a button. -const RadioButton = ({ id, value, checked, name, onChange, label, state }) => { - const stateColorMap = { - 'warn': '#ffe300', - 'ok': '#51d18f', - }; - - return - onChange(e.target.value)} - {...{ id, value, checked, name }} - sx={{ - display: 'none', - '&+label': { - border: theme => `2px solid ${theme.colors.gray[0]}`, - p: '1px 4px', - borderRadius: '6px', - mx: 1, - }, - '&:checked+label': { - bg: 'gray.0', - color: state ? stateColorMap[state] : 'white', - fontWeight: 'bold', - }, - }} - /> - - ; -}; From c05be4a1d4286e5551f746d68aca471433afe629 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Wed, 22 Apr 2020 15:13:45 +0200 Subject: [PATCH 14/24] Remove `extraHeight` feature of the resize box It is not needed anymore and just makes the algorithm more complex. --- src/ui/studio/elements.js | 42 ++++++++++++++------------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/src/ui/studio/elements.js b/src/ui/studio/elements.js index e33e6b5b..1ef9a74c 100644 --- a/src/ui/studio/elements.js +++ b/src/ui/studio/elements.js @@ -81,19 +81,14 @@ export const useVideoBoxResize = () => React.useContext(VideoBoxResizeContext); // Each child in `children` needs to be an object with the following fields: // // - `body`: the rendered DOM. -// - `aspectRatio`: the desired aspect ratio for the child. -// - `extraHeight`: sometimes the rendered child has a fixed extra height (for -// example to render a title above or information below the video stream). -// This extra height is included in the calculation such that the child minus -// this extra height has the perfect `aspectRatio` as given above. +// - `dimensions`: a function returning `[width, height]` of the child (also +// defining the aspect ratio). export function VideoBox({ gap = 0, children }) { const { ref, width = 1, height = 1 } = useResizeObserver(); // This is a dummy state to force a rerender. const [, setForceCounter] = useState(0); - const forceRender = () => { - setForceCounter(v => v + 1); - }; + const forceRender = () => setForceCounter(v => v + 1); // Setup the handler for when a video stream is resized. let dimensions = children.map(c => c.dimensions()); @@ -110,23 +105,20 @@ export function VideoBox({ gap = 0, children }) { switch (children.length) { case 1: { const child = children[0]; - const extraChildHeight = child.extraHeight || 0; const aspectRatio = ar(child.dimensions()); - // Calculate size of child let childWidth; let childHeight; - const availableHeight = height - extraChildHeight; - if (width > availableHeight * aspectRatio) { + if (width > height * aspectRatio) { // Child height perfectly matches container, extra space left and right - childHeight = availableHeight + extraChildHeight; - childWidth = availableHeight * aspectRatio; + childHeight = height; + childWidth = height * aspectRatio; } else { // Child width perfectly matches container, extra space top and bottom childWidth = width; - childHeight = (width / aspectRatio) + extraChildHeight; + childHeight = (width / aspectRatio); } return ( @@ -163,23 +155,21 @@ export function VideoBox({ gap = 0, children }) { // Videos side by side (row). const { rowWidths, rowHeights } = (() => { - const extraHeight = Math.max(children[0].extraHeight || 0, children[1].extraHeight || 0); - const availableHeight = height - extraHeight; const availableWidth = width - gap; const combinedAspectRatio = aspectRatios[0] + aspectRatios[1]; - if (availableWidth > availableHeight * combinedAspectRatio) { + if (availableWidth > height * combinedAspectRatio) { // Children height perfectly matches container, extra space left and // right. return { - rowHeights: Array(2).fill(availableHeight + extraHeight), - rowWidths: aspectRatios.map(ar => availableHeight * ar), + rowHeights: Array(2).fill(height), + rowWidths: aspectRatios.map(ar => height * ar), }; } else { // Children width perfectly matches container, extra space top and // bottom. const baseHeight = availableWidth / combinedAspectRatio; return { - rowHeights: children.map(c => baseHeight + (c.extraHeight || 0)), + rowHeights: children.map(c => baseHeight), rowWidths: aspectRatios.map(ar => baseHeight * ar), } } @@ -187,24 +177,22 @@ export function VideoBox({ gap = 0, children }) { // One video below the other (col/column). const { colWidths, colHeights } = (() => { - const extraHeight = gap + (children[0].extraHeight || 0) + (children[1].extraHeight || 0); - const availableHeight = height - extraHeight; const combinedAspectRatio = 1 / ((1 / aspectRatios[0]) + (1 / aspectRatios[1])); - if (width > availableHeight * combinedAspectRatio) { + if (width > height * combinedAspectRatio) { // Children height perfectly matches container, extra space left and // right. - const width = availableHeight * combinedAspectRatio; + const width = height * combinedAspectRatio; return { - colHeights: children.map((c, i) => (width / aspectRatios[i]) + (c.extraHeight || 0)), + colHeights: children.map((c, i) => (width / aspectRatios[i])), colWidths: Array(2).fill(width), }; } else { // Children width perfectly matches container, extra space top and // bottom. return { - colHeights: children.map((c, i) => (width / aspectRatios[i]) + (c.extraHeight || 0)), + colHeights: children.map((c, i) => (width / aspectRatios[i])), colWidths: Array(2).fill(width), } } From 7466849f3dbd4edb94bcf2411835368a233d5445 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Wed, 22 Apr 2020 15:40:50 +0200 Subject: [PATCH 15/24] Fix useless resizes when changing preferences of a stream Previously, if there was no stream, the resize algorithm would assume an aspect ratio of 16:9. Which is annoying when the old and the new stream are 4:3 for example. --- src/ui/studio/elements.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/ui/studio/elements.js b/src/ui/studio/elements.js index 1ef9a74c..97d5a1b3 100644 --- a/src/ui/studio/elements.js +++ b/src/ui/studio/elements.js @@ -7,7 +7,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons'; import { Box, Button, Flex } from '@theme-ui/components'; import { useTranslation } from 'react-i18next'; -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import equal from 'fast-deep-equal'; // A div containing optional "back" and "next" buttons as well as the centered @@ -72,7 +72,7 @@ export const useVideoBoxResize = () => React.useContext(VideoBoxResizeContext); // `
` that maintains this exact aspect ratio. In the one child case, that // `
` also perfectly fits within the parent container. In the case of two // children, those children are laid out in such a way that the utilized screen -// space is maximized. +// space is maximized while both children have the same width or height. // // `children` has to be the length 1 or 2! The parameter `gap` specifies the // width of the empty space between the two children in the case that two @@ -82,7 +82,8 @@ export const useVideoBoxResize = () => React.useContext(VideoBoxResizeContext); // // - `body`: the rendered DOM. // - `dimensions`: a function returning `[width, height]` of the child (also -// defining the aspect ratio). +// defining the aspect ratio). We require the dimensions instead of only the +// aspect ratio to better detect changes in the video stream. export function VideoBox({ gap = 0, children }) { const { ref, width = 1, height = 1 } = useResizeObserver(); @@ -90,12 +91,26 @@ export function VideoBox({ gap = 0, children }) { const [, setForceCounter] = useState(0); const forceRender = () => setForceCounter(v => v + 1); + // We try to remember the last valid dimension. Otherwise, changing video + // preferences for a non-16:9 strean leads to visual noise: the box always + // changes between its aspect ratio and the fallback 16:9 ratio. + const lastDimensions = useRef(children.map(() => [undefined, undefined])); + const updateLastDimensions = newDimensions => { + newDimensions.forEach(([w, h], i) => { + if (w && h) { + lastDimensions.current[i] = [w, h]; + } + }); + } + // Setup the handler for when a video stream is resized. let dimensions = children.map(c => c.dimensions()); + updateLastDimensions(dimensions); const resizeVideoBox = () => { const newDimensions = children.map(c => c.dimensions()); if (!equal(newDimensions, dimensions)) { dimensions = newDimensions; + updateLastDimensions(dimensions); forceRender(); } } @@ -105,7 +120,7 @@ export function VideoBox({ gap = 0, children }) { switch (children.length) { case 1: { const child = children[0]; - const aspectRatio = ar(child.dimensions()); + const aspectRatio = ar(lastDimensions.current[0]); // Calculate size of child let childWidth; @@ -151,7 +166,7 @@ export function VideoBox({ gap = 0, children }) { // 1:0.75 respectively. We can now add those, resulting in 1:1.31. // Finally, we normalize with respect to height again: 0.76:1 - const aspectRatios = children.map(c => ar(c.dimensions())); + const aspectRatios = lastDimensions.current.map(d => ar(d)); // Videos side by side (row). const { rowWidths, rowHeights } = (() => { From a45e6a009f4fbe47b09b13617e64318b874bafdd Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Wed, 22 Apr 2020 18:39:12 +0200 Subject: [PATCH 16/24] Merge user video preferences with max height settings --- src/ui/studio/capturer.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/ui/studio/capturer.js b/src/ui/studio/capturer.js index 1b96793d..23d8d269 100644 --- a/src/ui/studio/capturer.js +++ b/src/ui/studio/capturer.js @@ -1,3 +1,11 @@ +const mergeHeightConstraint = (maxHeight, videoConstraints, fallbackIdeal) => { + const maxField = maxHeight && { max: maxHeight }; + const ideal = videoConstraints?.height?.ideal || fallbackIdeal; + const idealField = ideal && (maxHeight ? { ideal: Math.min(ideal, maxHeight) } : { ideal }); + + return { height: { ...maxField, ...idealField } }; +}; + export async function startAudioCapture(dispatch, deviceId = null) { try { const stream = await navigator.mediaDevices.getUserMedia({ @@ -25,16 +33,14 @@ export async function startDisplayCapture(dispatch, settings, videoConstraints = const maxFps = settings.display?.maxFps ? { frameRate: { max: settings.display.maxFps } } : {}; - const maxHeight = settings.display?.maxHeight - ? { height: { max: settings.display.maxHeight } } - : {}; + const height = mergeHeightConstraint(settings.display?.maxHeight, videoConstraints); const constraints = { video: { cursor: 'always', ...maxFps, - ...maxHeight, - ...videoConstraints + ...videoConstraints, + ...height, }, audio: false, }; @@ -60,16 +66,14 @@ export async function startUserCapture(dispatch, settings, videoConstraints) { const maxFps = settings.camera?.maxFps ? { frameRate: { max: settings.camera.maxFps } } : {}; - const maxHeight = settings.camera?.maxHeight - ? { height: { ideal: Math.min(1080, settings.camera.maxHeight), max: settings.camera.maxHeight } } - : { height: { ideal: 1080 } }; + const height = mergeHeightConstraint(settings.camera?.maxHeight, videoConstraints, 1080); const constraints = { video: { facingMode: 'user', - ...maxFps, - ...maxHeight, ...videoConstraints, + ...maxFps, + ...height, }, audio: false, }; From 06f62e373055662f4cd19dd3efac5ec959eafcbb Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Wed, 22 Apr 2020 18:57:00 +0200 Subject: [PATCH 17/24] Only show quality options that are below the settings' `maxHeight` --- src/ui/studio/video-setup/prefs.js | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/ui/studio/video-setup/prefs.js b/src/ui/studio/video-setup/prefs.js index 1dca22e9..37a51c9e 100644 --- a/src/ui/studio/video-setup/prefs.js +++ b/src/ui/studio/video-setup/prefs.js @@ -48,12 +48,24 @@ export const prefsToConstraints = (prefs, exactDevice = false) => { }; // All aspect ratios the user can choose from. -export const ASPECT_RATIOS = ['4:3', '16:9']; +const ASPECT_RATIOS = ['4:3', '16:9']; + +// All quality options given to the user respecting the `maxHeight` from the +// settings. +const qualityOptions = maxHeight => { + const defaults = [480, 720, 1080, 1440, 2160]; + let out = defaults.filter(q => !maxHeight || q <= maxHeight); + if (maxHeight && (out.length === 0 || out[out.length - 1] !== maxHeight)) { + out.push(maxHeight); + } + + return out.map(n => `${n}p`); +} // Converts the given aspect ratio label (one of the elements in // `ASPECT_RATIOS`) into the numerical ratio, e.g. 4/3 = 1.333. If the argument // is not a valid label, `null` is returned. -export const parseAspectRatio = label => { +const parseAspectRatio = label => { const mapping = { '4:3': 4 / 3, '16:9': 16 / 9, @@ -64,7 +76,7 @@ export const parseAspectRatio = label => { // Converts the given quality label to the actual height as number. If the // argument is not a valid quality label (e.g. '720p'), `null` is returned. -export const parseQuality = label => { +const parseQuality = label => { if (!/^[0-9]+p$/.test(label)) { return null; } @@ -203,7 +215,7 @@ export const StreamSettings = ({ isDesktop, stream }) => { { !isDesktop && } - +
@@ -220,11 +232,12 @@ const StreamInfo = ({ stream }) => { return s ? [sizeInfo, fpsInfo].join(', ') : '...'; }; -const UniveralSettings = ({ isDesktop, updatePrefs, prefs, stream }) => { +const UniveralSettings = ({ isDesktop, updatePrefs, prefs, stream, settings }) => { const { t } = useTranslation(); const changeQuality = quality => updatePrefs({ quality }); - const qualities = ['480p', '720p', '1080p', '1440p', '2160p']; + const maxHeight = isDesktop ? settings.display?.maxHeight : settings.camera?.maxHeight; + const qualities = qualityOptions(maxHeight); const kind = isDesktop ? 'desktop' : 'user'; const [, currentHeight] = dimensionsOf(stream); From 7be7c4abc1b83bce3549591e339fe578f0fdc231 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Wed, 22 Apr 2020 19:13:23 +0200 Subject: [PATCH 18/24] Use `exact` to request a webcam when the user manually selects one --- src/ui/studio/video-setup/prefs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/studio/video-setup/prefs.js b/src/ui/studio/video-setup/prefs.js index 37a51c9e..fe746988 100644 --- a/src/ui/studio/video-setup/prefs.js +++ b/src/ui/studio/video-setup/prefs.js @@ -112,7 +112,7 @@ export const StreamSettings = ({ isDesktop, stream }) => { const updatePrefs = newPrefs => { // Merge and update preferences. const merged = { ...prefs, ...newPrefs }; - const constraints = prefsToConstraints(merged); + const constraints = prefsToConstraints(merged, true); // Update preferences in local storage and re-request stream. The latter // will cause the rerender. From b11577edda3f5d83dd90b3360ecde5604b96a52d Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Wed, 29 Apr 2020 11:05:55 +0200 Subject: [PATCH 19/24] Minor style improvements for video preference menu --- src/ui/studio/video-setup/prefs.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ui/studio/video-setup/prefs.js b/src/ui/studio/video-setup/prefs.js index fe746988..a351f212 100644 --- a/src/ui/studio/video-setup/prefs.js +++ b/src/ui/studio/video-setup/prefs.js @@ -156,9 +156,10 @@ export const StreamSettings = ({ isDesktop, stream }) => { }}> @@ -204,8 +205,9 @@ export const StreamSettings = ({ isDesktop, stream }) => { height: isExpanded ? (expandedHeight.current || 'auto') : 0, transition: 'height 0.2s', overflow: 'hidden', - backgroundColor: 'rgba(255, 255, 255, 0.95)', + backgroundColor: 'white', fontSize: '18px', + boxShadow: isExpanded ? '0 0 15px rgba(0, 0, 0, 0.3)' : 'none', }}>
{ {t('sources-video-device')}: changeDevice(e.target.value)} - > - { - devices.map((d, i) => ( - - )) - } - - - - - {t('sources-video-aspect-ratio')}: - - prefs.aspectRatio !== x)} - /> + {t('sources-video-device')}: + + + + + {t('sources-video-aspect-ratio')}: + + prefs.aspectRatio !== x)} + /> + { + ASPECT_RATIOS.map(ar => + + ) - } - - + /> + ) + } + ; }; From cabdb51a6091bd460486635ea904f93c6636f179 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Wed, 29 Apr 2020 17:27:40 +0200 Subject: [PATCH 21/24] Fix video preferences for small screens Now the user can scroll and the X is always visible --- src/ui/studio/elements.js | 23 +++++++++++++++++++---- src/ui/studio/video-setup/prefs.js | 7 ++++++- src/ui/studio/video-setup/preview.js | 5 ++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/ui/studio/elements.js b/src/ui/studio/elements.js index 97d5a1b3..426aac01 100644 --- a/src/ui/studio/elements.js +++ b/src/ui/studio/elements.js @@ -84,7 +84,7 @@ export const useVideoBoxResize = () => React.useContext(VideoBoxResizeContext); // - `dimensions`: a function returning `[width, height]` of the child (also // defining the aspect ratio). We require the dimensions instead of only the // aspect ratio to better detect changes in the video stream. -export function VideoBox({ gap = 0, children }) { +export function VideoBox({ gap = 0, minWidth = 180, children }) { const { ref, width = 1, height = 1 } = useResizeObserver(); // This is a dummy state to force a rerender. @@ -139,7 +139,12 @@ export function VideoBox({ gap = 0, children }) { return (
-
+
{ child.body }
@@ -242,10 +247,20 @@ export function VideoBox({ gap = 0, children }) { minHeight: 0, }} > -
+
{ children[0].body }
-
+
{ children[1].body }
diff --git a/src/ui/studio/video-setup/prefs.js b/src/ui/studio/video-setup/prefs.js index b938df46..27088504 100644 --- a/src/ui/studio/video-setup/prefs.js +++ b/src/ui/studio/video-setup/prefs.js @@ -169,6 +169,10 @@ export const StreamSettings = ({ isDesktop, stream }) => { left: 0, right: 0, bottom: 0, + height: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-end', }}>
{
{ return

Something went very wrong

; } + // Below this value, the video preference menu looks awful. + const minWidth = 300; + return ( { warnings } - { children } + { children } ); } From 0729f035b41918a2a8c9609b27e04828164f140d Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Wed, 29 Apr 2020 18:17:43 +0200 Subject: [PATCH 22/24] Add explanation about non-supported preferences I asked a few other people about those texts, including a certainly technically non-versed user :P We tweaked the texts and with this, everything should be pretty clear to the user. Particularly the "if in doubt" part of the text really helps as inexperienced users actually stick to those advices quite well. --- src/i18n/locales/de.json | 1 + src/i18n/locales/en.json | 1 + src/ui/studio/video-setup/prefs.js | 20 +++++++++++++++++--- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index a1a7f44a..18c6bef7 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -47,6 +47,7 @@ "sources-video-aspect-ratio-auto": "auto", "sources-video-quality": "Qualität", "sources-video-quality-auto": "auto", + "sources-video-preferences-note": "<0>*Hinweis: Dies sind lediglich Präferenzen. Es ist nicht garantiert, dass alle Einstellungen von Ihrem Gerät unterstützt werden. Im Zweifelsfall 'auto' wählen.", "source-display-not-allowed-title": "Zugriff auf Bildschirm nicht möglich", "source-display-not-allowed-text": "Versuchen Sie, die Quelle neu auszuwählen. Falls das nicht funkioniert, laden Sie entweder diese Seite neu oder erlauben Sie dieser Seite manuell Zugriff auf Ihren Bildschirm (typischerweise über einen Button neben der Adressleiste Ihres Browsers)", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 71f9413f..a0fddf4b 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -51,6 +51,7 @@ "sources-video-aspect-ratio-auto": "auto", "sources-video-quality": "Quality", "sources-video-quality-auto": "auto", + "sources-video-preferences-note": "<0>*Note: these are merely preferences and it cannot be guaranteed that all options are actually supported on your device. If in doubt, choose 'auto'.", "sources-audio-question": "Record Audio?", "sources-audio-microphone": "Microphone", diff --git a/src/ui/studio/video-setup/prefs.js b/src/ui/studio/video-setup/prefs.js index 27088504..268b0cdf 100644 --- a/src/ui/studio/video-setup/prefs.js +++ b/src/ui/studio/video-setup/prefs.js @@ -5,7 +5,7 @@ import { jsx } from 'theme-ui'; import { Fragment, useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { useTranslation, Trans } from 'react-i18next'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTimes, faCog } from '@fortawesome/free-solid-svg-icons'; @@ -229,6 +229,20 @@ export const StreamSettings = ({ isDesktop, stream }) => { { !isDesktop && }
+ +
`1px solid ${theme.colors.gray[2]}`, + }}> + + Note: Explanation. + +
@@ -276,7 +290,7 @@ const UniveralSettings = ({ isDesktop, updatePrefs, prefs, stream, settings }) = } return - {t('sources-video-quality')}: + {t('sources-video-quality')}*: { - {t('sources-video-aspect-ratio')}: + {t('sources-video-aspect-ratio')}*: Date: Wed, 29 Apr 2020 19:44:09 +0200 Subject: [PATCH 23/24] Improve determining the height of the video preference box Previously there existed a number of box where the box would be too high or too small. --- src/ui/studio/video-setup/prefs.js | 76 +++++++++++++++++------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/src/ui/studio/video-setup/prefs.js b/src/ui/studio/video-setup/prefs.js index 268b0cdf..5c6fb0c8 100644 --- a/src/ui/studio/video-setup/prefs.js +++ b/src/ui/studio/video-setup/prefs.js @@ -4,10 +4,11 @@ /** @jsx jsx */ import { jsx } from 'theme-ui'; -import { Fragment, useEffect, useRef, useState } from 'react'; +import { Fragment, useEffect, useState } from 'react'; import { useTranslation, Trans } from 'react-i18next'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTimes, faCog } from '@fortawesome/free-solid-svg-icons'; +import useResizeObserver from "use-resize-observer/polyfilled"; import { useSettings } from '../../../settings'; import { deviceIdOf, dimensionsOf } from '../../../util.js'; @@ -106,6 +107,16 @@ export const loadDisplayPrefs = () => ({ export const StreamSettings = ({ isDesktop, stream }) => { const dispatch = useDispatch(); const settings = useSettings(); + const [expandedHeight, setExpandedHeight] = useState(0); + const { ref } = useResizeObserver({ + // We don't use the height passed to the callback as we want the outer + // height. We also add a magic "4" here. 2 pixels are for the border of the + // outer div. The other two are "wiggle room". If we always set the height + // to fit exactly, this easily leads to unnessecary scrollbars appearing. + // This in turn might lead to rewrapping and then a change in height, in + // the worst case ending up in an infinite loop. + onResize: () => setExpandedHeight(ref.current?.offsetHeight + 4), + }); // The current preferences and the callback to update them. const prefs = isDesktop ? loadDisplayPrefs() : loadCameraPrefs(); @@ -143,7 +154,6 @@ export const StreamSettings = ({ isDesktop, stream }) => { // State about expanding and hiding the settings. const [isExpanded, setIsExpanded] = useState(false); - const expandedHeight = useRef(null); return
{
-
{ if (r) { expandedHeight.current = `${r.offsetHeight}px`; } }} - sx={{ p: 1, border: theme => `1px solid ${theme.colors.gray[0]}` }} - > -
- { !isDesktop && } - -
- -
`1px solid ${theme.colors.gray[2]}`, - }}> - - Note: Explanation. - +
`1px solid ${theme.colors.gray[0]}`, + height: '100%', + overflow: 'auto', + }}> +
+
+ { !isDesktop && } + +
+ +
`1px solid ${theme.colors.gray[2]}`, + }}> + + Note: Explanation. + +
From cc417fcb4fcb40ab76e1105b7c4bfa8e2c1a0335 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Mon, 4 May 2020 10:01:11 +0200 Subject: [PATCH 24/24] Remove max-width from container in video source selection --- src/ui/studio/video-setup/index.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ui/studio/video-setup/index.js b/src/ui/studio/video-setup/index.js index ad4ee68c..c16728c1 100644 --- a/src/ui/studio/video-setup/index.js +++ b/src/ui/studio/video-setup/index.js @@ -3,7 +3,7 @@ import { jsx } from 'theme-ui'; import { faChalkboard, faChalkboardTeacher, faUser } from '@fortawesome/free-solid-svg-icons'; -import { Container, Flex, Heading, Text } from '@theme-ui/components'; +import { Flex, Heading, Text } from '@theme-ui/components'; import { Styled } from 'theme-ui'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -140,15 +140,17 @@ export default function VideoSetup({ nextStep, userHasWebcam }) { && !state.userUnexpectedEnd && !state.displayUnexpectedEnd; return ( - - + { title } @@ -164,7 +166,7 @@ export default function VideoSetup({ nextStep, userHasWebcam }) { label: 'sources-video-reselect-source', }} /> } - +
); }