Skip to content

Commit 3741cb1

Browse files
authored
Fixed Camera orientation (#956)
on iOS device: window.matchMedia('(orientation: portrait)') can be unreliable.
1 parent 5380665 commit 3741cb1

File tree

6 files changed

+262
-241
lines changed

6 files changed

+262
-241
lines changed

packages/camera-web/src/Camera/Camera.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ export function Camera<T extends object>({
8585
);
8686
const {
8787
ref: videoRef,
88-
dimensions: streamDimensions,
8988
previewDimensions,
9089
error,
9190
retry,
@@ -98,7 +97,7 @@ export function Camera<T extends object>({
9897
});
9998
const { ref: canvasRef, dimensions: canvasDimensions } = useCameraCanvas({
10099
resolution,
101-
streamDimensions,
100+
streamDimensions: previewDimensions,
102101
allowImageUpscaling,
103102
});
104103
const takeScreenshot = useCameraScreenshot({
@@ -147,7 +146,7 @@ export function Camera<T extends object>({
147146
error,
148147
retry,
149148
isLoading,
150-
dimensions: streamDimensions,
149+
dimensions: previewDimensions,
151150
previewDimensions,
152151
}}
153152
cameraPreview={cameraPreview}

packages/camera-web/src/Camera/hooks/useCameraPreview.ts

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,34 @@
11
import { useMonitoring } from '@monkvision/monitoring';
2-
import { RefObject, useEffect, useMemo, useRef } from 'react';
2+
import { RefObject, useEffect, useMemo, useRef, useState } from 'react';
33
import { PixelDimensions } from '@monkvision/types';
44
import { useWindowDimensions } from '@monkvision/common';
55
import { CameraConfig, getMediaConstraints } from './utils';
66
import { UserMediaResult, useUserMedia } from './useUserMedia';
77

8+
function getPreviewDimensions(
9+
refVideo: RefObject<HTMLVideoElement>,
10+
windowDimensions: PixelDimensions,
11+
) {
12+
const height = refVideo.current?.videoHeight;
13+
const width = refVideo.current?.videoWidth;
14+
15+
if (!windowDimensions || !height || !width) {
16+
return null;
17+
}
18+
const windowAspectRatio = windowDimensions.width / windowDimensions.height;
19+
const streamAspectRatio = width / height;
20+
21+
return windowAspectRatio >= streamAspectRatio
22+
? {
23+
width: windowDimensions.height * streamAspectRatio,
24+
height: windowDimensions.height,
25+
}
26+
: {
27+
width: windowDimensions.width,
28+
height: windowDimensions.width / streamAspectRatio,
29+
};
30+
}
31+
832
/**
933
* An object containing properties used to handle the camera preview.
1034
*/
@@ -25,36 +49,37 @@ export interface CameraPreviewHandle extends UserMediaResult {
2549
*/
2650
export function useCameraPreview(config: CameraConfig): CameraPreviewHandle {
2751
const ref = useRef<HTMLVideoElement>(null);
52+
const [previewDimensions, setPreviewDimensions] = useState<PixelDimensions | null>(null);
2853
const windowDimensions = useWindowDimensions();
2954
const { handleError } = useMonitoring();
3055
const userMediaResult = useUserMedia(getMediaConstraints(config), ref);
3156

32-
const previewDimensions = useMemo(() => {
33-
if (!windowDimensions || !userMediaResult.dimensions) {
34-
return null;
35-
}
36-
const windowAspectRatio = windowDimensions.width / windowDimensions.height;
37-
const streamAspectRatio = userMediaResult.dimensions.width / userMediaResult.dimensions.height;
57+
useEffect(() => {
58+
const currentRef = ref.current;
3859

39-
return windowAspectRatio >= streamAspectRatio
40-
? {
41-
width: windowDimensions.height * streamAspectRatio,
42-
height: windowDimensions.height,
43-
}
44-
: {
45-
width: windowDimensions.width,
46-
height: windowDimensions.width / streamAspectRatio,
47-
};
48-
}, [windowDimensions, userMediaResult.dimensions]);
60+
if (userMediaResult.stream && currentRef) {
61+
currentRef.srcObject = userMediaResult.stream;
4962

50-
useEffect(() => {
51-
if (userMediaResult.stream && ref.current) {
52-
ref.current.srcObject = userMediaResult.stream;
53-
ref.current.onloadedmetadata = () => {
54-
ref.current?.play().catch(handleError);
63+
const handleMetadata = () => {
64+
currentRef?.play().catch(handleError);
65+
setPreviewDimensions(getPreviewDimensions(ref, windowDimensions));
66+
};
67+
68+
const handleResize = () => {
69+
setPreviewDimensions(getPreviewDimensions(ref, windowDimensions));
5570
};
71+
72+
currentRef.onloadedmetadata = handleMetadata;
73+
currentRef.onresize = handleResize;
5674
}
57-
}, [userMediaResult.stream]);
75+
76+
return () => {
77+
if (currentRef) {
78+
currentRef.onloadedmetadata = null;
79+
currentRef.onresize = null;
80+
}
81+
};
82+
}, [windowDimensions, userMediaResult.stream, handleError]);
5883

5984
return useMemo(
6085
() => ({

packages/camera-web/src/Camera/hooks/useUserMedia.ts

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { useMonitoring } from '@monkvision/monitoring';
22
import deepEqual from 'fast-deep-equal';
33
import { RefObject, useCallback, useEffect, useRef, useState } from 'react';
4-
import { PixelDimensions } from '@monkvision/types';
5-
import { isMobileDevice, useIsMounted, useObjectMemo } from '@monkvision/common';
4+
import { useIsMounted, useObjectMemo } from '@monkvision/common';
65
import { analyzeCameraDevices } from './utils';
76

87
/**
@@ -17,10 +16,6 @@ export enum InvalidStreamErrorName {
1716
* The stream had too many video tracks (more than one).
1817
*/
1918
TOO_MANY_VIDEO_TRACKS = 'TooManyVideoTracks',
20-
/**
21-
* The stream's video track had no dimensions.
22-
*/
23-
NO_DIMENSIONS = 'NoDimensions',
2419
}
2520

2621
class InvalidStreamError extends Error {
@@ -105,11 +100,6 @@ export interface UserMediaResult {
105100
* The resulting video stream. The stream can be null when not initialized or in case of an error.
106101
*/
107102
stream: MediaStream | null;
108-
/**
109-
* The dimensions of the resulting camera stream. Note that these dimensions can differ from the ones given in the
110-
* stream constraints if they are not supported or available on the current device.
111-
*/
112-
dimensions: PixelDimensions | null;
113103
/**
114104
* The error details. If no error has occurred, this object will be null.
115105
*/
@@ -156,31 +146,6 @@ function getStreamDeviceId(stream: MediaStream): string | null {
156146
return settings.deviceId ?? null;
157147
}
158148

159-
function swapDimensions(dimensions: PixelDimensions): PixelDimensions {
160-
return {
161-
width: dimensions.height,
162-
height: dimensions.width,
163-
};
164-
}
165-
166-
function getStreamDimensions(stream: MediaStream, checkOrientation: boolean): PixelDimensions {
167-
const { width, height } = getStreamVideoTrackSettings(stream);
168-
if (!width || !height) {
169-
throw new InvalidStreamError(
170-
'Unable to set up the Monk camera screenshoter because the video stream does not have the properties width and height defined.',
171-
InvalidStreamErrorName.NO_DIMENSIONS,
172-
);
173-
}
174-
const dimensions = { width, height };
175-
if (!isMobileDevice() || !checkOrientation) {
176-
return dimensions;
177-
}
178-
179-
const isStreamInPortrait = width < height;
180-
const isDeviceInPortrait = window.matchMedia('(orientation: portrait)').matches;
181-
return isStreamInPortrait !== isDeviceInPortrait ? swapDimensions(dimensions) : dimensions;
182-
}
183-
184149
/**
185150
* React hook that wraps the `navigator.mediaDevices.getUserMedia` browser function in order to add React logic layers
186151
* and utility tools :
@@ -205,7 +170,6 @@ export function useUserMedia(
205170
): UserMediaResult {
206171
const streamRef = useRef<MediaStream | null>(null);
207172
const [stream, setStream] = useState<MediaStream | null>(null);
208-
const [dimensions, setDimensions] = useState<PixelDimensions | null>(null);
209173
const [isLoading, setIsLoading] = useState(false);
210174
const [error, setError] = useState<UserMediaError | null>(null);
211175
const [availableCameraDevices, setAvailableCameraDevices] = useState<MediaDeviceInfo[]>([]);
@@ -278,10 +242,9 @@ export function useUserMedia(
278242
if (isMounted()) {
279243
setStream(str);
280244
streamRef.current = str;
281-
setDimensions(getStreamDimensions(str, true));
282-
setIsLoading(false);
283-
setAvailableCameraDevices(deviceDetails.availableDevices);
284245
setSelectedCameraDeviceId(getStreamDeviceId(str));
246+
setAvailableCameraDevices(deviceDetails.availableDevices);
247+
setIsLoading(false);
285248
}
286249
return str;
287250
}, [stream, constraints]);
@@ -323,17 +286,6 @@ export function useUserMedia(
323286
}
324287
}, [constraints, stream, error, isLoading, lastConstraintsApplied, getUserMedia, videoRef]);
325288

326-
useEffect(() => {
327-
if (stream && videoRef && videoRef.current) {
328-
// eslint-disable-next-line no-param-reassign
329-
videoRef.current.onresize = () => {
330-
if (isMounted()) {
331-
setDimensions(getStreamDimensions(stream, false));
332-
}
333-
};
334-
}
335-
}, [stream, videoRef]);
336-
337289
useEffect(() => {
338290
return () => {
339291
streamRef.current?.getTracks().forEach((track) => {
@@ -345,7 +297,6 @@ export function useUserMedia(
345297
return useObjectMemo({
346298
getUserMedia,
347299
stream,
348-
dimensions,
349300
error,
350301
retry,
351302
isLoading,

packages/camera-web/test/Camera/Camera.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,12 @@ describe('Camera component', () => {
8888
<Camera allowImageUpscaling={allowImageUpscaling} resolution={resolution} />,
8989
);
9090

91-
const streamDimensions = (useCameraPreview as jest.Mock).mock.results[0].value.dimensions;
91+
const { previewDimensions } = (useCameraPreview as jest.Mock).mock.results[0].value;
9292

9393
expect(useCameraCanvas).toHaveBeenCalledWith({
9494
allowImageUpscaling,
9595
resolution,
96-
streamDimensions,
96+
previewDimensions,
9797
});
9898
unmount();
9999
});
@@ -224,7 +224,7 @@ describe('Camera component', () => {
224224
error: useCameraPreviewResultMock.error,
225225
retry: useCameraPreviewResultMock.retry,
226226
isLoading: useCameraPreviewResultMock.isLoading || useTakePictureResultMock.isLoading,
227-
dimensions: useCameraPreviewResultMock.dimensions,
227+
dimensions: useCameraPreviewResultMock.previewDimensions,
228228
},
229229
cameraPreview: expect.anything(),
230230
});

0 commit comments

Comments
 (0)