diff --git a/src/client/actions/MediaActions.ts b/src/client/actions/MediaActions.ts index 8ef4410..838a867 100644 --- a/src/client/actions/MediaActions.ts +++ b/src/client/actions/MediaActions.ts @@ -1,6 +1,7 @@ import { makeAction, AsyncAction } from '../async' -import { MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_ENUMERATE, MEDIA_STREAM, ME, ME_DESKTOP } from '../constants' +import { MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_ENUMERATE, MEDIA_STREAM, ME, STREAM_TYPE_CAMERA, STREAM_TYPE_DESKTOP } from '../constants' import _debug from 'debug' +import { AddStreamPayload } from './StreamActions' const debug = _debug('peercalls') @@ -111,8 +112,9 @@ export const getMediaStream = makeAction( MEDIA_STREAM, async (constraints: GetMediaConstraints) => { debug('getMediaStream', constraints) - const payload: MediaStreamPayload = { + const payload: AddStreamPayload = { stream: await getUserMedia(constraints), + type: STREAM_TYPE_CAMERA, userId: ME, } return payload @@ -123,21 +125,17 @@ export const getDesktopStream = makeAction( MEDIA_STREAM, async () => { debug('getDesktopStream') - const payload: MediaStreamPayload = { + const payload: AddStreamPayload = { stream: await getDisplayMedia(), - userId: ME_DESKTOP, + type: STREAM_TYPE_DESKTOP, + userId: ME, } return payload }, ) -export interface MediaStreamPayload { - stream: MediaStream - userId: string -} - export type MediaEnumerateAction = AsyncAction<'MEDIA_ENUMERATE', MediaDevice[]> -export type MediaStreamAction = AsyncAction<'MEDIA_STREAM', MediaStreamPayload> +export type MediaStreamAction = AsyncAction<'MEDIA_STREAM', AddStreamPayload> export type MediaPlayAction = AsyncAction<'MEDIA_PLAY', void> export type MediaAction = diff --git a/src/client/actions/PeerActions.ts b/src/client/actions/PeerActions.ts index 6004f8a..e732be8 100644 --- a/src/client/actions/PeerActions.ts +++ b/src/client/actions/PeerActions.ts @@ -57,20 +57,18 @@ class PeerHandler { const state = getState() const peer = state.peers[user.id] const localStream = state.streams[constants.ME] - if (localStream && localStream.stream) { + localStream && localStream.streams.forEach(s => { // If the local user pressed join call before this peer has joined the // call, now is the time to share local media stream with the peer since // we no longer automatically send the stream to the peer. - peer.addStream(localStream.stream) - } - const desktopStream = state.streams[constants.ME_DESKTOP] - if (desktopStream && desktopStream.stream) { - peer.addStream(desktopStream.stream) - } + s.stream.getTracks().forEach(track => { + peer.addTrack(track, s.stream) + }) + }) } - handleStream = (stream: MediaStream) => { + handleTrack = (track: MediaStreamTrack, stream: MediaStream) => { const { user, dispatch } = this - debug('peer: %s, stream', user.id) + debug('peer: %s, track', user.id) dispatch(StreamActions.addStream({ userId: user.id, stream, @@ -160,7 +158,7 @@ export function createPeer (options: CreatePeerOptions) { peer.once(constants.PEER_EVENT_CONNECT, handler.handleConnect) peer.once(constants.PEER_EVENT_CLOSE, handler.handleClose) peer.on(constants.PEER_EVENT_SIGNAL, handler.handleSignal) - peer.on(constants.PEER_EVENT_STREAM, handler.handleStream) + peer.on(constants.PEER_EVENT_TRACK, handler.handleTrack) peer.on(constants.PEER_EVENT_DATA, handler.handleData) dispatch(addPeer({ peer, userId })) diff --git a/src/client/actions/SocketActions.test.ts b/src/client/actions/SocketActions.test.ts index 2b5423a..71dcd6b 100644 --- a/src/client/actions/SocketActions.test.ts +++ b/src/client/actions/SocketActions.test.ts @@ -136,7 +136,7 @@ describe('SocketActions', () => { }] }, } - peer.emit(constants.PEER_EVENT_STREAM, stream) + peer.emit(constants.PEER_EVENT_TRACK, stream.getTracks()[0], stream) expect(store.getState().streams).toEqual({ b: { @@ -151,7 +151,8 @@ describe('SocketActions', () => { describe('close', () => { beforeEach(() => { const stream = new MediaStream() - peer.emit(constants.PEER_EVENT_STREAM, stream) + const track = new MediaStreamTrack() + peer.emit(constants.PEER_EVENT_TRACK, track, stream) expect(store.getState().streams).toEqual({ b: { userId: 'b', diff --git a/src/client/actions/StreamActions.ts b/src/client/actions/StreamActions.ts index 51eb4dd..6afe140 100644 --- a/src/client/actions/StreamActions.ts +++ b/src/client/actions/StreamActions.ts @@ -1,9 +1,11 @@ import * as constants from '../constants' +export type StreamType = 'camera' | 'desktop' + export interface AddStreamPayload { userId: string + type?: StreamType stream: MediaStream - url?: string } export interface AddStreamAction { @@ -18,6 +20,7 @@ export interface RemoveStreamAction { export interface RemoveStreamPayload { userId: string + stream?: MediaStream } export interface SetActiveStreamAction { @@ -39,9 +42,12 @@ export const addStream = (payload: AddStreamPayload): AddStreamAction => ({ payload, }) -export const removeStream = (userId: string): RemoveStreamAction => ({ +export const removeStream = ( + userId: string, + stream?: MediaStream, +): RemoveStreamAction => ({ type: constants.STREAM_REMOVE, - payload: { userId }, + payload: { userId, stream }, }) export const setActive = (userId: string): SetActiveStreamAction => ({ diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index b3a78e8..6fb8d2f 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -5,7 +5,7 @@ import Peer from 'simple-peer' import { Message } from '../actions/ChatActions' import { dismissNotification, Notification } from '../actions/NotifyActions' import { TextMessage } from '../actions/PeerActions' -import { AddStreamPayload, removeStream } from '../actions/StreamActions' +import { removeStream } from '../actions/StreamActions' import * as constants from '../constants' import Chat from './Chat' import { Media } from './Media' @@ -14,6 +14,7 @@ import { Side } from './Side' import Toolbar from './Toolbar' import Video from './Video' import { getDesktopStream } from '../actions/MediaActions' +import { StreamsState } from '../reducers/streams' export interface AppProps { active: string | null @@ -25,7 +26,7 @@ export interface AppProps { peers: Record play: () => void sendMessage: (message: TextMessage) => void - streams: Record + streams: StreamsState getDesktopStream: typeof getDesktopStream removeStream: typeof removeStream onSendFile: (file: File) => void @@ -37,8 +38,6 @@ export interface AppState { chatVisible: boolean } -const localStreams: string[] = [constants.ME, constants.ME_DESKTOP] - export default class App extends React.PureComponent { state: AppState = { videos: {}, @@ -65,7 +64,6 @@ export default class App extends React.PureComponent { } onHangup = () => { this.props.removeStream(constants.ME) - this.props.removeStream(constants.ME_DESKTOP) } render () { const { @@ -88,6 +86,11 @@ export default class App extends React.PureComponent { 'chat-visible': this.state.chatVisible, }) + const localStreams = streams[constants.ME] || { + userId: constants.ME, + streams: [], + } + return (
@@ -97,8 +100,14 @@ export default class App extends React.PureComponent { onToggleChat={this.handleToggleChat} onSendFile={onSendFile} onHangup={this.onHangup} - stream={streams[constants.ME]} - desktopStream={streams[constants.ME_DESKTOP]} + stream={ + localStreams.streams + .filter(s => s.type === constants.STREAM_TYPE_CAMERA)[0] + } + desktopStream={ + localStreams.streams + .filter(s => s.type === constants.STREAM_TYPE_DESKTOP)[0] + } onGetDesktopStream={this.props.getDesktopStream} onRemoveStream={this.props.removeStream} /> @@ -117,33 +126,43 @@ export default class App extends React.PureComponent { visible={this.state.chatVisible} />
- {localStreams.filter(userId => !!streams[userId]).map(userId => ( -
diff --git a/src/client/components/Media.tsx b/src/client/components/Media.tsx index 9c0986e..1215429 100644 --- a/src/client/components/Media.tsx +++ b/src/client/components/Media.tsx @@ -5,7 +5,7 @@ import { MediaState } from '../reducers/media' import { State } from '../store' import { Alerts, Alert } from './Alerts' import { info, warning, error } from '../actions/NotifyActions' -import { ME } from '../constants' +import { ME, STREAM_TYPE_CAMERA } from '../constants' export type MediaProps = MediaState & { visible: boolean @@ -21,7 +21,10 @@ export type MediaProps = MediaState & { function mapStateToProps(state: State) { const localStream = state.streams[ME] - const visible = !localStream + const hidden = !!localStream && + localStream.streams.filter(s => s.type === STREAM_TYPE_CAMERA).length > 0 + const visible = !hidden + console.log('visible', visible) return { ...state.media, visible, diff --git a/src/client/components/Toolbar.test.tsx b/src/client/components/Toolbar.test.tsx index 1721540..0edb80a 100644 --- a/src/client/components/Toolbar.test.tsx +++ b/src/client/components/Toolbar.test.tsx @@ -2,11 +2,12 @@ jest.mock('../window') import React from 'react' import ReactDOM from 'react-dom' import TestUtils from 'react-dom/test-utils' -import Toolbar, { ToolbarProps } from './Toolbar' -import { MediaStream } from '../window' -import { AddStreamPayload, removeStream } from '../actions/StreamActions' -import { ME_DESKTOP } from '../constants' import { getDesktopStream } from '../actions/MediaActions' +import { AddStreamPayload, removeStream } from '../actions/StreamActions' +import { STREAM_TYPE_CAMERA, STREAM_TYPE_DESKTOP } from '../constants' +import { StreamWithURL } from '../reducers/streams' +import { MediaStream } from '../window' +import Toolbar, { ToolbarProps } from './Toolbar' describe('components/Toolbar', () => { @@ -42,7 +43,7 @@ describe('components/Toolbar', () => { let onHangup: jest.Mock<() => void> let onGetDesktopStream: jest.MockedFunction let onRemoveStream: jest.MockedFunction - let desktopStream: AddStreamPayload | undefined + let desktopStream: StreamWithURL | undefined async function render () { mediaStream = new MediaStream() onToggleChat = jest.fn() @@ -51,6 +52,11 @@ describe('components/Toolbar', () => { onGetDesktopStream = jest.fn() onRemoveStream = jest.fn() const div = document.createElement('div') + const stream: StreamWithURL = { + stream: mediaStream, + type: STREAM_TYPE_CAMERA, + url, + } await new Promise(resolve => { ReactDOM.render( { onToggleChat={onToggleChat} onSendFile={onSendFile} messagesCount={1} - stream={{ userId: '', stream: mediaStream, url }} + stream={stream} desktopStream={desktopStream} onGetDesktopStream={onGetDesktopStream} onRemoveStream={onRemoveStream} @@ -160,8 +166,8 @@ describe('components/Toolbar', () => { }) it('stops desktop sharing', async () => { desktopStream = { - userId: ME_DESKTOP, stream: new MediaStream(), + type: STREAM_TYPE_DESKTOP, } await render() const shareDesktop = node.querySelector('.share-desktop')! diff --git a/src/client/components/Toolbar.tsx b/src/client/components/Toolbar.tsx index 63299b8..432b9aa 100644 --- a/src/client/components/Toolbar.tsx +++ b/src/client/components/Toolbar.tsx @@ -1,9 +1,10 @@ import classnames from 'classnames' import React from 'react' import screenfull from 'screenfull' -import { AddStreamPayload, removeStream } from '../actions/StreamActions' -import { ME_DESKTOP } from '../constants' +import { removeStream } from '../actions/StreamActions' import { getDesktopStream } from '../actions/MediaActions' +import { StreamWithURL } from '../reducers/streams' +import { ME } from '../constants' const hidden = { display: 'none', @@ -11,8 +12,8 @@ const hidden = { export interface ToolbarProps { messagesCount: number - stream: AddStreamPayload - desktopStream: AddStreamPayload | undefined + stream: StreamWithURL + desktopStream: StreamWithURL | undefined onToggleChat: () => void onGetDesktopStream: typeof getDesktopStream onRemoveStream: typeof removeStream @@ -61,7 +62,6 @@ function ToolbarButton(props: ToolbarButtonProps) { export default class Toolbar extends React.PureComponent { file = React.createRef() - desktopStream: MediaStream | undefined constructor(props: ToolbarProps) { super(props) @@ -121,7 +121,7 @@ extends React.PureComponent { } handleToggleShareDesktop = () => { if (this.props.desktopStream) { - this.props.onRemoveStream(ME_DESKTOP) + this.props.onRemoveStream(ME, this.props.desktopStream.stream) } else { this.props.onGetDesktopStream().catch(() => {}) } @@ -162,7 +162,7 @@ extends React.PureComponent { className='stream-desktop' icon='icon-display' onClick={this.handleToggleShareDesktop} - on={!!this.desktopStream} + on={!!this.props.desktopStream} title='Share Desktop' /> diff --git a/src/client/components/Video.test.tsx b/src/client/components/Video.test.tsx index e07737f..a44b66e 100644 --- a/src/client/components/Video.test.tsx +++ b/src/client/components/Video.test.tsx @@ -2,14 +2,15 @@ jest.mock('../window') import React from 'react' import ReactDOM from 'react-dom' import TestUtils from 'react-dom/test-utils' -import { AddStreamPayload } from '../actions/StreamActions' import Video, { VideoProps } from './Video' import { MediaStream } from '../window' +import { STREAM_TYPE_CAMERA } from '../constants' +import { StreamWithURL } from '../reducers/streams' describe('components/Video', () => { interface VideoState { - stream: null | AddStreamPayload + stream: null | StreamWithURL } const play = jest.fn() @@ -61,12 +62,17 @@ describe('components/Video', () => { mediaStream = new MediaStream() const div = document.createElement('div') component = await new Promise(resolve => { + const stream: StreamWithURL = { + stream: mediaStream, + url, + type: STREAM_TYPE_CAMERA, + } ReactDOM.render( resolve(instance!)} videos={videos} active={flags.active} - stream={{ stream: mediaStream, url, userId: 'test' }} + stream={stream} onClick={onClick} play={play} userId="test" @@ -100,22 +106,38 @@ describe('components/Video', () => { it('updates src only when changed', () => { mediaStream = new MediaStream() component.setState({ - stream: { url: 'test', stream: mediaStream, userId: '' }, + stream: { + url: 'test', + stream: mediaStream, + type: STREAM_TYPE_CAMERA, + }, }) expect(video.videoRef.current!.src).toBe('http://localhost/test') component.setState({ - stream: { url: 'test', stream: mediaStream, userId: '' }, + stream: { + url: 'test', + stream: mediaStream, + type: STREAM_TYPE_CAMERA, + }, }) }) it('updates srcObject only when changed', () => { video.videoRef.current!.srcObject = null mediaStream = new MediaStream() component.setState({ - stream: { url: 'test', stream: mediaStream, userId: '' }, + stream: { + url: 'test', + stream: mediaStream, + type: STREAM_TYPE_CAMERA, + }, }) expect(video.videoRef.current!.srcObject).toBe(mediaStream) component.setState({ - stream: { url: 'test', stream: mediaStream, userId: '' }, + stream: { + url: 'test', + stream: mediaStream, + type: STREAM_TYPE_CAMERA, + }, }) }) }) diff --git a/src/client/components/Video.tsx b/src/client/components/Video.tsx index 8c3c0a8..e9f4e67 100644 --- a/src/client/components/Video.tsx +++ b/src/client/components/Video.tsx @@ -1,13 +1,13 @@ import React, { ReactEventHandler } from 'react' import classnames from 'classnames' import socket from '../socket' -import { AddStreamPayload } from '../actions/StreamActions' +import { StreamWithURL } from '../reducers/streams' export interface VideoProps { videos: Record onClick: (userId: string) => void active: boolean - stream?: AddStreamPayload + stream?: StreamWithURL userId: string muted: boolean mirrored: boolean diff --git a/src/client/constants.ts b/src/client/constants.ts index 33d198b..d55ae98 100644 --- a/src/client/constants.ts +++ b/src/client/constants.ts @@ -8,7 +8,6 @@ export const ALERT_CLEAR = 'ALERT_CLEAR' export const INIT = 'INIT' export const ME = '_me_' -export const ME_DESKTOP = '_me_desktop' export const NOTIFY = 'NOTIFY' export const NOTIFY_DISMISS = 'NOTIFY_DISMISS' @@ -30,7 +29,7 @@ export const PEER_EVENT_ERROR = 'error' export const PEER_EVENT_CONNECT = 'connect' export const PEER_EVENT_CLOSE = 'close' export const PEER_EVENT_SIGNAL = 'signal' -export const PEER_EVENT_STREAM = 'stream' +export const PEER_EVENT_TRACK = 'track' export const PEER_EVENT_DATA = 'data' export const SOCKET_EVENT_SIGNAL = 'signal' @@ -38,3 +37,6 @@ export const SOCKET_EVENT_USERS = 'users' export const STREAM_ADD = 'PEER_STREAM_ADD' export const STREAM_REMOVE = 'PEER_STREAM_REMOVE' + +export const STREAM_TYPE_CAMERA = 'camera' +export const STREAM_TYPE_DESKTOP = 'desktop' diff --git a/src/client/containers/App.test.tsx b/src/client/containers/App.test.tsx index 42c84fb..fbdd307 100644 --- a/src/client/containers/App.test.tsx +++ b/src/client/containers/App.test.tsx @@ -79,13 +79,19 @@ describe('App', () => { state.streams = { [constants.ME]: { userId: constants.ME, - stream: new MediaStream(), - url: 'blob://', + streams: [{ + stream: new MediaStream(), + type: constants.STREAM_TYPE_CAMERA, + url: 'blob://', + }], }, 'other-user': { userId: 'other-user', - stream: new MediaStream(), - url: 'blob://', + streams: [{ + stream: new MediaStream(), + type: undefined, + url: 'blob://', + }], }, } state.peers = { diff --git a/src/client/reducers/media.test.ts b/src/client/reducers/media.test.ts index 31b90b9..6a70be4 100644 --- a/src/client/reducers/media.test.ts +++ b/src/client/reducers/media.test.ts @@ -1,5 +1,5 @@ import * as MediaActions from '../actions/MediaActions' -import { MEDIA_ENUMERATE, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_STREAM, ME, PEERS_DESTROY, PEER_ADD, ME_DESKTOP } from '../constants' +import { MEDIA_ENUMERATE, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_STREAM, ME, PEERS_DESTROY, PEER_ADD, STREAM_TYPE_CAMERA, STREAM_TYPE_DESKTOP } from '../constants' import { createStore, Store } from '../store' import SimplePeer from 'simple-peer' @@ -102,6 +102,7 @@ describe('media', () => { video: true, })) expect(result.stream).toBe(stream) + expect(result.type).toBe(STREAM_TYPE_CAMERA) expect(result.userId).toBe(ME) } @@ -109,8 +110,11 @@ describe('media', () => { 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) + const localStreams = store.getState().streams[ME] + expect(localStreams).toBeTruthy() + expect(localStreams.streams.length).toBe(1) + expect(localStreams.streams[0].type).toBe(STREAM_TYPE_CAMERA) + expect(localStreams.streams[0].stream).toBe(stream) }) }) @@ -200,13 +204,17 @@ describe('media', () => { async function dispatch() { const result = await store.dispatch(MediaActions.getDesktopStream()) expect(result.stream).toBe(stream) - expect(result.userId).toBe(ME_DESKTOP) + expect(result.type).toBe(STREAM_TYPE_DESKTOP) + expect(result.userId).toBe(ME) } it('adds the local stream to the map of videos', async () => { - expect(store.getState().streams[ME_DESKTOP]).toBeFalsy() + expect(store.getState().streams[ME]).toBeFalsy() await dispatch() - expect(store.getState().streams[ME_DESKTOP]).toBeTruthy() - expect(store.getState().streams[ME_DESKTOP].stream).toBe(stream) + const localStreams = store.getState().streams[ME] + expect(localStreams).toBeTruthy() + expect(localStreams.streams.length).toBe(1) + expect(localStreams.streams[0].type).toBe(STREAM_TYPE_DESKTOP) + expect(localStreams.streams[0].stream).toBe(stream) }) }) diff --git a/src/client/reducers/notifications.ts b/src/client/reducers/notifications.ts index d68d58b..42f0840 100644 --- a/src/client/reducers/notifications.ts +++ b/src/client/reducers/notifications.ts @@ -12,7 +12,7 @@ export default function notifications ( action: AnyAction, ) { if (isRejectedAction(action)) { - action = error(action.payload) + action = error('' + action.payload) } return handleNotifications(state, action) } diff --git a/src/client/reducers/peers.ts b/src/client/reducers/peers.ts index 1bb455c..f338271 100644 --- a/src/client/reducers/peers.ts +++ b/src/client/reducers/peers.ts @@ -29,11 +29,15 @@ export default function peers( return defaultState case constants.MEDIA_STREAM: if (action.status === 'resolved') { - // userId can be ME or ME_DESKTOP forEach(state, peer => { const localStream = localStreams[action.payload.userId] - localStream && peer.removeStream(localStream) - peer.addStream(action.payload.stream) + localStream && localStream.getTracks().forEach(track => { + peer.removeTrack(track, localStream) + }) + const stream = action.payload.stream + stream.getTracks().forEach(track => { + peer.addTrack(track, stream) + }) }) localStreams[action.payload.userId] = action.payload.stream } diff --git a/src/client/reducers/streams.ts b/src/client/reducers/streams.ts index 4ac46b4..a3f4473 100644 --- a/src/client/reducers/streams.ts +++ b/src/client/reducers/streams.ts @@ -1,9 +1,9 @@ import _debug from 'debug' import omit from 'lodash/omit' -import { AddStreamAction, AddStreamPayload, RemoveStreamAction, StreamAction } from '../actions/StreamActions' +import { AddStreamAction, RemoveStreamAction, StreamAction, StreamType } from '../actions/StreamActions' import { STREAM_ADD, STREAM_REMOVE, MEDIA_STREAM } from '../constants' import { createObjectURL, revokeObjectURL } from '../window' -import { MediaStreamPayload, MediaStreamAction } from '../actions/MediaActions' +import { MediaStreamAction } from '../actions/MediaActions' const debug = _debug('peercalls') const defaultState = Object.freeze({}) @@ -17,8 +17,19 @@ function safeCreateObjectURL (stream: MediaStream) { } } +export interface StreamWithURL { + stream: MediaStream + type: StreamType | undefined + url?: string +} + +export interface UserStreams { + userId: string + streams: StreamWithURL[] +} + export interface StreamsState { - [userId: string]: AddStreamPayload + [userId: string]: UserStreams } function addStream ( @@ -26,40 +37,66 @@ function addStream ( ): StreamsState { const { userId, stream } = payload - const userStream: AddStreamPayload = { + const userStreams = state[userId] || { userId, + streams: [], + } + + if (userStreams.streams.map(s => s.stream).indexOf(stream) >= 0) { + return state + } + + const streamWithURL: StreamWithURL = { stream, + type: payload.type, url: safeCreateObjectURL(stream), } return { ...state, - [userId]: userStream, + [userId]: { + userId, + streams: [...userStreams.streams, streamWithURL], + }, } } function removeStream ( state: StreamsState, payload: RemoveStreamAction['payload'], ): StreamsState { - const { userId } = payload - const stream = state[userId] - if (stream && stream.stream) { - stream.stream.getTracks().forEach(track => track.stop()) + const { userId, stream } = payload + const userStreams = state[userId] + if (!userStreams) { + return state } - if (stream && stream.url) { - revokeObjectURL(stream.url) - } - return omit(state, [userId]) -} -function replaceStream( - state: StreamsState, - payload: MediaStreamPayload, -): StreamsState { - state = removeStream(state, { - userId: payload.userId, + if (stream) { + const streams = userStreams.streams.filter(s => { + const found = s.stream === stream + if (found) { + stream.getTracks().forEach(track => track.stop()) + s.url && revokeObjectURL(s.url) + } + return !found + }) + if (userStreams.streams.length > 0) { + return { + ...state, + [userId]: { + userId, + streams, + }, + } + } else { + omit(state, [userId]) + } + } + + userStreams && userStreams.streams.forEach(s => { + s.stream.getTracks().forEach(track => track.stop()) + s.url && revokeObjectURL(s.url) }) - return addStream(state, payload) + return omit(state, [userId]) } export default function streams( @@ -73,7 +110,7 @@ export default function streams( return removeStream(state, action.payload) case MEDIA_STREAM: if (action.status === 'resolved') { - return replaceStream(state, action.payload) + return addStream(state, action.payload) } else { return state }