From 975a5eb6031a838cd2624836579294a4136f709a Mon Sep 17 00:00:00 2001 From: ok2ju Date: Tue, 19 Feb 2019 17:58:57 +0300 Subject: [PATCH 1/9] raw wrtc components --- src/components/FUTURE/Provider.jsx | 79 +++++++++ src/components/FUTURE/RequestUserMedia.jsx | 185 +++++++++++++++++++++ src/components/FUTURE/rum.js | 107 ++++++++++++ 3 files changed, 371 insertions(+) create mode 100644 src/components/FUTURE/Provider.jsx create mode 100644 src/components/FUTURE/RequestUserMedia.jsx create mode 100644 src/components/FUTURE/rum.js diff --git a/src/components/FUTURE/Provider.jsx b/src/components/FUTURE/Provider.jsx new file mode 100644 index 0000000..cbaa7b5 --- /dev/null +++ b/src/components/FUTURE/Provider.jsx @@ -0,0 +1,79 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +const mapStateToProps = state => ({ + connectionState: '', + localMedia: '' +}) + +const mapDispatchToProps = dispatch => ({ + connect: () => {}, + disconnect: () => {}, + removeAllMedia: () => {} +}) + +class Provider extends Component { + componentDidMount () { + this.props.connect() + } + + componentWillMount () { + this.props.disconnect() + } + + render () { + const renderProps = { + connectionState: this.props.connectionState + } + + let render = this.props.render + if (!render && typeof this.props.children === 'function') { + render = this.props.children + } + + return render ? render(renderProps) : this.props.children + } +} + +const createConnectionStateComponent = (connectionState) => { + return connect(mapStateToProps)((props) => { + const renderProps = { + connectionState: props.connectionState + } + + let render = props.render + if (!render && typeof props.children === 'function') { + render = props.children + } + + if (this.props.connectionState === connectionState) { + return render ? render(renderProps) : props.children + } + + return null + }) +} + +export const NotConnected = connect(mapStateToProps, mapDispatchToProps)((props) => { + const renderProps = { + connectionState: props.connectionState + } + + let render = props.render + if (!render && typeof props.children === 'function') { + render = props.children + } + + if (props.connectionState !== 'connected') { + return render ? render(renderProps) : props.children + } + + return null +}) + +export const Connecting = createConnectionStateComponent('connecting') +export const Connected = createConnectionStateComponent('connected') +export const Disconnected = createConnectionStateComponent('disconnected') +export const Failed = createConnectionStateComponent('failed') + +export default connect(mapStateToProps, mapDispatchToProps)(Provider) diff --git a/src/components/FUTURE/RequestUserMedia.jsx b/src/components/FUTURE/RequestUserMedia.jsx new file mode 100644 index 0000000..a87fd7f --- /dev/null +++ b/src/components/FUTURE/RequestUserMedia.jsx @@ -0,0 +1,185 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +const mapStateToProps = (state, props) => { + const permissions = Selectors.getDevicePermissions(state) + + return { + ...props, + requestingCameraCapture: permissions.requestingCameraCapture, + requestingCapture: permissions.requestingCapture, + requestingMicrophoneCapture: permissions.requestingMicrophoneCapture + } +} + +const mapDispatchToProps = dispatch => ({ + addLocalAudio: (track, stream, replace) => dispatch(Actions.addLocalAudio(track, stream, replace)), + addLocalVideo: (track, stream, mirrored, replace) => dispatch(Actions.addLocalVideo(track, stream, mirrored, replace)), + cameraPermissionDenied: (err) => dispatch(Actions.cameraPermissionDenied(err)), + deviceCaptureRequest: (camera, microphone) => dispatch(Actions.deviceCaptureRequest(camera, microphone)), + fetchDevices: () => dispatch(Actions.fetchDevices()), + microphonePermissionDenied: (err) => dispatch(Actions.microphonePermissionDenied(err)), + removeAllMedia: (kind) => dispatch(Actions.removeAllMedia(kind)), + shareLocalMedia: (id) => dispatch(Actions.shareLocalMedia(id)) +}) + +const mergeConstraints = (defaults, provided, additional) => { + var disabled = (additional === false) || (!additional && !provided) + if (disabled) { + return false + } + + provided = (provided === true) ? {} : provided + additional = (additional === true) ? {} : additional + return { + ...defaults, + ...provided, + ...additional + } +} + +class RequestUserMedia extends Component { + constructor (props) { + super(props) + this.errorCount = 0 + } + + componentDidMount () { + if (this.props.auto) { + this.getMedia() + } + } + + componentDidUpdate (prevProps) { + if (this.props.auto && this.props.auto !== prevProps.auto) { + this.getMedia() + } + } + + async getMedia (additional = {}) { + let audioConstraints, videoConstraints, stream + const defaultAudioConstraints = {} + const supportedConstraints = navigator.mediaDevices.getSupportedConstraints() + + for (let constraint of ['autoGainControl', 'echoCancellation', 'noiseSuppression']) { + if (supportedConstraints[constraint]) { + defaultAudioConstraints[constraint] = true + } + } + + audioConstraints = mergeConstraints(defaultAudioConstraints, this.props.audio, additional.audio) + videoConstraints = mergeConstraints({}, this.props.video, additional.video) + + try { + if (!navigator.mediaDevices) { + throw new Error('getUserMedia not supported') + } + + this.props.deviceCaptureRequest(!!videoConstraints, !!audioConstraints) + + if (!audioConstraints) { + return + } + + await this.props.removeAllMedia('audio') + + try { + stream = await navigator.mediaDevices.getUserMedia({ + audio: audioConstraints, + video: videoConstraints + }) + } catch (error) { + this.errorCount += 1 + + if (error.name === 'AbortError' && this.errorCount < 12) { + setTimeout(() => this.getMedia(additional), 100 + Math.pow(2, this.errorCount)) + return {} + } + + if (error.name === 'NotAllowedError' || error.name === 'SecurityError') { + if (audioConstraints) { + this.props.microphonePermissionDenied() + } + + if (videoConstraints) { + this.props.cameraPermissionDenied() + } + } + + this.props.deviceCaptureRequest(false, false) + if (this.props.onError) { + this.props.onError(error) + } + + return {} + } + + this.errorCount = 0 + + const audio = stream.getAudioTracks()[0] + const video = stream.getVideoTracks()[0] + + if (audio) { + this.props.addLocalAudio(audio, stream, this.props.replaceAudio) + if (this.props.share !== false) { + this.props.shareLocalMedia(audio.id) + } + } else if (audioConstraints) { + this.props.microphonePermissionDenied() + } + + if (video) { + this.props.addLocalVideo(video, stream, this.props.mirrored, this.props.replaceVideo) + if (this.props.share !== false) { + this.props.shareLocalMedia(video.id) + } + } else if (videoConstraints) { + this.props.cameraPermissionDenied() + } + + await this.props.fetchDevices() + + await this.props.deviceCaptureRequest(false, false) + + const trackIds = { + audio: audio ? audio.id : undefined, + video: video ? video.id : undefined + } + + if (this.props.onSuccess) { + this.props.onSuccess(trackIds) + } + + return trackIds + } catch (error) { + + } + } + + render () { + const renderProps = this.getMedia() + let render = this.props.render + + if (!render && typeof this.props.children === 'function') { + render = this.props.children + } + + if (render) { + return render(renderProps) + } else if (this.props.children) { + return this.props.children + } + + if (this.props.auto) { + return null + } else { + return ( + + ) + } + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(RequestUserMedia) diff --git a/src/components/FUTURE/rum.js b/src/components/FUTURE/rum.js new file mode 100644 index 0000000..5050182 --- /dev/null +++ b/src/components/FUTURE/rum.js @@ -0,0 +1,107 @@ +async function getMedia (additional = {}) { + let audioConstraints, videoConstraints, stream + const defaultAudioConstraints = {} + const supportedConstraints = navigator.mediaDevices.getSupportedConstraints() + + for (let constraint of ['autoGainControl', 'echoCancellation', 'noiseSuppression']) { + if (supportedConstraints[constraint]) { + defaultAudioConstraints[constraint] = true + } + } + + audioConstraints = mergeConstraints(defaultAudioConstraints, this.props.audio, additional.audio) + videoConstraints = mergeConstraints({}, this.props.video, additional.video) + + try { + /* Block #1 - start */ + if (!navigator.mediaDevices) { + throw new Error('getUserMedia not supported') + } + + this.props.deviceCaptureRequest(!!videoConstraints, !!audioConstraints) + + if (!audioConstraints) { + return + } + + await this.props.removeAllMedia('audio') + /* Block #1 - end */ + + /* Block #3 - start */ + stream = await navigator.mediaDevices.getUserMedia({ + audio: audioConstraints, + video: videoConstraints + }) + /* Block #3 - end */ + + /* Block #6 - start */ + this.errorCount = 0 + + const audio = stream.getAudioTracks()[0] + const video = stream.getVideoTracks()[0] + + if (audio) { + this.props.addLocalAudio(audio, stream, this.props.replaceAudio) + if (this.props.share !== false) { + this.props.shareLocalMedia(audio.id) + } + } else if (audioConstraints) { + this.props.microphonePermissionDenied() + } + + if (video) { + this.props.addLocalVideo(video, stream, this.props.mirrored, this.props.replaceVideo) + if (this.props.share !== false) { + this.props.shareLocalMedia(video.id) + } + } else if (videoConstraints) { + this.props.cameraPermissionDenied() + } + + await this.props.fetchDevices() + /* Block #6 - end */ + + /* Block #7 - start */ + await this.props.deviceCaptureRequest(false, false) + /* Block #7 - end */ + + /* Block #8 - start */ + const trackIds = { + audio: audio ? audio.id : undefined, + video: video ? video.id : undefined + } + + if (this.props.onSuccess) { + this.props.onSuccess(trackIds) + } + + return trackIds + /* Block #8 - end */ + } catch (error) { + /* Block #5 - start */ + this.errorCount += 1 + + if (error.name === 'AbortError' && this.errorCount < 12) { + setTimeout(() => this.getMedia(additional), 100 + Math.pow(2, this.errorCount)) + return {} + } + + if (error.name === 'NotAllowedError' || error.name === 'SecurityError') { + if (audioConstraints) { + this.props.microphonePermissionDenied() + } + + if (videoConstraints) { + this.props.cameraPermissionDenied() + } + } + + this.props.deviceCaptureRequest(false, false) + if (this.props.onError) { + this.props.onError(error) + } + + return {} + /* Block #5 - end */ + } +} From 825bb3158d50f9e59d3f4bcf045a26f9d3fec4a7 Mon Sep 17 00:00:00 2001 From: ok2ju Date: Tue, 19 Feb 2019 20:37:06 +0300 Subject: [PATCH 2/9] add Video, Room components --- src/components/FUTURE/RequestUserMedia.jsx | 106 ++++++++++---------- src/components/FUTURE/Room.jsx | 87 +++++++++++++++++ src/components/FUTURE/Video.jsx | 48 +++++++++ src/components/FUTURE/rum.js | 107 --------------------- 4 files changed, 184 insertions(+), 164 deletions(-) create mode 100644 src/components/FUTURE/Room.jsx create mode 100644 src/components/FUTURE/Video.jsx delete mode 100644 src/components/FUTURE/rum.js diff --git a/src/components/FUTURE/RequestUserMedia.jsx b/src/components/FUTURE/RequestUserMedia.jsx index a87fd7f..199bdc1 100644 --- a/src/components/FUTURE/RequestUserMedia.jsx +++ b/src/components/FUTURE/RequestUserMedia.jsx @@ -77,83 +77,75 @@ class RequestUserMedia extends Component { this.props.deviceCaptureRequest(!!videoConstraints, !!audioConstraints) - if (!audioConstraints) { - return + if (audioConstraints) { + await this.props.removeAllMedia('audio') } - await this.props.removeAllMedia('audio') - - try { - stream = await navigator.mediaDevices.getUserMedia({ - audio: audioConstraints, - video: videoConstraints - }) - } catch (error) { - this.errorCount += 1 - - if (error.name === 'AbortError' && this.errorCount < 12) { - setTimeout(() => this.getMedia(additional), 100 + Math.pow(2, this.errorCount)) - return {} - } - - if (error.name === 'NotAllowedError' || error.name === 'SecurityError') { - if (audioConstraints) { - this.props.microphonePermissionDenied() - } - - if (videoConstraints) { - this.props.cameraPermissionDenied() - } - } - - this.props.deviceCaptureRequest(false, false) - if (this.props.onError) { - this.props.onError(error) - } + stream = await navigator.mediaDevices.getUserMedia({ + audio: audioConstraints, + video: videoConstraints + }) + } catch (error) { + this.errorCount += 1 + if (error.name === 'AbortError' && this.errorCount < 12) { + setTimeout(() => this.getMedia(additional), 100 + Math.pow(2, this.errorCount)) return {} } - this.errorCount = 0 - - const audio = stream.getAudioTracks()[0] - const video = stream.getVideoTracks()[0] + if (error.name === 'NotAllowedError' || error.name === 'SecurityError') { + if (!!audioConstraints) { + this.props.microphonePermissionDenied() + } - if (audio) { - this.props.addLocalAudio(audio, stream, this.props.replaceAudio) - if (this.props.share !== false) { - this.props.shareLocalMedia(audio.id) + if (!!videoConstraints) { + this.props.cameraPermissionDenied() } - } else if (audioConstraints) { - this.props.microphonePermissionDenied() } - if (video) { - this.props.addLocalVideo(video, stream, this.props.mirrored, this.props.replaceVideo) - if (this.props.share !== false) { - this.props.shareLocalMedia(video.id) - } - } else if (videoConstraints) { - this.props.cameraPermissionDenied() + this.props.deviceCaptureRequest(false, false) + if (this.props.onError) { + this.props.onError(error) } - await this.props.fetchDevices() + return {} + } + + this.errorCount = 0 - await this.props.deviceCaptureRequest(false, false) + const audio = stream.getAudioTracks()[0] + const video = stream.getVideoTracks()[0] - const trackIds = { - audio: audio ? audio.id : undefined, - video: video ? video.id : undefined + if (audio) { + this.props.addLocalAudio(audio, stream, this.props.replaceAudio) + if (this.props.share !== false) { + this.props.shareLocalMedia(audio.id) } + } else if (!!audioConstraints) { + this.props.microphonePermissionDenied() + } - if (this.props.onSuccess) { - this.props.onSuccess(trackIds) + if (video) { + this.props.addLocalVideo(video, stream, this.props.mirrored, this.props.replaceVideo) + if (this.props.share !== false) { + this.props.shareLocalMedia(video.id) } + } else if (!!videoConstraints) { + this.props.cameraPermissionDenied() + } - return trackIds - } catch (error) { + await this.props.fetchDevices() + await this.props.deviceCaptureRequest(false, false) + const trackIds = { + audio: audio ? audio.id : undefined, + video: video ? video.id : undefined + } + if (this.props.onSuccess) { + this.props.onSuccess(trackIds) } + + return trackIds } render () { diff --git a/src/components/FUTURE/Room.jsx b/src/components/FUTURE/Room.jsx new file mode 100644 index 0000000..422c337 --- /dev/null +++ b/src/components/FUTURE/Room.jsx @@ -0,0 +1,87 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +const mapStateToProps = (state, props) => { + let room + if (props.roomAddress) { + room = Selectors_1.getRoomByAddress(state, props.roomAddress) + } else if (props.name) { + room = Selectors_1.getRoomByProvidedName(state, props.name) + } + + return { + call: room ? Selectors_1.getCallForRoom(state, room.address) : undefined, + connectionState: Selectors_1.getConnectionState(state), + localMedia: Selectors_1.getLocalMedia(state), + peers: room ? Selectors_1.getPeersForRoom(state, room.address) : [], + remoteMedia: Selectors_1.getRemoteMedia(state), + room: room, + roomAddress: room ? room.address : undefined, + roomState: room ? room.roomState : 'joining' + } +} + +const mapDispatchToProps = (dispatch, props) => ({ + destroy: (roomAddress) => dispatch(Actions.destroyRoom(roomAddress)), + join: () => dispatch(Actions.joinRoom(props.name, { password: props.password || undefined })), + leave: (roomAddress) => dispatch(Actions.leaveRoom(roomAddress)), + lock: (roomAddress, password) => dispatch(Actions.lockRoom(roomAddress, password)), + unlock: (roomAddress) => dispatch(Actions.unlockRoom(roomAddress)), +}) + +class Room extends Component { + componentDidMount () { + if (this.props.connectionState === 'connected') { + this.props.join() + } + } + + componentDidUpdate (prevProps) { + if (this.props.connectionState !== 'connected') { + return + } + + if (this.props.connectionState !== prevProps.connectionState) { + this.props.join() + return + } + if (!this.props.room) { + return + } + + if (this.props.password !== prevProps.password) { + if (this.props.room.roomState === 'joined') { + if (this.props.password) { + this.props.lock(this.props.roomAddress, this.props.password) + } else { + this.props.unlock(this.props.roomAddress) + } + } else { + this.props.join() + } + } + } + + componentWillUnmount () { + this.props.leave(this.props.roomAddress) + } + + render() { + const renderProps = { + call: this.props.call || {}, + joined: this.props.room ? this.props.room.joined : false, + localMedia: this.props.localMedia || [], + peers: this.props.peers || [], + remoteMedia: this.props.remoteMedia || [], + room: this.props.room || {} + } + + let render = this.props.render + if (!render && typeof this.props.children === 'function') { + render = this.props.children + } + return render ? render(renderProps) : this.props.children + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Room) diff --git a/src/components/FUTURE/Video.jsx b/src/components/FUTURE/Video.jsx new file mode 100644 index 0000000..665b4c1 --- /dev/null +++ b/src/components/FUTURE/Video.jsx @@ -0,0 +1,48 @@ +import React, { Component } from 'react' + +class Video extends Component { + componentDidMount () { + this.setup(); + } + + componentDidUpdate () { + this.setup(); + } + + setup () { + if (!this.props.media || !this.video) { + return + } + + this.video.oncontextmenu = (event) => { + event.preventDefault() + } + + this.video.muted = true + this.video.autoplay = true + + if (this.video.srcObject !== this.props.media.stream) { + this.video.srcObject = this.props.media.stream + } + } + + render() { + if (!this.props.media || !this.props.media.loaded) { + return null + } + + return ( +