Use addTrack/removeTrack over addStream/removeStream

The addStream and removeStream are deprecated and the MDN docs
recommend using addStream/removeStream instead.

While we add tracks, we can also add event listeners to whether or not a
track has ended and then remove a stream once all tracks in the streams
have ended.
This commit is contained in:
Jerko Steiner 2020-03-10 11:21:33 +01:00
parent 53ddcdfcbf
commit f056048d62
16 changed files with 230 additions and 120 deletions

View File

@ -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 =

View File

@ -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 }))

View File

@ -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',

View File

@ -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 => ({

View File

@ -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<string, Peer.Instance>
play: () => void
sendMessage: (message: TextMessage) => void
streams: Record<string, AddStreamPayload>
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<AppProps, AppState> {
state: AppState = {
videos: {},
@ -65,7 +64,6 @@ export default class App extends React.PureComponent<AppProps, AppState> {
}
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<AppProps, AppState> {
'chat-visible': this.state.chatVisible,
})
const localStreams = streams[constants.ME] || {
userId: constants.ME,
streams: [],
}
return (
<div className="app">
<Side align='flex-end' left zIndex={2}>
@ -97,8 +100,14 @@ export default class App extends React.PureComponent<AppProps, AppState> {
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<AppProps, AppState> {
visible={this.state.chatVisible}
/>
<div className={classnames('videos', chatVisibleClassName)}>
{localStreams.filter(userId => !!streams[userId]).map(userId => (
<Video
videos={videos}
key={userId}
active={active === userId}
onClick={toggleActive}
play={play}
stream={streams[userId]}
userId={userId}
muted
mirrored={userId == constants.ME}
/>
))}
{localStreams.streams.map((s, i) => {
const key = localStreams.userId + '_' + i
return (
<Video
videos={videos}
key={key}
active={active === key}
onClick={toggleActive}
play={play}
stream={s}
userId={key}
muted
mirrored={s.type === 'camera'}
/>
)
})}
{
map(peers, (_, userId) => userId)
.filter(stream => !!stream)
.map(userId =>
<Video
active={userId === active}
key={userId}
onClick={toggleActive}
play={play}
stream={streams[userId]}
userId={userId}
videos={videos}
/>,
)
.map(userId => streams[userId])
.filter(userStreams => !!userStreams)
.map(userStreams => {
return userStreams.streams.map((s, i) => {
const key = userStreams.userId + '_' + i
return (
<Video
active={key === active}
key={key}
onClick={toggleActive}
play={play}
stream={s}
userId={key}
videos={videos}
/>
)
})
})
}
</div>
</div>

View File

@ -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,

View File

@ -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<typeof getDesktopStream>
let onRemoveStream: jest.MockedFunction<typeof removeStream>
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<ToolbarWrapper>(resolve => {
ReactDOM.render(
<ToolbarWrapper
@ -60,7 +66,7 @@ describe('components/Toolbar', () => {
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')!

View File

@ -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<ToolbarProps, ToolbarState> {
file = React.createRef<HTMLInputElement>()
desktopStream: MediaStream | undefined
constructor(props: ToolbarProps) {
super(props)
@ -121,7 +121,7 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
}
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<ToolbarProps, ToolbarState> {
className='stream-desktop'
icon='icon-display'
onClick={this.handleToggleShareDesktop}
on={!!this.desktopStream}
on={!!this.props.desktopStream}
title='Share Desktop'
/>

View File

@ -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<VideoWrapper>(resolve => {
const stream: StreamWithURL = {
stream: mediaStream,
url,
type: STREAM_TYPE_CAMERA,
}
ReactDOM.render(
<VideoWrapper
ref={instance => 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,
},
})
})
})

View File

@ -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<string, unknown>
onClick: (userId: string) => void
active: boolean
stream?: AddStreamPayload
stream?: StreamWithURL
userId: string
muted: boolean
mirrored: boolean

View File

@ -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'

View File

@ -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 = {

View File

@ -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)
})
})

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}