Skip to content

Commit

Permalink
Merge pull request #579 from LukasKalbertodt/webcam-selection
Browse files Browse the repository at this point in the history
Add configurations to let the user choose the webcam device, the aspect ratio and the resolution
  • Loading branch information
LukasKalbertodt authored May 6, 2020
2 parents cbd3a89 + cc417fc commit e3507de
Show file tree
Hide file tree
Showing 10 changed files with 672 additions and 204 deletions.
6 changes: 6 additions & 0 deletions src/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions src/studio-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const STATE_ERROR = 'error';


const initialState = () => ({
mediaDevices: [],

audioAllowed: null,
audioStream: null,
audioUnexpectedEnd: false,
Expand Down Expand Up @@ -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 };

Expand Down
26 changes: 16 additions & 10 deletions src/ui/studio/capturer.js
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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,
};
Expand All @@ -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,
};
Expand Down
88 changes: 53 additions & 35 deletions src/ui/studio/elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -72,7 +72,7 @@ export const useVideoBoxResize = () => React.useContext(VideoBoxResizeContext);
// `<div>` that maintains this exact aspect ratio. In the one child case, that
// `<div>` 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
Expand All @@ -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();
}
}
Expand All @@ -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 (
<VideoBoxResizeContext.Provider value={resizeVideoBox}>
<div ref={ref} sx={{ flex: '1 0 0', minHeight: 0, display: 'flex' }}>
<div sx={{ height: childHeight, width: childWidth, minWidth: '180px', margin: 'auto' }}>
<div sx={{
height: childHeight,
width: childWidth,
minWidth: `${minWidth}px`,
margin: 'auto',
}}>
{ child.body }
</div>
</div>
Expand All @@ -159,52 +171,48 @@ 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),
}
}
})();

// 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),
}
}
Expand Down Expand Up @@ -239,10 +247,20 @@ export function VideoBox({ gap = 0, children }) {
minHeight: 0,
}}
>
<div sx={{ height: heights[0], width: widths[0], minWidth: '180px', margin: 'auto' }}>
<div sx={{
height: heights[0],
width: widths[0],
minWidth: `${minWidth}px`,
margin: 'auto',
}}>
{ children[0].body }
</div>
<div sx={{ height: heights[1], width: widths[1], minWidth: '180px', margin: 'auto' }}>
<div sx={{
height: heights[1],
width: widths[1],
minWidth: `${minWidth}px`,
margin: 'auto',
}}>
{ children[1].body }
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/ui/studio/save-creation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => <input sx={{ variant: 'styles.input' }} {...props} />;

Expand Down
Loading

0 comments on commit e3507de

Please sign in to comment.