From cf9b869b12376e2822123d319750821246d85e31 Mon Sep 17 00:00:00 2001 From: Bhavin K <58818598+techbhavin@users.noreply.github.com> Date: Wed, 3 Aug 2022 11:32:23 +0530 Subject: [PATCH] fix: improve camera widget (#14679) * fix: issue points Android :2,3,6 * fix: improving the camera widget * fix: show the device menu in fullscreen mode * refactor: use getPlatformOS for check platform os * refactor: add missing yarn file * fix: camera switching issue when video mode * fix: removed the audio/video mute button from ios devices * fix: Disable the media button instead of hide in ios devices --- app/client/package.json | 2 +- app/client/src/utils/helpers.tsx | 95 +++ .../widgets/CameraWidget/component/index.tsx | 621 ++++++++++-------- .../src/widgets/CameraWidget/widget/index.tsx | 2 - app/client/yarn.lock | 8 +- 5 files changed, 434 insertions(+), 294 deletions(-) diff --git a/app/client/package.json b/app/client/package.json index 3901dfa3f6..95cf55d3fd 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -135,7 +135,7 @@ "react-transition-group": "^4.3.0", "react-use-gesture": "^7.0.4", "react-virtuoso": "^1.9.0", - "react-webcam": "^6.0.0", + "react-webcam": "^7.0.1", "react-window": "^1.8.6", "react-zoom-pan-pinch": "^1.6.1", "redux": "^4.0.1", diff --git a/app/client/src/utils/helpers.tsx b/app/client/src/utils/helpers.tsx index e811c52a87..a1bf41d3eb 100644 --- a/app/client/src/utils/helpers.tsx +++ b/app/client/src/utils/helpers.tsx @@ -269,6 +269,53 @@ export const isMacOrIOS = () => { return platformOS === PLATFORM_OS.MAC || platformOS === PLATFORM_OS.IOS; }; +export const getBrowserInfo = () => { + const userAgent = + typeof navigator !== "undefined" ? navigator.userAgent : null; + + if (userAgent) { + let specificMatch; + let match = + userAgent.match( + /(opera|chrome|safari|firefox|msie|CriOS|trident(?=\/))\/?\s*(\d+)/i, + ) || []; + + // browser + if (/CriOS/i.test(match[1])) match[1] = "Chrome"; + + if (match[1] === "Chrome") { + specificMatch = userAgent.match(/\b(OPR|Edge)\/(\d+)/); + if (specificMatch) { + const opera = specificMatch.slice(1); + return { + browser: opera[0].replace("OPR", "Opera"), + version: opera[1], + }; + } + + specificMatch = userAgent.match(/\b(Edg)\/(\d+)/); + if (specificMatch) { + const edge = specificMatch.slice(1); + return { + browser: edge[0].replace("Edg", "Edge (Chromium)"), + version: edge[1], + }; + } + } + + // version + match = match[2] + ? [match[1], match[2]] + : [navigator.appName, navigator.appVersion, "-?"]; + const version = userAgent.match(/version\/(\d+)/i); + + version && match.splice(1, 1, version[1]); + + return { browser: match[0], version: match[1] }; + } + return null; +}; + /** * Removes the trailing slashes from the path * @param path @@ -810,6 +857,54 @@ export const updateSlugNamesInURL = (params: Record) => { history.replace(newURL + search); }; +/** + * Function to get valid supported mimeType for different browsers + * @param media "video" | "audio" + * @returns mimeType string + */ +export const getSupportedMimeTypes = (media: "video" | "audio") => { + const videoTypes = ["webm", "ogg", "mp4", "x-matroska"]; + const audioTypes = ["webm", "ogg", "mp3", "x-matroska"]; + const codecs = [ + "should-not-be-supported", + "vp9", + "vp9.0", + "vp8", + "vp8.0", + "avc1", + "av1", + "h265", + "h.265", + "h264", + "h.264", + "opus", + "pcm", + "aac", + "mpeg", + "mp4a", + ]; + const supported: Array = []; + const isSupported = MediaRecorder.isTypeSupported; + const types = media === "video" ? videoTypes : audioTypes; + + types.forEach((type: string) => { + const mimeType = `${media}/${type}`; + // without codecs + isSupported(mimeType) && supported.push(mimeType); + + // with codecs + codecs.forEach((codec) => + [ + `${mimeType};codecs=${codec}`, + `${mimeType};codecs=${codec.toUpperCase()}`, + ].forEach( + (variation) => isSupported(variation) && supported.push(variation), + ), + ); + }); + return supported[0]; +}; + export function AutoBind(target: any, _: string, descriptor: any) { if (typeof descriptor.value === "function") descriptor.value = descriptor.value.bind(target); diff --git a/app/client/src/widgets/CameraWidget/component/index.tsx b/app/client/src/widgets/CameraWidget/component/index.tsx index 1b4cf640a1..e2bdaa60aa 100644 --- a/app/client/src/widgets/CameraWidget/component/index.tsx +++ b/app/client/src/widgets/CameraWidget/component/index.tsx @@ -1,4 +1,10 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import styled, { css } from "styled-components"; import { Button, Icon, Menu, MenuItem } from "@blueprintjs/core"; import { Popover2 } from "@blueprintjs/popover2"; @@ -22,7 +28,12 @@ import { SupportedLayouts } from "reducers/entityReducers/pageListReducer"; import { getCurrentApplicationLayout } from "selectors/editorSelectors"; import { useSelector } from "store"; import { Colors } from "constants/Colors"; -import { TooltipComponent } from "design-system"; +import { + getBrowserInfo, + getPlatformOS, + getSupportedMimeTypes, + PLATFORM_OS, +} from "utils/helpers"; import { CameraMode, @@ -95,6 +106,7 @@ const CameraContainer = styled.div` video { height: 100%; } + background: ${Colors.BLACK}; } `; @@ -144,6 +156,11 @@ const MediaInputsContainer = styled.div` display: flex; flex: 1; justify-content: flex-start; + + & .bp3-minimal { + height: 30px; + width: 40px; + } `; const MainControlContainer = styled.div` @@ -166,6 +183,15 @@ const TimerContainer = styled.div` color: #ffffff; `; +const DeviceButtonContainer = styled.div` + position: relative; +`; + +const DeviceMenuContainer = styled.div` + position: absolute; + bottom: 34px; +`; + export interface StyledButtonProps { variant: ButtonVariant; borderRadius: ButtonBorderRadius; @@ -201,7 +227,6 @@ export interface ControlPanelProps { fullScreenHandle: FullScreenHandle; onImageCapture: () => void; onImageSave: () => void; - onError: (errorMessage: string) => void; onMediaInputChange: (mediaDeviceInfo: MediaDeviceInfo) => void; onRecordingStart: () => void; onRecordingStop: () => void; @@ -221,7 +246,6 @@ function ControlPanel(props: ControlPanelProps) { audioMuted, fullScreenHandle, mode, - onError, onImageCapture, onImageSave, onMediaInputChange, @@ -238,27 +262,66 @@ function ControlPanel(props: ControlPanelProps) { videoInputs, videoMuted, } = props; + const [isOpenAudioDeviceMenu, setIsOpenAudioDeviceMenu] = useState( + false, + ); + const [isOpenVideoDeviceMenu, setIsOpenVideoDeviceMenu] = useState( + false, + ); + + // disable the camera and audio during the video recording + const isDisableCameraAndAudioMenu = useMemo(() => { + return ( + mode === CameraModeTypes.VIDEO && + status === MediaCaptureStatusTypes.VIDEO_RECORDING + ); + }, [mode, status]); + + // Close the device menu by user click anywhere on the screen when fullscreen is true + useEffect(() => { + if (fullScreenHandle.active) { + const handleClickOutside = () => { + if (fullScreenHandle.node.current) { + isOpenVideoDeviceMenu && setIsOpenVideoDeviceMenu(false); + isOpenAudioDeviceMenu && setIsOpenAudioDeviceMenu(false); + } + }; + + document.addEventListener("click", handleClickOutside, false); + return () => { + document.removeEventListener("click", handleClickOutside, false); + }; + } + }, [fullScreenHandle, isOpenVideoDeviceMenu, isOpenAudioDeviceMenu]); + + // Close the device menu when user exit from full screen mode + useEffect(() => { + if (fullScreenHandle.active) { + setIsOpenVideoDeviceMenu(false); + setIsOpenAudioDeviceMenu(false); + } + }, [fullScreenHandle]); + + const handleOnAudioCaretClick = (isMenuOpen: boolean) => { + isOpenVideoDeviceMenu && setIsOpenVideoDeviceMenu(false); + setIsOpenAudioDeviceMenu(isMenuOpen); + }; + + const handleOnVideoCaretClick = (isMenuOpen: boolean) => { + isOpenAudioDeviceMenu && setIsOpenAudioDeviceMenu(false); + setIsOpenVideoDeviceMenu(isMenuOpen); + }; const handleControlClick = (action: MediaCaptureAction) => { return () => { switch (action) { case MediaCaptureActionTypes.IMAGE_CAPTURE: - // First, check for media device permissions - navigator.mediaDevices - .getUserMedia({ video: true, audio: false }) - .then(() => { - onImageCapture(); - onStatusChange(MediaCaptureStatusTypes.IMAGE_CAPTURED); - }) - .catch((err) => { - onError(err.message); - }); - + onImageCapture(); + onStatusChange(MediaCaptureStatusTypes.IMAGE_CAPTURED); break; case MediaCaptureActionTypes.IMAGE_SAVE: onImageSave(); onStatusChange(MediaCaptureStatusTypes.IMAGE_SAVED); - break; case MediaCaptureActionTypes.IMAGE_DISCARD: onResetMedia(); @@ -270,17 +333,8 @@ function ControlPanel(props: ControlPanelProps) { break; case MediaCaptureActionTypes.RECORDING_START: - // First, check for media device permissions - navigator.mediaDevices - .getUserMedia({ video: true, audio: true }) - .then(() => { - onRecordingStart(); - onStatusChange(MediaCaptureStatusTypes.VIDEO_RECORDING); - }) - .catch((err) => { - onError(err.message); - }); - + onRecordingStart(); + onStatusChange(MediaCaptureStatusTypes.VIDEO_RECORDING); break; case MediaCaptureActionTypes.RECORDING_STOP: onRecordingStop(); @@ -321,23 +375,40 @@ function ControlPanel(props: ControlPanelProps) { }; const renderMediaDeviceSelectors = () => { + const browserInfo = getBrowserInfo(); + const isSafari = + getPlatformOS() === PLATFORM_OS.IOS || + (getPlatformOS() === PLATFORM_OS.MAC && + typeof browserInfo === "object" && + browserInfo?.browser === "Safari"); + return ( <> {mode === CameraModeTypes.VIDEO && ( )} ); @@ -347,283 +418,225 @@ function ControlPanel(props: ControlPanelProps) { switch (status) { case MediaCaptureStatusTypes.IMAGE_DEFAULT: return ( - - } - onClick={handleControlClick( - MediaCaptureActionTypes.IMAGE_CAPTURE, - )} - variant={ButtonVariantTypes.SECONDARY} - /> - + } + onClick={handleControlClick(MediaCaptureActionTypes.IMAGE_CAPTURE)} + variant={ButtonVariantTypes.SECONDARY} + /> ); case MediaCaptureStatusTypes.IMAGE_CAPTURED: return ( <> - - } - onClick={handleControlClick(MediaCaptureActionTypes.IMAGE_SAVE)} - variant={ButtonVariantTypes.PRIMARY} - /> - - - } - onClick={handleControlClick( - MediaCaptureActionTypes.IMAGE_DISCARD, - )} - variant={ButtonVariantTypes.TERTIARY} - /> - + } + onClick={handleControlClick(MediaCaptureActionTypes.IMAGE_SAVE)} + variant={ButtonVariantTypes.PRIMARY} + /> + } + onClick={handleControlClick( + MediaCaptureActionTypes.IMAGE_DISCARD, + )} + variant={ButtonVariantTypes.TERTIARY} + /> ); case MediaCaptureStatusTypes.IMAGE_SAVED: return ( - - } - onClick={handleControlClick( - MediaCaptureActionTypes.IMAGE_REFRESH, - )} - variant={ButtonVariantTypes.TERTIARY} - /> - + } + onClick={handleControlClick(MediaCaptureActionTypes.IMAGE_REFRESH)} + variant={ButtonVariantTypes.TERTIARY} + /> ); case MediaCaptureStatusTypes.VIDEO_DEFAULT: return ( - - } - onClick={handleControlClick( - MediaCaptureActionTypes.RECORDING_START, - )} - variant={ButtonVariantTypes.SECONDARY} - /> - + } + onClick={handleControlClick( + MediaCaptureActionTypes.RECORDING_START, + )} + variant={ButtonVariantTypes.SECONDARY} + /> ); case MediaCaptureStatusTypes.VIDEO_RECORDING: return ( <> - - } - onClick={handleControlClick( - MediaCaptureActionTypes.RECORDING_STOP, - )} - variant={ButtonVariantTypes.SECONDARY} - /> - - - } - onClick={handleControlClick( - MediaCaptureActionTypes.RECORDING_DISCARD, - )} - variant={ButtonVariantTypes.TERTIARY} - /> - + } + onClick={handleControlClick( + MediaCaptureActionTypes.RECORDING_STOP, + )} + variant={ButtonVariantTypes.SECONDARY} + /> + } + onClick={handleControlClick( + MediaCaptureActionTypes.RECORDING_DISCARD, + )} + variant={ButtonVariantTypes.TERTIARY} + /> ); case MediaCaptureStatusTypes.VIDEO_CAPTURED: return ( <> - - } - onClick={handleControlClick( - MediaCaptureActionTypes.RECORDING_SAVE, - )} - variant={ButtonVariantTypes.PRIMARY} - /> - - - } - onClick={handleControlClick(MediaCaptureActionTypes.VIDEO_PLAY)} - variant={ButtonVariantTypes.TERTIARY} - /> - - - } - onClick={handleControlClick( - MediaCaptureActionTypes.RECORDING_DISCARD, - )} - variant={ButtonVariantTypes.TERTIARY} - /> - + } + onClick={handleControlClick( + MediaCaptureActionTypes.RECORDING_SAVE, + )} + variant={ButtonVariantTypes.PRIMARY} + /> + } + onClick={handleControlClick(MediaCaptureActionTypes.VIDEO_PLAY)} + variant={ButtonVariantTypes.TERTIARY} + /> + } + onClick={handleControlClick( + MediaCaptureActionTypes.RECORDING_DISCARD, + )} + variant={ButtonVariantTypes.TERTIARY} + /> ); case MediaCaptureStatusTypes.VIDEO_PLAYING: return ( <> - - } - onClick={handleControlClick( - MediaCaptureActionTypes.RECORDING_SAVE, - )} - variant={ButtonVariantTypes.PRIMARY} - /> - - - } - onClick={handleControlClick( - MediaCaptureActionTypes.VIDEO_PAUSE, - )} - variant={ButtonVariantTypes.TERTIARY} - /> - - - } - onClick={handleControlClick( - MediaCaptureActionTypes.RECORDING_DISCARD, - )} - variant={ButtonVariantTypes.TERTIARY} - /> - + } + onClick={handleControlClick( + MediaCaptureActionTypes.RECORDING_SAVE, + )} + variant={ButtonVariantTypes.PRIMARY} + /> + } + onClick={handleControlClick(MediaCaptureActionTypes.VIDEO_PAUSE)} + variant={ButtonVariantTypes.TERTIARY} + /> + } + onClick={handleControlClick( + MediaCaptureActionTypes.RECORDING_DISCARD, + )} + variant={ButtonVariantTypes.TERTIARY} + /> ); case MediaCaptureStatusTypes.VIDEO_PAUSED: return ( <> - - } - onClick={handleControlClick( - MediaCaptureActionTypes.RECORDING_SAVE, - )} - variant={ButtonVariantTypes.PRIMARY} - /> - - - } - onClick={handleControlClick(MediaCaptureActionTypes.VIDEO_PLAY)} - variant={ButtonVariantTypes.TERTIARY} - /> - - - } - onClick={handleControlClick( - MediaCaptureActionTypes.RECORDING_DISCARD, - )} - variant={ButtonVariantTypes.TERTIARY} - /> - + } + onClick={handleControlClick( + MediaCaptureActionTypes.RECORDING_SAVE, + )} + variant={ButtonVariantTypes.PRIMARY} + /> + } + onClick={handleControlClick(MediaCaptureActionTypes.VIDEO_PLAY)} + variant={ButtonVariantTypes.TERTIARY} + /> + } + onClick={handleControlClick( + MediaCaptureActionTypes.RECORDING_DISCARD, + )} + variant={ButtonVariantTypes.TERTIARY} + /> ); case MediaCaptureStatusTypes.VIDEO_SAVED: return ( <> - - } - onClick={handleControlClick( - MediaCaptureActionTypes.VIDEO_PLAY_AFTER_SAVE, - )} - variant={ButtonVariantTypes.TERTIARY} - /> - - - } - onClick={handleControlClick( - MediaCaptureActionTypes.VIDEO_REFRESH, - )} - variant={ButtonVariantTypes.TERTIARY} - /> - + } + onClick={handleControlClick( + MediaCaptureActionTypes.VIDEO_PLAY_AFTER_SAVE, + )} + variant={ButtonVariantTypes.TERTIARY} + /> + } + onClick={handleControlClick( + MediaCaptureActionTypes.VIDEO_REFRESH, + )} + variant={ButtonVariantTypes.TERTIARY} + /> ); case MediaCaptureStatusTypes.VIDEO_PLAYING_AFTER_SAVE: return ( <> - - } - onClick={handleControlClick( - MediaCaptureActionTypes.VIDEO_PAUSE_AFTER_SAVE, - )} - variant={ButtonVariantTypes.TERTIARY} - /> - - - } - onClick={handleControlClick( - MediaCaptureActionTypes.VIDEO_REFRESH, - )} - variant={ButtonVariantTypes.TERTIARY} - /> - + } + onClick={handleControlClick( + MediaCaptureActionTypes.VIDEO_PAUSE_AFTER_SAVE, + )} + variant={ButtonVariantTypes.TERTIARY} + /> + } + onClick={handleControlClick( + MediaCaptureActionTypes.VIDEO_REFRESH, + )} + variant={ButtonVariantTypes.TERTIARY} + /> ); case MediaCaptureStatusTypes.VIDEO_PAUSED_AFTER_SAVE: return ( <> - - } - onClick={handleControlClick( - MediaCaptureActionTypes.VIDEO_PLAY_AFTER_SAVE, - )} - variant={ButtonVariantTypes.TERTIARY} - /> - - - } - onClick={handleControlClick( - MediaCaptureActionTypes.VIDEO_REFRESH, - )} - variant={ButtonVariantTypes.TERTIARY} - /> - + } + onClick={handleControlClick( + MediaCaptureActionTypes.VIDEO_PLAY_AFTER_SAVE, + )} + variant={ButtonVariantTypes.TERTIARY} + /> + } + onClick={handleControlClick( + MediaCaptureActionTypes.VIDEO_REFRESH, + )} + variant={ButtonVariantTypes.TERTIARY} + /> ); @@ -633,34 +646,27 @@ function ControlPanel(props: ControlPanelProps) { }; const renderFullscreenControl = () => { + // Remove fullscreen functionality for ios mobile devices + // due to fullscreen API is not supported to ios mobile devices. + // https://caniuse.com/fullscreen + if (getPlatformOS() === PLATFORM_OS.IOS) return null; + return fullScreenHandle.active ? ( - - } iconSize={20} /> - } - onClick={fullScreenHandle.exit} - variant={ButtonVariantTypes.TERTIARY} - /> - + } iconSize={20} /> + } + onClick={fullScreenHandle.exit} + variant={ButtonVariantTypes.TERTIARY} + /> ) : ( - - } iconSize={20} />} - onClick={fullScreenHandle.enter} - variant={ButtonVariantTypes.TERTIARY} - /> - + } iconSize={20} />} + onClick={fullScreenHandle.enter} + variant={ButtonVariantTypes.TERTIARY} + /> ); }; @@ -727,13 +733,29 @@ function DeviceMenu(props: DeviceMenuProps) { export interface DevicePopoverProps { deviceType: DeviceType; disabled?: boolean; + disabledIcon?: boolean; + disabledMenu?: boolean; + fullScreenHandle: FullScreenHandle; + isMenuOpen: boolean; items: MediaDeviceInfo[]; onDeviceMute?: (isMute: boolean) => void; onItemClick: (item: MediaDeviceInfo) => void; + onMenuClick: (isMenuOpen: boolean) => void; } function DevicePopover(props: DevicePopoverProps) { - const { deviceType, disabled, items, onDeviceMute, onItemClick } = props; + const { + deviceType, + disabled, + disabledIcon, + disabledMenu, + fullScreenHandle, + isMenuOpen, + items, + onDeviceMute, + onItemClick, + onMenuClick, + } = props; const handleDeviceMute = useCallback(() => { if (onDeviceMute) { @@ -757,16 +779,39 @@ function DevicePopover(props: DevicePopoverProps) { return ( <>