Remove each user stream individually

This commit is contained in:
Jerko Steiner 2020-03-10 11:58:15 +01:00
parent f056048d62
commit 46a0b1f7ea
8 changed files with 106 additions and 20 deletions

View File

@ -68,9 +68,20 @@ class PeerHandler {
} }
handleTrack = (track: MediaStreamTrack, stream: MediaStream) => { handleTrack = (track: MediaStreamTrack, stream: MediaStream) => {
const { user, dispatch } = this 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({ dispatch(StreamActions.addStream({
userId: user.id, userId,
stream, stream,
})) }))
} }
@ -97,10 +108,13 @@ class PeerHandler {
} }
} }
handleClose = () => { handleClose = () => {
const { dispatch, user } = this const { dispatch, user, getState } = this
debug('peer: %s, close', user.id)
dispatch(NotifyActions.error('Peer connection closed')) 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)) dispatch(removePeer(user.id))
} }
} }

View File

@ -20,12 +20,16 @@ export interface RemoveStreamAction {
export interface RemoveStreamPayload { export interface RemoveStreamPayload {
userId: string userId: string
stream?: MediaStream stream: MediaStream
}
export interface SetActiveStreamPayload {
userId: string
} }
export interface SetActiveStreamAction { export interface SetActiveStreamAction {
type: 'ACTIVE_SET' type: 'ACTIVE_SET'
payload: RemoveStreamPayload payload: SetActiveStreamPayload
} }
export interface ToggleActiveStreamAction { export interface ToggleActiveStreamAction {
@ -33,6 +37,17 @@ export interface ToggleActiveStreamAction {
payload: UserIdPayload payload: UserIdPayload
} }
export interface RemoveStreamTrackPayload {
userId: string
stream: MediaStream
track: MediaStreamTrack
}
export interface RemoveStreamTrackAction {
type: 'PEER_STREAM_TRACK_REMOVE'
payload: RemoveStreamTrackPayload
}
export interface UserIdPayload { export interface UserIdPayload {
userId: string userId: string
} }
@ -44,12 +59,19 @@ export const addStream = (payload: AddStreamPayload): AddStreamAction => ({
export const removeStream = ( export const removeStream = (
userId: string, userId: string,
stream?: MediaStream, stream: MediaStream,
): RemoveStreamAction => ({ ): RemoveStreamAction => ({
type: constants.STREAM_REMOVE, type: constants.STREAM_REMOVE,
payload: { userId, stream }, payload: { userId, stream },
}) })
export const removeTrack = (
payload: RemoveStreamTrackPayload,
): RemoveStreamTrackAction => ({
type: constants.STREAM_TRACK_REMOVE,
payload,
})
export const setActive = (userId: string): SetActiveStreamAction => ({ export const setActive = (userId: string): SetActiveStreamAction => ({
type: constants.ACTIVE_SET, type: constants.ACTIVE_SET,
payload: { userId }, payload: { userId },
@ -64,4 +86,5 @@ export type StreamAction =
AddStreamAction | AddStreamAction |
RemoveStreamAction | RemoveStreamAction |
SetActiveStreamAction | SetActiveStreamAction |
ToggleActiveStreamAction ToggleActiveStreamAction |
RemoveStreamTrackAction

View File

@ -63,7 +63,16 @@ export default class App extends React.PureComponent<AppProps, AppState> {
init() init()
} }
onHangup = () => { 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 () { render () {
const { const {
@ -86,10 +95,7 @@ export default class App extends React.PureComponent<AppProps, AppState> {
'chat-visible': this.state.chatVisible, 'chat-visible': this.state.chatVisible,
}) })
const localStreams = streams[constants.ME] || { const localStreams = this.getLocalStreams()
userId: constants.ME,
streams: [],
}
return ( return (
<div className="app"> <div className="app">

View File

@ -24,7 +24,6 @@ function mapStateToProps(state: State) {
const hidden = !!localStream && const hidden = !!localStream &&
localStream.streams.filter(s => s.type === STREAM_TYPE_CAMERA).length > 0 localStream.streams.filter(s => s.type === STREAM_TYPE_CAMERA).length > 0
const visible = !hidden const visible = !hidden
console.log('visible', visible)
return { return {
...state.media, ...state.media,
visible, visible,

View File

@ -37,6 +37,7 @@ export const SOCKET_EVENT_USERS = 'users'
export const STREAM_ADD = 'PEER_STREAM_ADD' export const STREAM_ADD = 'PEER_STREAM_ADD'
export const STREAM_REMOVE = 'PEER_STREAM_REMOVE' 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_CAMERA = 'camera'
export const STREAM_TYPE_DESKTOP = 'desktop' export const STREAM_TYPE_DESKTOP = 'desktop'

View File

@ -4,6 +4,7 @@ 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' import { MediaStreamAction } from '../actions/MediaActions'
import { RemoveStreamAction } from '../actions/StreamActions'
export type PeersState = Record<string, Peer.Instance> export type PeersState = Record<string, Peer.Instance>
@ -11,9 +12,26 @@ const defaultState: PeersState = {}
let localStreams: Record<string, MediaStream> = {} let localStreams: Record<string, MediaStream> = {}
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( export default function peers(
state = defaultState, state = defaultState,
action: PeerAction | MediaStreamAction, action: PeerAction | MediaStreamAction | RemoveStreamAction,
): PeersState { ): PeersState {
switch (action.type) { switch (action.type) {
case constants.PEER_ADD: case constants.PEER_ADD:
@ -27,6 +45,8 @@ export default function peers(
localStreams = {} localStreams = {}
forEach(state, peer => peer.destroy()) forEach(state, peer => peer.destroy())
return defaultState return defaultState
case constants.STREAM_REMOVE:
return handleRemoveStream(state, action)
case constants.MEDIA_STREAM: case constants.MEDIA_STREAM:
if (action.status === 'resolved') { if (action.status === 'resolved') {
forEach(state, peer => { forEach(state, peer => {

View File

@ -57,11 +57,11 @@ describe('reducers/alerts', () => {
describe('removeStream', () => { describe('removeStream', () => {
it('removes a stream', () => { it('removes a stream', () => {
store.dispatch(StreamActions.addStream({ userId, stream })) store.dispatch(StreamActions.addStream({ userId, stream }))
store.dispatch(StreamActions.removeStream(userId)) store.dispatch(StreamActions.removeStream(userId, stream))
expect(store.getState().streams).toEqual({}) expect(store.getState().streams).toEqual({})
}) })
it('does not fail when no stream', () => { it('does not fail when no stream', () => {
store.dispatch(StreamActions.removeStream(userId)) store.dispatch(StreamActions.removeStream(userId, stream))
}) })
}) })

View File

@ -1,7 +1,7 @@
import _debug from 'debug' import _debug from 'debug'
import omit from 'lodash/omit' import omit from 'lodash/omit'
import { AddStreamAction, RemoveStreamAction, StreamAction, StreamType } from '../actions/StreamActions' import { AddStreamAction, RemoveStreamAction, StreamAction, StreamType, RemoveStreamTrackAction } from '../actions/StreamActions'
import { STREAM_ADD, STREAM_REMOVE, MEDIA_STREAM } from '../constants' import { STREAM_ADD, STREAM_REMOVE, MEDIA_STREAM, STREAM_TRACK_REMOVE } from '../constants'
import { createObjectURL, revokeObjectURL } from '../window' import { createObjectURL, revokeObjectURL } from '../window'
import { MediaStreamAction } from '../actions/MediaActions' import { MediaStreamAction } from '../actions/MediaActions'
@ -99,6 +99,27 @@ function removeStream (
return omit(state, [userId]) 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( export default function streams(
state = defaultState, state = defaultState,
action: StreamAction | MediaStreamAction, action: StreamAction | MediaStreamAction,
@ -108,6 +129,8 @@ export default function streams(
return addStream(state, action.payload) return addStream(state, action.payload)
case STREAM_REMOVE: case STREAM_REMOVE:
return removeStream(state, action.payload) return removeStream(state, action.payload)
case STREAM_TRACK_REMOVE:
return removeStreamTrack(state, action.payload)
case MEDIA_STREAM: case MEDIA_STREAM:
if (action.status === 'resolved') { if (action.status === 'resolved') {
return addStream(state, action.payload) return addStream(state, action.payload)