diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json
index 86854926..18c6bef7 100644
--- a/src/i18n/locales/de.json
+++ b/src/i18n/locales/de.json
@@ -42,6 +42,12 @@
"sources-scenario-user": "Kamera",
"sources-display": "Bildschirm",
"sources-user": "Kamera",
+ "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",
+ "sources-video-preferences-note": "<0>*Hinweis:0> 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 9ee5b52d..a0fddf4b 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -46,6 +46,12 @@
"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-video-aspect-ratio": "Aspect ratio",
+ "sources-video-aspect-ratio-auto": "auto",
+ "sources-video-quality": "Quality",
+ "sources-video-quality-auto": "auto",
+ "sources-video-preferences-note": "<0>*Note:0> 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/studio-state.js b/src/studio-state.js
index db7eb472..14175889 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,9 @@ const initialState = () => ({
const reducer = (state, action) => {
switch (action.type) {
+ case 'UPDATE_MEDIA_DEVICES':
+ 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..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({
@@ -21,19 +29,18 @@ 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 } }
: {};
- 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,
+ ...height,
},
audio: false,
};
@@ -55,19 +62,18 @@ 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 } }
: {};
- 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',
+ ...videoConstraints,
...maxFps,
- ...maxHeight,
+ ...height,
},
audio: false,
};
diff --git a/src/ui/studio/elements.js b/src/ui/studio/elements.js
index e33e6b5b..426aac01 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
@@ -81,26 +81,36 @@ 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.
-export function VideoBox({ gap = 0, children }) {
+// - `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, minWidth = 180, 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);
+
+ // 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();
}
}
@@ -110,29 +120,31 @@ 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());
-
+ const aspectRatio = ar(lastDimensions.current[0]);
// 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 (
-
+
{ child.body }
@@ -159,27 +171,25 @@ 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 } = (() => {
- 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 +197,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),
}
}
@@ -239,10 +247,20 @@ export function VideoBox({ gap = 0, children }) {
minHeight: 0,
}}
>
-