Skip to content

Commit

Permalink
feat: switch cameras and allow translations (#88)
Browse files Browse the repository at this point in the history
Co-authored-by: George Jordanov <[email protected]>
Co-authored-by: Francisco Baio Dias <[email protected]>
  • Loading branch information
3 people committed May 3, 2021
1 parent d416ec0 commit f780b19
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 39 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,5 @@ storybook-static

.DS_Store
.npmrc

package-lock.json
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"lint": "eslint .",
"clean": "rm -rf lib",
"build": "babel src -d lib --copy-files --ignore **/*.stories.js,**/*.test.js,**/*.browser-test.js",
"build:watch": "babel --watch src -d lib --copy-files --ignore **/*.stories.js,**/*.test.js,**/*.browser-test.js",
"prebuild": "yarn run clean",
"prepublish": "NODE_ENV=production yarn run build",
"start": "yarn run storybook",
Expand Down
7 changes: 7 additions & 0 deletions src/custom-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,10 @@ export class ReactVideoRecorderMediaRecorderUnavailableError extends Error {
this.name = 'ReactVideoRecorderMediaRecorderUnavailableError'
}
}

export class ReactVideoRecorderDeviceUnavailableError extends Error {
constructor () {
super("Couldn't get selected device")
this.name = 'ReactVideoRecorderDeviceUnavailableError'
}
}
7 changes: 4 additions & 3 deletions src/defaults/button.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ const Button = styled.button`
background: ${(props) => props.backgroundColor};
color: ${(props) => props.color};
border-radius: 4px;
padding: 17px 18px;
height: 40px;
padding: 0px 18px;
border: none;
margin: 5px;
font-size: 18px;
margin: -8px;
font-size: 14px;
font-weight: bold;
outline: none;
cursor: pointer;
Expand Down
17 changes: 12 additions & 5 deletions src/defaults/record-button.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import styled from 'styled-components'
import PropTypes from 'prop-types'

const Button = styled.button`
background: ${(props) => props.backgroundColor};
Expand All @@ -11,7 +12,7 @@ const Button = styled.button`
outline: none;
border: none;
cursor: pointer;
z-index: 5;
:hover {
background: #fb6d42;
}
Expand Down Expand Up @@ -49,16 +50,22 @@ Button.defaultProps = {
backgroundColor: 'white'
}

export default (props) => (
const RecordButton = ({ t, ...props }) => (
<RecWrapper>
<Instructions>
<div>PRESS </div>
<InstuctionsHighlight> REC </InstuctionsHighlight>
WHEN READY
<div>{t('PRESS')} </div>
<InstuctionsHighlight> {t('REC')} </InstuctionsHighlight>
{t('WHEN READY')}
</Instructions>

<ButtonBorder>
<Button {...props} />
</ButtonBorder>
</RecWrapper>
)

RecordButton.propTypes = {
t: PropTypes.func
}

export default RecordButton
11 changes: 7 additions & 4 deletions src/defaults/render-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const ActionsWrapper = styled.div`
`

const Actions = ({
t,
isVideoInputSupported,
isInlineRecordingSupported,
thereWasAnError,
Expand Down Expand Up @@ -63,7 +64,7 @@ const Actions = ({
onClick={onStopReplaying}
data-qa='start-replaying'
>
Use another video
{t('Use another video')}
</Button>
)
}
Expand All @@ -81,6 +82,7 @@ const Actions = ({
if (isCameraOn && streamIsReady) {
return (
<RecordButton
t={t}
type='button'
onClick={onStartRecording}
data-qa='start-recording'
Expand All @@ -91,18 +93,18 @@ const Actions = ({
if (useVideoInput) {
return (
<Button type='button' onClick={onOpenVideoInput} data-qa='open-input'>
Upload a video
{t('Upload a video')}
</Button>
)
}

return shouldUseVideoInput ? (
<Button type='button' onClick={onOpenVideoInput} data-qa='open-input'>
Record a video
{t('Record a video')}
</Button>
) : (
<Button type='button' onClick={onTurnOnCamera} data-qa='turn-on-camera'>
Turn my camera ON
{t('Turn my camera ON')}
</Button>
)
}
Expand All @@ -117,6 +119,7 @@ const Actions = ({
}

Actions.propTypes = {
t: PropTypes.func,
isVideoInputSupported: PropTypes.bool,
isInlineRecordingSupported: PropTypes.bool,
thereWasAnError: PropTypes.bool,
Expand Down
32 changes: 32 additions & 0 deletions src/defaults/switch-camera-view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react'
import SVGInline from 'react-svg-inline'
import styled from 'styled-components'

const SVGWrapper = styled.div`
width: 80px;
height: 80px;
bottom: 4px;
right: 4px;
z-index: 10;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 50%;
padding-left: 8px;
padding-top: 10px;
position: absolute;
cursor: pointer;
`

const icon = `
<svg width="64px" height="64px" viewBox="0 0 1300 1300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M1000.809 230.795h-136l-32.4-90.8a32.07 32.07 0 0 0-30.2-21.2h-306.8a31.945 31.945 0 0 0-30.1 21.2l-32.5 90.8h-136a79.978 79.978 0 0 0-80 80v456a79.978 79.978 0 0 0 80 80h704a79.978 79.978 0 0 0 80-80v-456a79.978 79.978 0 0 0-80-80zm8 536a8.023 8.023 0 0 1-8 8h-704a8.024 8.024 0 0 1-8-8v-456a8.024 8.024 0 0 1 8-8h186.7l17.1-47.8 22.9-64.2h250.5l22.9 64.2 17.1 47.8h186.8a8.024 8.024 0 0 1 8 8zm-360-400a160 160 0 1 0 160 160 159.956 159.956 0 0 0-160-160zm0 256a96 96 0 1 1 96-96 96.025 96.025 0 0 1-96 96z"/>
<path d="M646.53 1051.072L510.64 972.616a23.18 23.18 0 0 0-34.769 20.074v28.335c-209.332-23.19-359.49-86.314-359.49-160.553 0-19.125 9.968-37.511 28.341-54.668V710.41a317.567 317.567 0 0 0-32.93 21.447c-60.674 45.451-73.41 95.028-73.41 128.615s12.736 83.164 73.41 128.615c34.059 25.513 80.563 47.712 138.22 65.98 64.915 20.568 142.076 35.611 225.86 44.406v50.128a23.18 23.18 0 0 0 34.769 20.074l135.888-78.456a23.18 23.18 0 0 0 0-40.147zM1194.334 731.857a330.209 330.209 0 0 0-41.61-26.256v92.688c23.895 19.26 37.02 40.236 37.02 62.183 0 76.572-159.746 141.32-379.404 162.621v78.34c91.47-8.42 175.747-24.178 245.774-46.366 57.658-18.268 104.162-40.467 138.22-65.98 60.675-45.451 73.41-95.028 73.41-128.615s-12.735-83.164-73.41-128.615z"/>
</svg>
`

const SwitchCameraView = (props) => (
<SVGWrapper {...props}>
<SVGInline svg={icon} fill='white' />
</SVGWrapper>
)

export default SwitchCameraView
124 changes: 97 additions & 27 deletions src/video-recorder.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import UnsupportedView from './defaults/unsupported-view'
import ErrorView from './defaults/error-view'
import DisconnectedView from './defaults/disconnected-view'
import LoadingView from './defaults/loading-view'
import SwitchCameraView from './defaults/switch-camera-view'
import renderActions from './defaults/render-actions'
import getVideoInfo, { captureThumb } from './get-video-info'
import {
ReactVideoRecorderDataIssueError,
ReactVideoRecorderRecordedBlobsUnavailableError,
ReactVideoRecorderDataAvailableTimeoutError,
ReactVideoRecorderMediaRecorderUnavailableError
ReactVideoRecorderMediaRecorderUnavailableError,
ReactVideoRecorderDeviceUnavailableError
} from './custom-errors'

const MIME_TYPES = [
Expand Down Expand Up @@ -105,8 +107,12 @@ export default class VideoRecorder extends Component {
renderErrorView: PropTypes.func,
renderActions: PropTypes.func,

/** Use this to localize the texts */
t: PropTypes.func,

onCameraOn: PropTypes.func,
onTurnOnCamera: PropTypes.func,
onSwitchCamera: PropTypes.func,
onTurnOffCamera: PropTypes.func,
onStartRecording: PropTypes.func,
onStopRecording: PropTypes.func,
Expand All @@ -124,6 +130,7 @@ export default class VideoRecorder extends Component {
renderVideoInputView: ({ videoInput }) => <>{videoInput}</>,
renderDisconnectedView: () => <DisconnectedView />,
renderLoadingView: () => <LoadingView />,
t: (x) => x,
renderActions,
isFlipped: true,
countdownTime: 3000,
Expand All @@ -149,7 +156,9 @@ export default class VideoRecorder extends Component {
streamIsReady: false,
isInlineRecordingSupported: null,
isVideoInputSupported: null,
stream: undefined
stream: undefined,
currentDeviceId: null,
availableDeviceIds: []
}

componentDidMount () {
Expand Down Expand Up @@ -201,42 +210,91 @@ export default class VideoRecorder extends Component {
this.isComponentUnmounted = true
}

turnOnCamera = () => {
turnOnCamera = (deviceId = null) => {
if (this.props.onTurnOnCamera) {
this.props.onTurnOnCamera()
}

this.setState({
isConnecting: true,
isReplayingVideo: false,
thereWasAnError: false,
error: null
})

const fallbackContraints = {
audio: true,
video: true
}

navigator.mediaDevices
.getUserMedia(this.props.constraints)
.catch((err) => {
// there's a bug in chrome in some windows computers where using `ideal` in the constraints throws a NotReadableError
.enumerateDevices()
.then((mediaDevices) => {
const videoDevices = mediaDevices.filter((x) => x.kind === 'videoinput')
if (
err.name === 'NotReadableError' ||
err.name === 'OverconstrainedError'
deviceId &&
videoDevices[0] &&
videoDevices.find((x) => x.deviceId) === undefined
) {
console.warn(
`Got ${err.name}, trying getUserMedia again with fallback constraints`
return this.handleError(
new ReactVideoRecorderDeviceUnavailableError()
)
return navigator.mediaDevices.getUserMedia(fallbackContraints)
}
throw err

const currentDeviceId =
typeof deviceId === 'string' ? deviceId : videoDevices[0].deviceId

this.setState({
isConnecting: true,
isReplayingVideo: false,
thereWasAnError: false,
currentDeviceId,
availableDeviceIds: videoDevices.map((x) => x.deviceId),
error: null
})

const fallbackContraints = {
audio: true,
video: true
}

const currentConstraints = {
...this.props.constraints,
video: {
deviceId: {
exact: currentDeviceId
}
}
}

navigator.mediaDevices
.getUserMedia(currentConstraints)
.catch((err) => {
// there's a bug in chrome in some windows computers where using `ideal` in the constraints throws a NotReadableError
if (
err.name === 'NotReadableError' ||
err.name === 'OverconstrainedError'
) {
console.warn(
`Got ${err.name}, trying getUserMedia again with fallback constraints`
)
return navigator.mediaDevices.getUserMedia(fallbackContraints)
}
throw err
})
.then(this.handleSuccess)
.catch(this.handleError)
})
.then(this.handleSuccess)
.catch(this.handleError)
}

handleSwitchCamera = () => {
if (this.props.onSwitchCamera) {
this.props.onSwitchCamera()
}
const { currentDeviceId, availableDeviceIds } = this.state
// Stop media tracks
this.stream && this.stream.getTracks().forEach((stream) => stream.stop())

const index = availableDeviceIds.findIndex((x) => x === currentDeviceId)
const maxIndex = availableDeviceIds.length - 1

if (index < 0) {
return this.handleError(new ReactVideoRecorderDeviceUnavailableError())
}

if (index + 1 > maxIndex) return this.turnOnCamera(availableDeviceIds[0])
return this.turnOnCamera(availableDeviceIds[index + 1])
}

turnOffCamera = () => {
if (this.props.onTurnOffCamera) {
this.props.onTurnOffCamera()
Expand Down Expand Up @@ -671,7 +729,9 @@ export default class VideoRecorder extends Component {
error,
isCameraOn,
isConnecting,
isReplayVideoMuted
isReplayVideoMuted,
isRecording,
availableDeviceIds
} = this.state

const shouldUseVideoInput =
Expand Down Expand Up @@ -721,6 +781,12 @@ export default class VideoRecorder extends Component {
}

if (isCameraOn) {
// Enable switch camera button, only if not recording and multiple video sources available
const switchCameraControl =
availableDeviceIds && availableDeviceIds.length >= 1 && !isRecording ? (
<SwitchCameraView onClick={this.handleSwitchCamera} />
) : null

return (
<CameraView key='camera'>
<Video
Expand All @@ -729,6 +795,7 @@ export default class VideoRecorder extends Component {
autoPlay
muted
/>
{switchCameraControl}
</CameraView>
)
}
Expand Down Expand Up @@ -760,13 +827,15 @@ export default class VideoRecorder extends Component {
showReplayControls,
replayVideoAutoplayAndLoopOff,
renderActions,
t,
useVideoInput
} = this.props

return (
<Wrapper>
{this.renderCameraView()}
{renderActions({
t,
isVideoInputSupported,
isInlineRecordingSupported,
thereWasAnError,
Expand All @@ -783,7 +852,8 @@ export default class VideoRecorder extends Component {
replayVideoAutoplayAndLoopOff,
useVideoInput,

onTurnOnCamera: this.turnOnCamera,
onTurnOnCamera: () => this.turnOnCamera(),
onSwitchCamera: this.handleSwitchCamera,
onTurnOffCamera: this.turnOffCamera,
onOpenVideoInput: this.handleOpenVideoInput,
onStartRecording: this.handleStartRecording,
Expand Down

1 comment on commit f780b19

@vercel
Copy link

@vercel vercel bot commented on f780b19 May 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.