Do not join call automatically
Present a user with a menu to join call manually
This commit is contained in:
parent
22380ea381
commit
a8f3757d53
@ -55,6 +55,8 @@ overrides:
|
|||||||
- files:
|
- files:
|
||||||
- '*.test.ts'
|
- '*.test.ts'
|
||||||
- '*.test.tsx'
|
- '*.test.tsx'
|
||||||
|
- '**/__mocks__/*.ts'
|
||||||
|
- '**/__mocks__/*.tsx'
|
||||||
rules:
|
rules:
|
||||||
'@typescript-eslint/no-explicit-any': off
|
'@typescript-eslint/no-explicit-any': off
|
||||||
- files:
|
- files:
|
||||||
|
|||||||
@ -14,18 +14,16 @@ export class MediaStream {
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function getUserMedia () {
|
|
||||||
return !getUserMedia.shouldFail
|
|
||||||
? Promise.resolve(getUserMedia.stream)
|
|
||||||
: Promise.reject(new Error('test'))
|
|
||||||
}
|
|
||||||
getUserMedia.shouldFail = false
|
|
||||||
getUserMedia.fail = (shouldFail: boolean) =>
|
|
||||||
getUserMedia.shouldFail = shouldFail
|
|
||||||
getUserMedia.stream = new MediaStream()
|
|
||||||
|
|
||||||
export const navigator = window.navigator
|
export const navigator = window.navigator
|
||||||
|
|
||||||
|
;(window as any).navigator.mediaDevices = {}
|
||||||
|
window.navigator.mediaDevices.enumerateDevices = async () => {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
window.navigator.mediaDevices.getUserMedia = async () => {
|
||||||
|
return {} as any
|
||||||
|
}
|
||||||
|
|
||||||
export const play = jest.fn()
|
export const play = jest.fn()
|
||||||
|
|
||||||
export const valueOf = jest.fn()
|
export const valueOf = jest.fn()
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import * as CallActions from './CallActions'
|
|||||||
import * as SocketActions from './SocketActions'
|
import * as SocketActions from './SocketActions'
|
||||||
import * as constants from '../constants'
|
import * as constants from '../constants'
|
||||||
import socket from '../socket'
|
import socket from '../socket'
|
||||||
import { callId, getUserMedia } from '../window'
|
import { callId } from '../window'
|
||||||
import { bindActionCreators, createStore, AnyAction, combineReducers, applyMiddleware } from 'redux'
|
import { bindActionCreators, createStore, AnyAction, combineReducers, applyMiddleware } from 'redux'
|
||||||
import reducers from '../reducers'
|
import reducers from '../reducers'
|
||||||
import { middlewares } from '../middlewares'
|
import { middlewares } from '../middlewares'
|
||||||
@ -33,7 +33,6 @@ describe('CallActions', () => {
|
|||||||
applyMiddleware(...middlewares),
|
applyMiddleware(...middlewares),
|
||||||
)
|
)
|
||||||
callActions = bindActionCreators(CallActions, store.dispatch);
|
callActions = bindActionCreators(CallActions, store.dispatch);
|
||||||
(getUserMedia as any).fail(false);
|
|
||||||
(SocketActions.handshake as jest.Mock).mockReturnValue(jest.fn())
|
(SocketActions.handshake as jest.Mock).mockReturnValue(jest.fn())
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -55,19 +54,12 @@ describe('CallActions', () => {
|
|||||||
message: 'Connected to server socket',
|
message: 'Connected to server socket',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
},
|
},
|
||||||
}, {
|
|
||||||
type: constants.STREAM_ADD,
|
|
||||||
payload: {
|
|
||||||
stream: jasmine.anything(),
|
|
||||||
userId: constants.ME,
|
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
type: constants.INIT,
|
type: constants.INIT,
|
||||||
}])
|
}])
|
||||||
expect((SocketActions.handshake as jest.Mock).mock.calls).toEqual([[{
|
expect((SocketActions.handshake as jest.Mock).mock.calls).toEqual([[{
|
||||||
socket,
|
socket,
|
||||||
roomName: callId,
|
roomName: callId,
|
||||||
stream: (getUserMedia as any).stream,
|
|
||||||
}]])
|
}]])
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -94,7 +86,6 @@ describe('CallActions', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('dispatches alert when failed to get media stream', async () => {
|
it('dispatches alert when failed to get media stream', async () => {
|
||||||
(getUserMedia as any).fail(true)
|
|
||||||
const promise = callActions.init()
|
const promise = callActions.init()
|
||||||
socket.emit('connect', undefined)
|
socket.emit('connect', undefined)
|
||||||
await promise
|
await promise
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import * as constants from '../constants'
|
|
||||||
import socket from '../socket'
|
import socket from '../socket'
|
||||||
import { Dispatch, ThunkResult } from '../store'
|
import { Dispatch, ThunkResult } from '../store'
|
||||||
import { callId, getUserMedia } from '../window'
|
import { callId } from '../window'
|
||||||
import { ClientSocket } from '../socket'
|
import { ClientSocket } from '../socket'
|
||||||
import * as NotifyActions from './NotifyActions'
|
import * as NotifyActions from './NotifyActions'
|
||||||
import * as SocketActions from './SocketActions'
|
import * as SocketActions from './SocketActions'
|
||||||
import * as StreamActions from './StreamActions'
|
|
||||||
|
|
||||||
export interface InitAction {
|
export interface InitAction {
|
||||||
type: 'INIT'
|
type: 'INIT'
|
||||||
@ -23,12 +21,12 @@ const initialize = (): InitializeAction => ({
|
|||||||
export const init = (): ThunkResult<Promise<void>> =>
|
export const init = (): ThunkResult<Promise<void>> =>
|
||||||
async (dispatch, getState) => {
|
async (dispatch, getState) => {
|
||||||
const socket = await dispatch(connect())
|
const socket = await dispatch(connect())
|
||||||
const stream = await dispatch(getCameraStream())
|
// const stream = await dispatch(getCameraStream())
|
||||||
|
|
||||||
dispatch(SocketActions.handshake({
|
dispatch(SocketActions.handshake({
|
||||||
socket,
|
socket,
|
||||||
roomName: callId,
|
roomName: callId,
|
||||||
stream,
|
// stream,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
dispatch(initialize())
|
dispatch(initialize())
|
||||||
@ -48,16 +46,17 @@ export const connect = () => (dispatch: Dispatch) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCameraStream = () => async (dispatch: Dispatch) => {
|
// export const getCameraStream = () => async (dispatch: Dispatch) => {
|
||||||
try {
|
// try {
|
||||||
const stream = await getUserMedia({
|
// const stream = await getUserMedia({
|
||||||
video: { facingMode: 'user' },
|
// video: { facingMode: 'user' },
|
||||||
audio: true,
|
// audio: true,
|
||||||
})
|
// })
|
||||||
dispatch(StreamActions.addStream({ stream, userId: constants.ME }))
|
// dispatch(StreamActions.addStream({ stream, userId: constants.ME }))
|
||||||
return stream
|
// return stream
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
dispatch(NotifyActions.alert('Could not get access to microphone & camera'))
|
// dispatch(
|
||||||
return
|
// NotifyActions.alert('Could not get access to microphone & camera'))
|
||||||
}
|
// return
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import Chat from './Chat'
|
|||||||
import Notifications from './Notifications'
|
import Notifications from './Notifications'
|
||||||
import Toolbar from './Toolbar'
|
import Toolbar from './Toolbar'
|
||||||
import Video from './Video'
|
import Video from './Video'
|
||||||
|
import { Media } from './Media'
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
active: string | null
|
active: string | null
|
||||||
@ -84,6 +85,7 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
|||||||
/>
|
/>
|
||||||
<Alerts alerts={alerts} dismiss={dismissAlert} />
|
<Alerts alerts={alerts} dismiss={dismissAlert} />
|
||||||
<Notifications notifications={notifications} />
|
<Notifications notifications={notifications} />
|
||||||
|
<Media />
|
||||||
<Chat
|
<Chat
|
||||||
messages={messages}
|
messages={messages}
|
||||||
onClose={this.handleHideChat}
|
onClose={this.handleHideChat}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
jest.mock('../window')
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
@ -8,9 +10,7 @@ import { MEDIA_ENUMERATE } from '../constants'
|
|||||||
|
|
||||||
describe('Media', () => {
|
describe('Media', () => {
|
||||||
|
|
||||||
const onSave = jest.fn()
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks()
|
|
||||||
store = createStore()
|
store = createStore()
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: MEDIA_ENUMERATE,
|
type: MEDIA_ENUMERATE,
|
||||||
@ -34,7 +34,7 @@ describe('Media', () => {
|
|||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<div ref={div => resolve(div!)}>
|
<div ref={div => resolve(div!)}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Media onSave={onSave} />
|
<Media />
|
||||||
</Provider>
|
</Provider>
|
||||||
</div>,
|
</div>,
|
||||||
div,
|
div,
|
||||||
@ -44,18 +44,27 @@ describe('Media', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('submit', () => {
|
describe('submit', () => {
|
||||||
it('calls onSave', async () => {
|
const stream = {} as MediaStream
|
||||||
|
let promise: Promise<MediaStream>
|
||||||
|
beforeEach(() => {
|
||||||
|
navigator.mediaDevices.getUserMedia = async () => {
|
||||||
|
promise = Promise.resolve(stream)
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it('tries to retrieve audio/video media stream', async () => {
|
||||||
const node = await render()
|
const node = await render()
|
||||||
expect(node.tagName).toBe('FORM')
|
expect(node.tagName).toBe('FORM')
|
||||||
TestUtils.Simulate.submit(node)
|
TestUtils.Simulate.submit(node)
|
||||||
expect(onSave.mock.calls.length).toBe(1)
|
expect(promise).toBeDefined()
|
||||||
|
await promise
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('onVideoChange', () => {
|
describe('onVideoChange', () => {
|
||||||
it('calls onSetVideoConstraint', async () => {
|
it('calls onSetVideoConstraint', async () => {
|
||||||
const node = await render()
|
const node = await render()
|
||||||
const select = node.querySelector('select.media-video')!
|
const select = node.querySelector('select[name=video-input]')!
|
||||||
TestUtils.Simulate.change(select, {
|
TestUtils.Simulate.change(select, {
|
||||||
target: {
|
target: {
|
||||||
value: '{"deviceId":123}',
|
value: '{"deviceId":123}',
|
||||||
@ -68,7 +77,7 @@ describe('Media', () => {
|
|||||||
describe('onAudioChange', () => {
|
describe('onAudioChange', () => {
|
||||||
it('calls onSetAudioConstraint', async () => {
|
it('calls onSetAudioConstraint', async () => {
|
||||||
const node = await render()
|
const node = await render()
|
||||||
const select = node.querySelector('select.media-audio')!
|
const select = node.querySelector('select[name="audio-input"]')!
|
||||||
TestUtils.Simulate.change(select, {
|
TestUtils.Simulate.change(select, {
|
||||||
target: {
|
target: {
|
||||||
value: '{"deviceId":456}',
|
value: '{"deviceId":456}',
|
||||||
|
|||||||
@ -1,19 +1,14 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { AudioConstraint, MediaDevice, setAudioConstraint, setVideoConstraint, VideoConstraint } from '../actions/MediaActions'
|
import { AudioConstraint, MediaDevice, setAudioConstraint, setVideoConstraint, VideoConstraint, getMediaStream, enumerateDevices } from '../actions/MediaActions'
|
||||||
import { MediaState } from '../reducers/media'
|
import { MediaState } from '../reducers/media'
|
||||||
import { State } from '../store'
|
import { State } from '../store'
|
||||||
|
|
||||||
export type MediaProps = MediaState & {
|
export type MediaProps = MediaState & {
|
||||||
|
enumerateDevices: typeof enumerateDevices
|
||||||
onSetVideoConstraint: typeof setVideoConstraint
|
onSetVideoConstraint: typeof setVideoConstraint
|
||||||
onSetAudioConstraint: typeof setAudioConstraint
|
onSetAudioConstraint: typeof setAudioConstraint
|
||||||
onSave: () => void
|
getMediaStream: typeof getMediaStream
|
||||||
}
|
|
||||||
|
|
||||||
function getId(constraint: VideoConstraint | AudioConstraint) {
|
|
||||||
return typeof constraint === 'object' && 'deviceId' in constraint
|
|
||||||
? constraint.deviceId
|
|
||||||
: ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state: State) {
|
function mapStateToProps(state: State) {
|
||||||
@ -23,17 +18,27 @@ function mapStateToProps(state: State) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
enumerateDevices,
|
||||||
onSetVideoConstraint: setVideoConstraint,
|
onSetVideoConstraint: setVideoConstraint,
|
||||||
onSetAudioConstraint: setAudioConstraint,
|
onSetAudioConstraint: setAudioConstraint,
|
||||||
|
getMediaStream,
|
||||||
}
|
}
|
||||||
|
|
||||||
const c = connect(mapStateToProps, mapDispatchToProps)
|
const c = connect(mapStateToProps, mapDispatchToProps)
|
||||||
|
|
||||||
export const Media = c(React.memo(function Media(props: MediaProps) {
|
export const Media = c(React.memo(function Media(props: MediaProps) {
|
||||||
|
|
||||||
function onSave(event: React.FormEvent<HTMLFormElement>) {
|
React.useMemo(async () => await props.enumerateDevices(), [])
|
||||||
|
|
||||||
|
async function onSave(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
props.onSave()
|
const { audio, video } = props
|
||||||
|
try {
|
||||||
|
await props.getMediaStream({ audio, video })
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.stack)
|
||||||
|
// TODO display a message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onVideoChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
function onVideoChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||||
@ -46,12 +51,17 @@ export const Media = c(React.memo(function Media(props: MediaProps) {
|
|||||||
props.onSetAudioConstraint(constraint)
|
props.onSetAudioConstraint(constraint)
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoId = getId(props.video)
|
const videoId = JSON.stringify(props.video)
|
||||||
const audioId = getId(props.audio)
|
const audioId = JSON.stringify(props.audio)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className='media' onSubmit={onSave}>
|
<form className='media' onSubmit={onSave}>
|
||||||
<select className='media-video' onChange={onVideoChange} value={videoId}>
|
<label htmlFor='video-input'>Video</label>
|
||||||
|
<select
|
||||||
|
name='video-input'
|
||||||
|
onChange={onVideoChange}
|
||||||
|
value={videoId}
|
||||||
|
>
|
||||||
<Options
|
<Options
|
||||||
devices={props.devices}
|
devices={props.devices}
|
||||||
default='{"facingMode":"user"}'
|
default='{"facingMode":"user"}'
|
||||||
@ -59,7 +69,12 @@ export const Media = c(React.memo(function Media(props: MediaProps) {
|
|||||||
/>
|
/>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select className='media-audio' onChange={onAudioChange} value={audioId}>
|
<label htmlFor='video-input'>Audio</label>
|
||||||
|
<select
|
||||||
|
name='audio-input'
|
||||||
|
onChange={onAudioChange}
|
||||||
|
value={audioId}
|
||||||
|
>
|
||||||
<Options
|
<Options
|
||||||
devices={props.devices}
|
devices={props.devices}
|
||||||
default='true'
|
default='true'
|
||||||
@ -68,7 +83,7 @@ export const Media = c(React.memo(function Media(props: MediaProps) {
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<button type='submit'>
|
<button type='submit'>
|
||||||
Save
|
Join Call
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
@ -91,7 +106,9 @@ function Options(props: OptionsProps) {
|
|||||||
.map(device =>
|
.map(device =>
|
||||||
<option
|
<option
|
||||||
key={device.id}
|
key={device.id}
|
||||||
value={JSON.stringify({deviceId: device.id})}>{device.name}
|
value={JSON.stringify({deviceId: device.id})}
|
||||||
|
>
|
||||||
|
{device.name || device.type}
|
||||||
</option>,
|
</option>,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import * as MediaActions from '../actions/MediaActions'
|
import * as MediaActions from '../actions/MediaActions'
|
||||||
import { MEDIA_ENUMERATE, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_STREAM } from '../constants'
|
import { MEDIA_ENUMERATE, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_STREAM, ME, PEERS_DESTROY, PEER_ADD } from '../constants'
|
||||||
import { createStore, Store } from '../store'
|
import { createStore, Store } from '../store'
|
||||||
|
import SimplePeer from 'simple-peer'
|
||||||
|
|
||||||
describe('media', () => {
|
describe('media', () => {
|
||||||
|
|
||||||
@ -95,14 +96,73 @@ describe('media', () => {
|
|||||||
navigator.mediaDevices.getUserMedia = async () => stream
|
navigator.mediaDevices.getUserMedia = async () => stream
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns a promise with media stream', async () => {
|
async function dispatch() {
|
||||||
const promise = MediaActions.getMediaStream({
|
const promise = store.dispatch(MediaActions.getMediaStream({
|
||||||
audio: true,
|
audio: true,
|
||||||
video: true,
|
video: true,
|
||||||
})
|
}))
|
||||||
expect(promise.type).toBe('MEDIA_STREAM')
|
|
||||||
expect(promise.status).toBe('pending')
|
|
||||||
expect(await promise).toBe(stream)
|
expect(await promise).toBe(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('reducers/streams', () => {
|
||||||
|
it('adds the local stream to the map of videos', async () => {
|
||||||
|
expect(store.getState().streams[ME]).toBeFalsy()
|
||||||
|
await dispatch()
|
||||||
|
expect(store.getState().streams[ME]).toBeTruthy()
|
||||||
|
expect(store.getState().streams[ME].stream).toBe(stream)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('reducers/peers', () => {
|
||||||
|
const peer1 = new SimplePeer()
|
||||||
|
peer1.addStream = jest.fn()
|
||||||
|
peer1.removeStream = jest.fn()
|
||||||
|
const peer2 = new SimplePeer()
|
||||||
|
peer2.addStream = jest.fn()
|
||||||
|
peer2.removeStream = jest.fn()
|
||||||
|
const peers = [peer1, peer2]
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store.dispatch({
|
||||||
|
type: PEERS_DESTROY,
|
||||||
|
})
|
||||||
|
store.dispatch({
|
||||||
|
type: PEER_ADD,
|
||||||
|
payload: {
|
||||||
|
userId: '1',
|
||||||
|
peer: peer1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
store.dispatch({
|
||||||
|
type: PEER_ADD,
|
||||||
|
payload: {
|
||||||
|
userId: '2',
|
||||||
|
peer: peer2,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.dispatch({
|
||||||
|
type: PEERS_DESTROY,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('replaces local stream on all peers', async () => {
|
||||||
|
await dispatch()
|
||||||
|
peers.forEach(peer => {
|
||||||
|
expect((peer.addStream as jest.Mock).mock.calls)
|
||||||
|
.toEqual([[ stream ]])
|
||||||
|
expect((peer.removeStream as jest.Mock).mock.calls).toEqual([])
|
||||||
|
})
|
||||||
|
await dispatch()
|
||||||
|
peers.forEach(peer => {
|
||||||
|
expect((peer.addStream as jest.Mock).mock.calls)
|
||||||
|
.toEqual([[ stream ], [ stream ]])
|
||||||
|
expect((peer.removeStream as jest.Mock).mock.calls)
|
||||||
|
.toEqual([[ stream ]])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3,14 +3,17 @@ import omit from 'lodash/omit'
|
|||||||
import Peer from 'simple-peer'
|
import Peer from 'simple-peer'
|
||||||
import { PeerAction } from '../actions/PeerActions'
|
import { PeerAction } from '../actions/PeerActions'
|
||||||
import * as constants from '../constants'
|
import * as constants from '../constants'
|
||||||
|
import { MediaStreamAction } from '../actions/MediaActions'
|
||||||
|
|
||||||
export type PeersState = Record<string, Peer.Instance>
|
export type PeersState = Record<string, Peer.Instance>
|
||||||
|
|
||||||
const defaultState: PeersState = {}
|
const defaultState: PeersState = {}
|
||||||
|
|
||||||
|
let localStream: MediaStream | undefined
|
||||||
|
|
||||||
export default function peers(
|
export default function peers(
|
||||||
state = defaultState,
|
state = defaultState,
|
||||||
action: PeerAction,
|
action: PeerAction | MediaStreamAction,
|
||||||
): PeersState {
|
): PeersState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case constants.PEER_ADD:
|
case constants.PEER_ADD:
|
||||||
@ -21,8 +24,18 @@ export default function peers(
|
|||||||
case constants.PEER_REMOVE:
|
case constants.PEER_REMOVE:
|
||||||
return omit(state, [action.payload.userId])
|
return omit(state, [action.payload.userId])
|
||||||
case constants.PEERS_DESTROY:
|
case constants.PEERS_DESTROY:
|
||||||
|
localStream = undefined
|
||||||
forEach(state, peer => peer.destroy())
|
forEach(state, peer => peer.destroy())
|
||||||
return defaultState
|
return defaultState
|
||||||
|
case constants.MEDIA_STREAM:
|
||||||
|
if (action.status === 'resolved') {
|
||||||
|
forEach(state, peer => {
|
||||||
|
localStream && peer.removeStream(localStream)
|
||||||
|
peer.addStream(action.payload)
|
||||||
|
})
|
||||||
|
localStream = action.payload
|
||||||
|
}
|
||||||
|
return state
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import _debug from 'debug'
|
import _debug from 'debug'
|
||||||
import omit from 'lodash/omit'
|
import omit from 'lodash/omit'
|
||||||
import { AddStreamAction, AddStreamPayload, RemoveStreamAction, StreamAction } from '../actions/StreamActions'
|
import { AddStreamAction, AddStreamPayload, RemoveStreamAction, StreamAction } from '../actions/StreamActions'
|
||||||
import { STREAM_ADD, STREAM_REMOVE } from '../constants'
|
import { STREAM_ADD, STREAM_REMOVE, MEDIA_STREAM, ME } from '../constants'
|
||||||
import { createObjectURL, revokeObjectURL } from '../window'
|
import { createObjectURL, revokeObjectURL } from '../window'
|
||||||
|
import { MediaStreamAction } from '../actions/MediaActions'
|
||||||
|
|
||||||
const debug = _debug('peercalls')
|
const debug = _debug('peercalls')
|
||||||
const defaultState = Object.freeze({})
|
const defaultState = Object.freeze({})
|
||||||
@ -21,9 +22,9 @@ export interface StreamsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addStream (
|
function addStream (
|
||||||
state: StreamsState, action: AddStreamAction,
|
state: StreamsState, payload: AddStreamAction['payload'],
|
||||||
): StreamsState {
|
): StreamsState {
|
||||||
const { userId, stream } = action.payload
|
const { userId, stream } = payload
|
||||||
|
|
||||||
const userStream: AddStreamPayload = {
|
const userStream: AddStreamPayload = {
|
||||||
userId,
|
userId,
|
||||||
@ -38,9 +39,9 @@ function addStream (
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeStream (
|
function removeStream (
|
||||||
state: StreamsState, action: RemoveStreamAction,
|
state: StreamsState, payload: RemoveStreamAction['payload'],
|
||||||
): StreamsState {
|
): StreamsState {
|
||||||
const { userId } = action.payload
|
const { userId } = payload
|
||||||
const stream = state[userId]
|
const stream = state[userId]
|
||||||
if (stream && stream.url) {
|
if (stream && stream.url) {
|
||||||
revokeObjectURL(stream.url)
|
revokeObjectURL(stream.url)
|
||||||
@ -48,15 +49,31 @@ function removeStream (
|
|||||||
return omit(state, [userId])
|
return omit(state, [userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replaceStream(state: StreamsState, stream: MediaStream): StreamsState {
|
||||||
|
state = removeStream(state, {
|
||||||
|
userId: ME,
|
||||||
|
})
|
||||||
|
return addStream(state, {
|
||||||
|
userId: ME,
|
||||||
|
stream,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export default function streams(
|
export default function streams(
|
||||||
state = defaultState,
|
state = defaultState,
|
||||||
action: StreamAction,
|
action: StreamAction | MediaStreamAction,
|
||||||
): StreamsState {
|
): StreamsState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case STREAM_ADD:
|
case STREAM_ADD:
|
||||||
return addStream(state, action)
|
return addStream(state, action.payload)
|
||||||
case STREAM_REMOVE:
|
case STREAM_REMOVE:
|
||||||
return removeStream(state, action)
|
return removeStream(state, action.payload)
|
||||||
|
case MEDIA_STREAM:
|
||||||
|
if (action.status === 'resolved') {
|
||||||
|
return replaceStream(state, action.payload)
|
||||||
|
} else {
|
||||||
|
return state
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,58 +1,7 @@
|
|||||||
import {
|
import { createObjectURL, play, revokeObjectURL, valueOf } from './window'
|
||||||
createObjectURL,
|
|
||||||
revokeObjectURL,
|
|
||||||
getUserMedia,
|
|
||||||
navigator,
|
|
||||||
play,
|
|
||||||
valueOf,
|
|
||||||
} from './window'
|
|
||||||
|
|
||||||
describe('window', () => {
|
describe('window', () => {
|
||||||
|
|
||||||
describe('getUserMedia', () => {
|
|
||||||
|
|
||||||
class MediaStream {}
|
|
||||||
const stream = new MediaStream()
|
|
||||||
const constraints = { video: true }
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
delete (navigator as any).mediaDevices
|
|
||||||
delete navigator.getUserMedia
|
|
||||||
delete (navigator as any).webkitGetUserMedia
|
|
||||||
})
|
|
||||||
|
|
||||||
it('calls navigator.mediaDevices.getUserMedia', async () => {
|
|
||||||
const promise = Promise.resolve(stream);
|
|
||||||
(navigator as any).mediaDevices = {
|
|
||||||
getUserMedia: jest.fn().mockReturnValue(promise),
|
|
||||||
}
|
|
||||||
expect(await getUserMedia(constraints)).toBe(stream)
|
|
||||||
})
|
|
||||||
|
|
||||||
;['getUserMedia', 'webkitGetUserMedia'].forEach((method) => {
|
|
||||||
it(`it calls navigator.${method} as a fallback`, () => {
|
|
||||||
(navigator as any)[method] = jest.fn()
|
|
||||||
.mockImplementation(
|
|
||||||
(constraints, onSuccess, onError) => onSuccess(stream),
|
|
||||||
)
|
|
||||||
return getUserMedia(constraints)
|
|
||||||
.then(s => expect(s).toBe(stream))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws error when no supported method', async () => {
|
|
||||||
let error: Error
|
|
||||||
try {
|
|
||||||
await getUserMedia(constraints)
|
|
||||||
} catch (err) {
|
|
||||||
error = err
|
|
||||||
}
|
|
||||||
expect(error!).toBeTruthy()
|
|
||||||
expect(error!.message).toBe('Browser unsupported')
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('play', () => {
|
describe('play', () => {
|
||||||
|
|
||||||
let v1: HTMLVideoElement & { play: jest.Mock }
|
let v1: HTMLVideoElement & { play: jest.Mock }
|
||||||
|
|||||||
@ -2,27 +2,10 @@ import _debug from 'debug'
|
|||||||
|
|
||||||
const debug = _debug('peercalls')
|
const debug = _debug('peercalls')
|
||||||
|
|
||||||
export async function getUserMedia (constraints: MediaStreamConstraints) {
|
|
||||||
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
||||||
return navigator.mediaDevices.getUserMedia(constraints)
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<MediaStream>((resolve, reject) => {
|
|
||||||
const getMedia = navigator.getUserMedia ||
|
|
||||||
(navigator as unknown as {
|
|
||||||
webkitGetUserMedia: typeof navigator.getUserMedia
|
|
||||||
}).webkitGetUserMedia
|
|
||||||
if (!getMedia) reject(new Error('Browser unsupported'))
|
|
||||||
getMedia.call(navigator, constraints, resolve, reject)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createObjectURL = (object: unknown) =>
|
export const createObjectURL = (object: unknown) =>
|
||||||
window.URL.createObjectURL(object)
|
window.URL.createObjectURL(object)
|
||||||
export const revokeObjectURL = (url: string) => window.URL.revokeObjectURL(url)
|
export const revokeObjectURL = (url: string) => window.URL.revokeObjectURL(url)
|
||||||
|
|
||||||
export const navigator = window.navigator
|
|
||||||
|
|
||||||
export function play () {
|
export function play () {
|
||||||
const videos = window.document.querySelectorAll('video')
|
const videos = window.document.querySelectorAll('video')
|
||||||
Array.prototype.forEach.call(videos, (video, index) => {
|
Array.prototype.forEach.call(videos, (video, index) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user