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:
parent
53ddcdfcbf
commit
f056048d62
@ -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 =
|
||||
|
||||
@ -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 }))
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 => ({
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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')!
|
||||
|
||||
@ -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'
|
||||
/>
|
||||
|
||||
|
||||
@ -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,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user