From 46a0b1f7ea4b295eca592fc3bebb2c25e39923d7 Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Tue, 10 Mar 2020 11:58:15 +0100 Subject: [PATCH] Remove each user stream individually --- src/client/actions/PeerActions.ts | 24 +++++++++++++++++----- src/client/actions/StreamActions.ts | 31 +++++++++++++++++++++++++---- src/client/components/App.tsx | 16 ++++++++++----- src/client/components/Media.tsx | 1 - src/client/constants.ts | 1 + src/client/reducers/peers.ts | 22 +++++++++++++++++++- src/client/reducers/streams.test.ts | 4 ++-- src/client/reducers/streams.ts | 27 +++++++++++++++++++++++-- 8 files changed, 106 insertions(+), 20 deletions(-) diff --git a/src/client/actions/PeerActions.ts b/src/client/actions/PeerActions.ts index e732be8..0ca7798 100644 --- a/src/client/actions/PeerActions.ts +++ b/src/client/actions/PeerActions.ts @@ -68,9 +68,20 @@ class PeerHandler { } handleTrack = (track: MediaStreamTrack, stream: MediaStream) => { const { user, dispatch } = this - debug('peer: %s, track', user.id) + const userId = user.id + debug('peer: %s, track', userId) + // Listen to mute event to know when a track was removed + // https://github.com/feross/simple-peer/issues/512 + track.onmute = () => { + debug('peer: %s, track muted', userId) + dispatch(StreamActions.removeTrack({ + userId, + stream, + track, + })) + } dispatch(StreamActions.addStream({ - userId: user.id, + userId, stream, })) } @@ -97,10 +108,13 @@ class PeerHandler { } } handleClose = () => { - const { dispatch, user } = this - debug('peer: %s, close', user.id) + const { dispatch, user, getState } = this dispatch(NotifyActions.error('Peer connection closed')) - dispatch(StreamActions.removeStream(user.id)) + const state = getState() + const userStreams = state.streams[user.id] + userStreams && userStreams.streams.forEach(s => { + dispatch(StreamActions.removeStream(user.id, s.stream)) + }) dispatch(removePeer(user.id)) } } diff --git a/src/client/actions/StreamActions.ts b/src/client/actions/StreamActions.ts index 6afe140..88a2a60 100644 --- a/src/client/actions/StreamActions.ts +++ b/src/client/actions/StreamActions.ts @@ -20,12 +20,16 @@ export interface RemoveStreamAction { export interface RemoveStreamPayload { userId: string - stream?: MediaStream + stream: MediaStream +} + +export interface SetActiveStreamPayload { + userId: string } export interface SetActiveStreamAction { type: 'ACTIVE_SET' - payload: RemoveStreamPayload + payload: SetActiveStreamPayload } export interface ToggleActiveStreamAction { @@ -33,6 +37,17 @@ export interface ToggleActiveStreamAction { payload: UserIdPayload } +export interface RemoveStreamTrackPayload { + userId: string + stream: MediaStream + track: MediaStreamTrack +} + +export interface RemoveStreamTrackAction { + type: 'PEER_STREAM_TRACK_REMOVE' + payload: RemoveStreamTrackPayload +} + export interface UserIdPayload { userId: string } @@ -44,12 +59,19 @@ export const addStream = (payload: AddStreamPayload): AddStreamAction => ({ export const removeStream = ( userId: string, - stream?: MediaStream, + stream: MediaStream, ): RemoveStreamAction => ({ type: constants.STREAM_REMOVE, payload: { userId, stream }, }) +export const removeTrack = ( + payload: RemoveStreamTrackPayload, +): RemoveStreamTrackAction => ({ + type: constants.STREAM_TRACK_REMOVE, + payload, +}) + export const setActive = (userId: string): SetActiveStreamAction => ({ type: constants.ACTIVE_SET, payload: { userId }, @@ -64,4 +86,5 @@ export type StreamAction = AddStreamAction | RemoveStreamAction | SetActiveStreamAction | - ToggleActiveStreamAction + ToggleActiveStreamAction | + RemoveStreamTrackAction diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 6fb8d2f..3a15999 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -63,7 +63,16 @@ export default class App extends React.PureComponent { init() } onHangup = () => { - this.props.removeStream(constants.ME) + const localStreams = this.getLocalStreams() + localStreams.streams.forEach(s => { + this.props.removeStream(constants.ME, s.stream) + }) + } + getLocalStreams() { + return this.props.streams[constants.ME] || { + userId: constants.ME, + streams: [], + } } render () { const { @@ -86,10 +95,7 @@ export default class App extends React.PureComponent { 'chat-visible': this.state.chatVisible, }) - const localStreams = streams[constants.ME] || { - userId: constants.ME, - streams: [], - } + const localStreams = this.getLocalStreams() return (
diff --git a/src/client/components/Media.tsx b/src/client/components/Media.tsx index 1215429..e106f98 100644 --- a/src/client/components/Media.tsx +++ b/src/client/components/Media.tsx @@ -24,7 +24,6 @@ function mapStateToProps(state: State) { 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/constants.ts b/src/client/constants.ts index d55ae98..20a945c 100644 --- a/src/client/constants.ts +++ b/src/client/constants.ts @@ -37,6 +37,7 @@ export const SOCKET_EVENT_USERS = 'users' export const STREAM_ADD = 'PEER_STREAM_ADD' export const STREAM_REMOVE = 'PEER_STREAM_REMOVE' +export const STREAM_TRACK_REMOVE = 'PEER_STREAM_TRACK_REMOVE' export const STREAM_TYPE_CAMERA = 'camera' export const STREAM_TYPE_DESKTOP = 'desktop' diff --git a/src/client/reducers/peers.ts b/src/client/reducers/peers.ts index f338271..6214e82 100644 --- a/src/client/reducers/peers.ts +++ b/src/client/reducers/peers.ts @@ -4,6 +4,7 @@ import Peer from 'simple-peer' import { PeerAction } from '../actions/PeerActions' import * as constants from '../constants' import { MediaStreamAction } from '../actions/MediaActions' +import { RemoveStreamAction } from '../actions/StreamActions' export type PeersState = Record @@ -11,9 +12,26 @@ const defaultState: PeersState = {} let localStreams: Record = {} +function handleRemoveStream( + state: PeersState, + action: RemoveStreamAction, +): PeersState { + const stream = action.payload.stream + if (action.payload.userId === constants.ME) { + forEach(state, peer => { + console.log('removing track from peer') + stream.getTracks().forEach(track => { + peer.removeTrack(track, stream) + }) + }) + } + + return state +} + export default function peers( state = defaultState, - action: PeerAction | MediaStreamAction, + action: PeerAction | MediaStreamAction | RemoveStreamAction, ): PeersState { switch (action.type) { case constants.PEER_ADD: @@ -27,6 +45,8 @@ export default function peers( localStreams = {} forEach(state, peer => peer.destroy()) return defaultState + case constants.STREAM_REMOVE: + return handleRemoveStream(state, action) case constants.MEDIA_STREAM: if (action.status === 'resolved') { forEach(state, peer => { diff --git a/src/client/reducers/streams.test.ts b/src/client/reducers/streams.test.ts index 0150785..c8ba706 100644 --- a/src/client/reducers/streams.test.ts +++ b/src/client/reducers/streams.test.ts @@ -57,11 +57,11 @@ describe('reducers/alerts', () => { describe('removeStream', () => { it('removes a stream', () => { store.dispatch(StreamActions.addStream({ userId, stream })) - store.dispatch(StreamActions.removeStream(userId)) + store.dispatch(StreamActions.removeStream(userId, stream)) expect(store.getState().streams).toEqual({}) }) it('does not fail when no stream', () => { - store.dispatch(StreamActions.removeStream(userId)) + store.dispatch(StreamActions.removeStream(userId, stream)) }) }) diff --git a/src/client/reducers/streams.ts b/src/client/reducers/streams.ts index a3f4473..17f939e 100644 --- a/src/client/reducers/streams.ts +++ b/src/client/reducers/streams.ts @@ -1,7 +1,7 @@ import _debug from 'debug' import omit from 'lodash/omit' -import { AddStreamAction, RemoveStreamAction, StreamAction, StreamType } from '../actions/StreamActions' -import { STREAM_ADD, STREAM_REMOVE, MEDIA_STREAM } from '../constants' +import { AddStreamAction, RemoveStreamAction, StreamAction, StreamType, RemoveStreamTrackAction } from '../actions/StreamActions' +import { STREAM_ADD, STREAM_REMOVE, MEDIA_STREAM, STREAM_TRACK_REMOVE } from '../constants' import { createObjectURL, revokeObjectURL } from '../window' import { MediaStreamAction } from '../actions/MediaActions' @@ -99,6 +99,27 @@ function removeStream ( return omit(state, [userId]) } +function removeStreamTrack( + state: StreamsState, payload: RemoveStreamTrackAction['payload'], +): StreamsState { + const { userId, stream, track } = payload + const userStreams = state[userId] + if (!userStreams) { + return state + } + const index = userStreams.streams.map(s => s.stream).indexOf(stream) + if (index < 0) { + return state + } + stream.removeTrack(track) + if (stream.getTracks().length === 0) { + return removeStream(state, {userId, stream}) + } + // UI does not update when a stream track is removed so there is no need to + // update the state object + return state +} + export default function streams( state = defaultState, action: StreamAction | MediaStreamAction, @@ -108,6 +129,8 @@ export default function streams( return addStream(state, action.payload) case STREAM_REMOVE: return removeStream(state, action.payload) + case STREAM_TRACK_REMOVE: + return removeStreamTrack(state, action.payload) case MEDIA_STREAM: if (action.status === 'resolved') { return addStream(state, action.payload)