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 { 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 _debug from 'debug'
|
||||||
|
import { AddStreamPayload } from './StreamActions'
|
||||||
|
|
||||||
const debug = _debug('peercalls')
|
const debug = _debug('peercalls')
|
||||||
|
|
||||||
@ -111,8 +112,9 @@ export const getMediaStream = makeAction(
|
|||||||
MEDIA_STREAM,
|
MEDIA_STREAM,
|
||||||
async (constraints: GetMediaConstraints) => {
|
async (constraints: GetMediaConstraints) => {
|
||||||
debug('getMediaStream', constraints)
|
debug('getMediaStream', constraints)
|
||||||
const payload: MediaStreamPayload = {
|
const payload: AddStreamPayload = {
|
||||||
stream: await getUserMedia(constraints),
|
stream: await getUserMedia(constraints),
|
||||||
|
type: STREAM_TYPE_CAMERA,
|
||||||
userId: ME,
|
userId: ME,
|
||||||
}
|
}
|
||||||
return payload
|
return payload
|
||||||
@ -123,21 +125,17 @@ export const getDesktopStream = makeAction(
|
|||||||
MEDIA_STREAM,
|
MEDIA_STREAM,
|
||||||
async () => {
|
async () => {
|
||||||
debug('getDesktopStream')
|
debug('getDesktopStream')
|
||||||
const payload: MediaStreamPayload = {
|
const payload: AddStreamPayload = {
|
||||||
stream: await getDisplayMedia(),
|
stream: await getDisplayMedia(),
|
||||||
userId: ME_DESKTOP,
|
type: STREAM_TYPE_DESKTOP,
|
||||||
|
userId: ME,
|
||||||
}
|
}
|
||||||
return payload
|
return payload
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface MediaStreamPayload {
|
|
||||||
stream: MediaStream
|
|
||||||
userId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MediaEnumerateAction = AsyncAction<'MEDIA_ENUMERATE', MediaDevice[]>
|
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 MediaPlayAction = AsyncAction<'MEDIA_PLAY', void>
|
||||||
|
|
||||||
export type MediaAction =
|
export type MediaAction =
|
||||||
|
|||||||
@ -57,20 +57,18 @@ class PeerHandler {
|
|||||||
const state = getState()
|
const state = getState()
|
||||||
const peer = state.peers[user.id]
|
const peer = state.peers[user.id]
|
||||||
const localStream = state.streams[constants.ME]
|
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
|
// 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
|
// call, now is the time to share local media stream with the peer since
|
||||||
// we no longer automatically send the stream to the peer.
|
// we no longer automatically send the stream to the peer.
|
||||||
peer.addStream(localStream.stream)
|
s.stream.getTracks().forEach(track => {
|
||||||
|
peer.addTrack(track, s.stream)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const desktopStream = state.streams[constants.ME_DESKTOP]
|
handleTrack = (track: MediaStreamTrack, stream: MediaStream) => {
|
||||||
if (desktopStream && desktopStream.stream) {
|
|
||||||
peer.addStream(desktopStream.stream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleStream = (stream: MediaStream) => {
|
|
||||||
const { user, dispatch } = this
|
const { user, dispatch } = this
|
||||||
debug('peer: %s, stream', user.id)
|
debug('peer: %s, track', user.id)
|
||||||
dispatch(StreamActions.addStream({
|
dispatch(StreamActions.addStream({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
stream,
|
stream,
|
||||||
@ -160,7 +158,7 @@ export function createPeer (options: CreatePeerOptions) {
|
|||||||
peer.once(constants.PEER_EVENT_CONNECT, handler.handleConnect)
|
peer.once(constants.PEER_EVENT_CONNECT, handler.handleConnect)
|
||||||
peer.once(constants.PEER_EVENT_CLOSE, handler.handleClose)
|
peer.once(constants.PEER_EVENT_CLOSE, handler.handleClose)
|
||||||
peer.on(constants.PEER_EVENT_SIGNAL, handler.handleSignal)
|
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)
|
peer.on(constants.PEER_EVENT_DATA, handler.handleData)
|
||||||
|
|
||||||
dispatch(addPeer({ peer, userId }))
|
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({
|
expect(store.getState().streams).toEqual({
|
||||||
b: {
|
b: {
|
||||||
@ -151,7 +151,8 @@ describe('SocketActions', () => {
|
|||||||
describe('close', () => {
|
describe('close', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const stream = new MediaStream()
|
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({
|
expect(store.getState().streams).toEqual({
|
||||||
b: {
|
b: {
|
||||||
userId: 'b',
|
userId: 'b',
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import * as constants from '../constants'
|
import * as constants from '../constants'
|
||||||
|
|
||||||
|
export type StreamType = 'camera' | 'desktop'
|
||||||
|
|
||||||
export interface AddStreamPayload {
|
export interface AddStreamPayload {
|
||||||
userId: string
|
userId: string
|
||||||
|
type?: StreamType
|
||||||
stream: MediaStream
|
stream: MediaStream
|
||||||
url?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddStreamAction {
|
export interface AddStreamAction {
|
||||||
@ -18,6 +20,7 @@ export interface RemoveStreamAction {
|
|||||||
|
|
||||||
export interface RemoveStreamPayload {
|
export interface RemoveStreamPayload {
|
||||||
userId: string
|
userId: string
|
||||||
|
stream?: MediaStream
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetActiveStreamAction {
|
export interface SetActiveStreamAction {
|
||||||
@ -39,9 +42,12 @@ export const addStream = (payload: AddStreamPayload): AddStreamAction => ({
|
|||||||
payload,
|
payload,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const removeStream = (userId: string): RemoveStreamAction => ({
|
export const removeStream = (
|
||||||
|
userId: string,
|
||||||
|
stream?: MediaStream,
|
||||||
|
): RemoveStreamAction => ({
|
||||||
type: constants.STREAM_REMOVE,
|
type: constants.STREAM_REMOVE,
|
||||||
payload: { userId },
|
payload: { userId, stream },
|
||||||
})
|
})
|
||||||
|
|
||||||
export const setActive = (userId: string): SetActiveStreamAction => ({
|
export const setActive = (userId: string): SetActiveStreamAction => ({
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import Peer from 'simple-peer'
|
|||||||
import { Message } from '../actions/ChatActions'
|
import { Message } from '../actions/ChatActions'
|
||||||
import { dismissNotification, Notification } from '../actions/NotifyActions'
|
import { dismissNotification, Notification } from '../actions/NotifyActions'
|
||||||
import { TextMessage } from '../actions/PeerActions'
|
import { TextMessage } from '../actions/PeerActions'
|
||||||
import { AddStreamPayload, removeStream } from '../actions/StreamActions'
|
import { removeStream } from '../actions/StreamActions'
|
||||||
import * as constants from '../constants'
|
import * as constants from '../constants'
|
||||||
import Chat from './Chat'
|
import Chat from './Chat'
|
||||||
import { Media } from './Media'
|
import { Media } from './Media'
|
||||||
@ -14,6 +14,7 @@ import { Side } from './Side'
|
|||||||
import Toolbar from './Toolbar'
|
import Toolbar from './Toolbar'
|
||||||
import Video from './Video'
|
import Video from './Video'
|
||||||
import { getDesktopStream } from '../actions/MediaActions'
|
import { getDesktopStream } from '../actions/MediaActions'
|
||||||
|
import { StreamsState } from '../reducers/streams'
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
active: string | null
|
active: string | null
|
||||||
@ -25,7 +26,7 @@ export interface AppProps {
|
|||||||
peers: Record<string, Peer.Instance>
|
peers: Record<string, Peer.Instance>
|
||||||
play: () => void
|
play: () => void
|
||||||
sendMessage: (message: TextMessage) => void
|
sendMessage: (message: TextMessage) => void
|
||||||
streams: Record<string, AddStreamPayload>
|
streams: StreamsState
|
||||||
getDesktopStream: typeof getDesktopStream
|
getDesktopStream: typeof getDesktopStream
|
||||||
removeStream: typeof removeStream
|
removeStream: typeof removeStream
|
||||||
onSendFile: (file: File) => void
|
onSendFile: (file: File) => void
|
||||||
@ -37,8 +38,6 @@ export interface AppState {
|
|||||||
chatVisible: boolean
|
chatVisible: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const localStreams: string[] = [constants.ME, constants.ME_DESKTOP]
|
|
||||||
|
|
||||||
export default class App extends React.PureComponent<AppProps, AppState> {
|
export default class App extends React.PureComponent<AppProps, AppState> {
|
||||||
state: AppState = {
|
state: AppState = {
|
||||||
videos: {},
|
videos: {},
|
||||||
@ -65,7 +64,6 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
onHangup = () => {
|
onHangup = () => {
|
||||||
this.props.removeStream(constants.ME)
|
this.props.removeStream(constants.ME)
|
||||||
this.props.removeStream(constants.ME_DESKTOP)
|
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
@ -88,6 +86,11 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
|||||||
'chat-visible': this.state.chatVisible,
|
'chat-visible': this.state.chatVisible,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const localStreams = streams[constants.ME] || {
|
||||||
|
userId: constants.ME,
|
||||||
|
streams: [],
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<Side align='flex-end' left zIndex={2}>
|
<Side align='flex-end' left zIndex={2}>
|
||||||
@ -97,8 +100,14 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
|||||||
onToggleChat={this.handleToggleChat}
|
onToggleChat={this.handleToggleChat}
|
||||||
onSendFile={onSendFile}
|
onSendFile={onSendFile}
|
||||||
onHangup={this.onHangup}
|
onHangup={this.onHangup}
|
||||||
stream={streams[constants.ME]}
|
stream={
|
||||||
desktopStream={streams[constants.ME_DESKTOP]}
|
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}
|
onGetDesktopStream={this.props.getDesktopStream}
|
||||||
onRemoveStream={this.props.removeStream}
|
onRemoveStream={this.props.removeStream}
|
||||||
/>
|
/>
|
||||||
@ -117,33 +126,43 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
|||||||
visible={this.state.chatVisible}
|
visible={this.state.chatVisible}
|
||||||
/>
|
/>
|
||||||
<div className={classnames('videos', chatVisibleClassName)}>
|
<div className={classnames('videos', chatVisibleClassName)}>
|
||||||
{localStreams.filter(userId => !!streams[userId]).map(userId => (
|
{localStreams.streams.map((s, i) => {
|
||||||
|
const key = localStreams.userId + '_' + i
|
||||||
|
return (
|
||||||
<Video
|
<Video
|
||||||
videos={videos}
|
videos={videos}
|
||||||
key={userId}
|
key={key}
|
||||||
active={active === userId}
|
active={active === key}
|
||||||
onClick={toggleActive}
|
onClick={toggleActive}
|
||||||
play={play}
|
play={play}
|
||||||
stream={streams[userId]}
|
stream={s}
|
||||||
userId={userId}
|
userId={key}
|
||||||
muted
|
muted
|
||||||
mirrored={userId == constants.ME}
|
mirrored={s.type === 'camera'}
|
||||||
/>
|
/>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
{
|
{
|
||||||
map(peers, (_, userId) => userId)
|
map(peers, (_, userId) => userId)
|
||||||
.filter(stream => !!stream)
|
.filter(stream => !!stream)
|
||||||
.map(userId =>
|
.map(userId => streams[userId])
|
||||||
|
.filter(userStreams => !!userStreams)
|
||||||
|
.map(userStreams => {
|
||||||
|
return userStreams.streams.map((s, i) => {
|
||||||
|
const key = userStreams.userId + '_' + i
|
||||||
|
return (
|
||||||
<Video
|
<Video
|
||||||
active={userId === active}
|
active={key === active}
|
||||||
key={userId}
|
key={key}
|
||||||
onClick={toggleActive}
|
onClick={toggleActive}
|
||||||
play={play}
|
play={play}
|
||||||
stream={streams[userId]}
|
stream={s}
|
||||||
userId={userId}
|
userId={key}
|
||||||
videos={videos}
|
videos={videos}
|
||||||
/>,
|
/>
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { MediaState } from '../reducers/media'
|
|||||||
import { State } from '../store'
|
import { State } from '../store'
|
||||||
import { Alerts, Alert } from './Alerts'
|
import { Alerts, Alert } from './Alerts'
|
||||||
import { info, warning, error } from '../actions/NotifyActions'
|
import { info, warning, error } from '../actions/NotifyActions'
|
||||||
import { ME } from '../constants'
|
import { ME, STREAM_TYPE_CAMERA } from '../constants'
|
||||||
|
|
||||||
export type MediaProps = MediaState & {
|
export type MediaProps = MediaState & {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
@ -21,7 +21,10 @@ export type MediaProps = MediaState & {
|
|||||||
|
|
||||||
function mapStateToProps(state: State) {
|
function mapStateToProps(state: State) {
|
||||||
const localStream = state.streams[ME]
|
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 {
|
return {
|
||||||
...state.media,
|
...state.media,
|
||||||
visible,
|
visible,
|
||||||
|
|||||||
@ -2,11 +2,12 @@ jest.mock('../window')
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
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 { 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', () => {
|
describe('components/Toolbar', () => {
|
||||||
|
|
||||||
@ -42,7 +43,7 @@ describe('components/Toolbar', () => {
|
|||||||
let onHangup: jest.Mock<() => void>
|
let onHangup: jest.Mock<() => void>
|
||||||
let onGetDesktopStream: jest.MockedFunction<typeof getDesktopStream>
|
let onGetDesktopStream: jest.MockedFunction<typeof getDesktopStream>
|
||||||
let onRemoveStream: jest.MockedFunction<typeof removeStream>
|
let onRemoveStream: jest.MockedFunction<typeof removeStream>
|
||||||
let desktopStream: AddStreamPayload | undefined
|
let desktopStream: StreamWithURL | undefined
|
||||||
async function render () {
|
async function render () {
|
||||||
mediaStream = new MediaStream()
|
mediaStream = new MediaStream()
|
||||||
onToggleChat = jest.fn()
|
onToggleChat = jest.fn()
|
||||||
@ -51,6 +52,11 @@ describe('components/Toolbar', () => {
|
|||||||
onGetDesktopStream = jest.fn()
|
onGetDesktopStream = jest.fn()
|
||||||
onRemoveStream = jest.fn()
|
onRemoveStream = jest.fn()
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
|
const stream: StreamWithURL = {
|
||||||
|
stream: mediaStream,
|
||||||
|
type: STREAM_TYPE_CAMERA,
|
||||||
|
url,
|
||||||
|
}
|
||||||
await new Promise<ToolbarWrapper>(resolve => {
|
await new Promise<ToolbarWrapper>(resolve => {
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<ToolbarWrapper
|
<ToolbarWrapper
|
||||||
@ -60,7 +66,7 @@ describe('components/Toolbar', () => {
|
|||||||
onToggleChat={onToggleChat}
|
onToggleChat={onToggleChat}
|
||||||
onSendFile={onSendFile}
|
onSendFile={onSendFile}
|
||||||
messagesCount={1}
|
messagesCount={1}
|
||||||
stream={{ userId: '', stream: mediaStream, url }}
|
stream={stream}
|
||||||
desktopStream={desktopStream}
|
desktopStream={desktopStream}
|
||||||
onGetDesktopStream={onGetDesktopStream}
|
onGetDesktopStream={onGetDesktopStream}
|
||||||
onRemoveStream={onRemoveStream}
|
onRemoveStream={onRemoveStream}
|
||||||
@ -160,8 +166,8 @@ describe('components/Toolbar', () => {
|
|||||||
})
|
})
|
||||||
it('stops desktop sharing', async () => {
|
it('stops desktop sharing', async () => {
|
||||||
desktopStream = {
|
desktopStream = {
|
||||||
userId: ME_DESKTOP,
|
|
||||||
stream: new MediaStream(),
|
stream: new MediaStream(),
|
||||||
|
type: STREAM_TYPE_DESKTOP,
|
||||||
}
|
}
|
||||||
await render()
|
await render()
|
||||||
const shareDesktop = node.querySelector('.share-desktop')!
|
const shareDesktop = node.querySelector('.share-desktop')!
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import screenfull from 'screenfull'
|
import screenfull from 'screenfull'
|
||||||
import { AddStreamPayload, removeStream } from '../actions/StreamActions'
|
import { removeStream } from '../actions/StreamActions'
|
||||||
import { ME_DESKTOP } from '../constants'
|
|
||||||
import { getDesktopStream } from '../actions/MediaActions'
|
import { getDesktopStream } from '../actions/MediaActions'
|
||||||
|
import { StreamWithURL } from '../reducers/streams'
|
||||||
|
import { ME } from '../constants'
|
||||||
|
|
||||||
const hidden = {
|
const hidden = {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
@ -11,8 +12,8 @@ const hidden = {
|
|||||||
|
|
||||||
export interface ToolbarProps {
|
export interface ToolbarProps {
|
||||||
messagesCount: number
|
messagesCount: number
|
||||||
stream: AddStreamPayload
|
stream: StreamWithURL
|
||||||
desktopStream: AddStreamPayload | undefined
|
desktopStream: StreamWithURL | undefined
|
||||||
onToggleChat: () => void
|
onToggleChat: () => void
|
||||||
onGetDesktopStream: typeof getDesktopStream
|
onGetDesktopStream: typeof getDesktopStream
|
||||||
onRemoveStream: typeof removeStream
|
onRemoveStream: typeof removeStream
|
||||||
@ -61,7 +62,6 @@ function ToolbarButton(props: ToolbarButtonProps) {
|
|||||||
export default class Toolbar
|
export default class Toolbar
|
||||||
extends React.PureComponent<ToolbarProps, ToolbarState> {
|
extends React.PureComponent<ToolbarProps, ToolbarState> {
|
||||||
file = React.createRef<HTMLInputElement>()
|
file = React.createRef<HTMLInputElement>()
|
||||||
desktopStream: MediaStream | undefined
|
|
||||||
|
|
||||||
constructor(props: ToolbarProps) {
|
constructor(props: ToolbarProps) {
|
||||||
super(props)
|
super(props)
|
||||||
@ -121,7 +121,7 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
|
|||||||
}
|
}
|
||||||
handleToggleShareDesktop = () => {
|
handleToggleShareDesktop = () => {
|
||||||
if (this.props.desktopStream) {
|
if (this.props.desktopStream) {
|
||||||
this.props.onRemoveStream(ME_DESKTOP)
|
this.props.onRemoveStream(ME, this.props.desktopStream.stream)
|
||||||
} else {
|
} else {
|
||||||
this.props.onGetDesktopStream().catch(() => {})
|
this.props.onGetDesktopStream().catch(() => {})
|
||||||
}
|
}
|
||||||
@ -162,7 +162,7 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
|
|||||||
className='stream-desktop'
|
className='stream-desktop'
|
||||||
icon='icon-display'
|
icon='icon-display'
|
||||||
onClick={this.handleToggleShareDesktop}
|
onClick={this.handleToggleShareDesktop}
|
||||||
on={!!this.desktopStream}
|
on={!!this.props.desktopStream}
|
||||||
title='Share Desktop'
|
title='Share Desktop'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -2,14 +2,15 @@ jest.mock('../window')
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
import { AddStreamPayload } from '../actions/StreamActions'
|
|
||||||
import Video, { VideoProps } from './Video'
|
import Video, { VideoProps } from './Video'
|
||||||
import { MediaStream } from '../window'
|
import { MediaStream } from '../window'
|
||||||
|
import { STREAM_TYPE_CAMERA } from '../constants'
|
||||||
|
import { StreamWithURL } from '../reducers/streams'
|
||||||
|
|
||||||
describe('components/Video', () => {
|
describe('components/Video', () => {
|
||||||
|
|
||||||
interface VideoState {
|
interface VideoState {
|
||||||
stream: null | AddStreamPayload
|
stream: null | StreamWithURL
|
||||||
}
|
}
|
||||||
|
|
||||||
const play = jest.fn()
|
const play = jest.fn()
|
||||||
@ -61,12 +62,17 @@ describe('components/Video', () => {
|
|||||||
mediaStream = new MediaStream()
|
mediaStream = new MediaStream()
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
component = await new Promise<VideoWrapper>(resolve => {
|
component = await new Promise<VideoWrapper>(resolve => {
|
||||||
|
const stream: StreamWithURL = {
|
||||||
|
stream: mediaStream,
|
||||||
|
url,
|
||||||
|
type: STREAM_TYPE_CAMERA,
|
||||||
|
}
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<VideoWrapper
|
<VideoWrapper
|
||||||
ref={instance => resolve(instance!)}
|
ref={instance => resolve(instance!)}
|
||||||
videos={videos}
|
videos={videos}
|
||||||
active={flags.active}
|
active={flags.active}
|
||||||
stream={{ stream: mediaStream, url, userId: 'test' }}
|
stream={stream}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
play={play}
|
play={play}
|
||||||
userId="test"
|
userId="test"
|
||||||
@ -100,22 +106,38 @@ describe('components/Video', () => {
|
|||||||
it('updates src only when changed', () => {
|
it('updates src only when changed', () => {
|
||||||
mediaStream = new MediaStream()
|
mediaStream = new MediaStream()
|
||||||
component.setState({
|
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')
|
expect(video.videoRef.current!.src).toBe('http://localhost/test')
|
||||||
component.setState({
|
component.setState({
|
||||||
stream: { url: 'test', stream: mediaStream, userId: '' },
|
stream: {
|
||||||
|
url: 'test',
|
||||||
|
stream: mediaStream,
|
||||||
|
type: STREAM_TYPE_CAMERA,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('updates srcObject only when changed', () => {
|
it('updates srcObject only when changed', () => {
|
||||||
video.videoRef.current!.srcObject = null
|
video.videoRef.current!.srcObject = null
|
||||||
mediaStream = new MediaStream()
|
mediaStream = new MediaStream()
|
||||||
component.setState({
|
component.setState({
|
||||||
stream: { url: 'test', stream: mediaStream, userId: '' },
|
stream: {
|
||||||
|
url: 'test',
|
||||||
|
stream: mediaStream,
|
||||||
|
type: STREAM_TYPE_CAMERA,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
expect(video.videoRef.current!.srcObject).toBe(mediaStream)
|
expect(video.videoRef.current!.srcObject).toBe(mediaStream)
|
||||||
component.setState({
|
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 React, { ReactEventHandler } from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import socket from '../socket'
|
import socket from '../socket'
|
||||||
import { AddStreamPayload } from '../actions/StreamActions'
|
import { StreamWithURL } from '../reducers/streams'
|
||||||
|
|
||||||
export interface VideoProps {
|
export interface VideoProps {
|
||||||
videos: Record<string, unknown>
|
videos: Record<string, unknown>
|
||||||
onClick: (userId: string) => void
|
onClick: (userId: string) => void
|
||||||
active: boolean
|
active: boolean
|
||||||
stream?: AddStreamPayload
|
stream?: StreamWithURL
|
||||||
userId: string
|
userId: string
|
||||||
muted: boolean
|
muted: boolean
|
||||||
mirrored: boolean
|
mirrored: boolean
|
||||||
|
|||||||
@ -8,7 +8,6 @@ export const ALERT_CLEAR = 'ALERT_CLEAR'
|
|||||||
export const INIT = 'INIT'
|
export const INIT = 'INIT'
|
||||||
|
|
||||||
export const ME = '_me_'
|
export const ME = '_me_'
|
||||||
export const ME_DESKTOP = '_me_desktop'
|
|
||||||
|
|
||||||
export const NOTIFY = 'NOTIFY'
|
export const NOTIFY = 'NOTIFY'
|
||||||
export const NOTIFY_DISMISS = 'NOTIFY_DISMISS'
|
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_CONNECT = 'connect'
|
||||||
export const PEER_EVENT_CLOSE = 'close'
|
export const PEER_EVENT_CLOSE = 'close'
|
||||||
export const PEER_EVENT_SIGNAL = 'signal'
|
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 PEER_EVENT_DATA = 'data'
|
||||||
|
|
||||||
export const SOCKET_EVENT_SIGNAL = 'signal'
|
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_ADD = 'PEER_STREAM_ADD'
|
||||||
export const STREAM_REMOVE = 'PEER_STREAM_REMOVE'
|
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 = {
|
state.streams = {
|
||||||
[constants.ME]: {
|
[constants.ME]: {
|
||||||
userId: constants.ME,
|
userId: constants.ME,
|
||||||
|
streams: [{
|
||||||
stream: new MediaStream(),
|
stream: new MediaStream(),
|
||||||
|
type: constants.STREAM_TYPE_CAMERA,
|
||||||
url: 'blob://',
|
url: 'blob://',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
'other-user': {
|
'other-user': {
|
||||||
userId: 'other-user',
|
userId: 'other-user',
|
||||||
|
streams: [{
|
||||||
stream: new MediaStream(),
|
stream: new MediaStream(),
|
||||||
|
type: undefined,
|
||||||
url: 'blob://',
|
url: 'blob://',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
state.peers = {
|
state.peers = {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import * as MediaActions from '../actions/MediaActions'
|
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 { createStore, Store } from '../store'
|
||||||
import SimplePeer from 'simple-peer'
|
import SimplePeer from 'simple-peer'
|
||||||
|
|
||||||
@ -102,6 +102,7 @@ describe('media', () => {
|
|||||||
video: true,
|
video: true,
|
||||||
}))
|
}))
|
||||||
expect(result.stream).toBe(stream)
|
expect(result.stream).toBe(stream)
|
||||||
|
expect(result.type).toBe(STREAM_TYPE_CAMERA)
|
||||||
expect(result.userId).toBe(ME)
|
expect(result.userId).toBe(ME)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,8 +110,11 @@ describe('media', () => {
|
|||||||
it('adds the local stream to the map of videos', async () => {
|
it('adds the local stream to the map of videos', async () => {
|
||||||
expect(store.getState().streams[ME]).toBeFalsy()
|
expect(store.getState().streams[ME]).toBeFalsy()
|
||||||
await dispatch()
|
await dispatch()
|
||||||
expect(store.getState().streams[ME]).toBeTruthy()
|
const localStreams = store.getState().streams[ME]
|
||||||
expect(store.getState().streams[ME].stream).toBe(stream)
|
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() {
|
async function dispatch() {
|
||||||
const result = await store.dispatch(MediaActions.getDesktopStream())
|
const result = await store.dispatch(MediaActions.getDesktopStream())
|
||||||
expect(result.stream).toBe(stream)
|
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 () => {
|
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()
|
await dispatch()
|
||||||
expect(store.getState().streams[ME_DESKTOP]).toBeTruthy()
|
const localStreams = store.getState().streams[ME]
|
||||||
expect(store.getState().streams[ME_DESKTOP].stream).toBe(stream)
|
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,
|
action: AnyAction,
|
||||||
) {
|
) {
|
||||||
if (isRejectedAction(action)) {
|
if (isRejectedAction(action)) {
|
||||||
action = error(action.payload)
|
action = error('' + action.payload)
|
||||||
}
|
}
|
||||||
return handleNotifications(state, action)
|
return handleNotifications(state, action)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,11 +29,15 @@ export default function peers(
|
|||||||
return defaultState
|
return defaultState
|
||||||
case constants.MEDIA_STREAM:
|
case constants.MEDIA_STREAM:
|
||||||
if (action.status === 'resolved') {
|
if (action.status === 'resolved') {
|
||||||
// userId can be ME or ME_DESKTOP
|
|
||||||
forEach(state, peer => {
|
forEach(state, peer => {
|
||||||
const localStream = localStreams[action.payload.userId]
|
const localStream = localStreams[action.payload.userId]
|
||||||
localStream && peer.removeStream(localStream)
|
localStream && localStream.getTracks().forEach(track => {
|
||||||
peer.addStream(action.payload.stream)
|
peer.removeTrack(track, localStream)
|
||||||
|
})
|
||||||
|
const stream = action.payload.stream
|
||||||
|
stream.getTracks().forEach(track => {
|
||||||
|
peer.addTrack(track, stream)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
localStreams[action.payload.userId] = action.payload.stream
|
localStreams[action.payload.userId] = action.payload.stream
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import _debug from 'debug'
|
import _debug from 'debug'
|
||||||
import omit from 'lodash/omit'
|
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 { STREAM_ADD, STREAM_REMOVE, MEDIA_STREAM } from '../constants'
|
||||||
import { createObjectURL, revokeObjectURL } from '../window'
|
import { createObjectURL, revokeObjectURL } from '../window'
|
||||||
import { MediaStreamPayload, MediaStreamAction } from '../actions/MediaActions'
|
import { MediaStreamAction } from '../actions/MediaActions'
|
||||||
|
|
||||||
const debug = _debug('peercalls')
|
const debug = _debug('peercalls')
|
||||||
const defaultState = Object.freeze({})
|
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 {
|
export interface StreamsState {
|
||||||
[userId: string]: AddStreamPayload
|
[userId: string]: UserStreams
|
||||||
}
|
}
|
||||||
|
|
||||||
function addStream (
|
function addStream (
|
||||||
@ -26,40 +37,66 @@ function addStream (
|
|||||||
): StreamsState {
|
): StreamsState {
|
||||||
const { userId, stream } = payload
|
const { userId, stream } = payload
|
||||||
|
|
||||||
const userStream: AddStreamPayload = {
|
const userStreams = state[userId] || {
|
||||||
userId,
|
userId,
|
||||||
|
streams: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userStreams.streams.map(s => s.stream).indexOf(stream) >= 0) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamWithURL: StreamWithURL = {
|
||||||
stream,
|
stream,
|
||||||
|
type: payload.type,
|
||||||
url: safeCreateObjectURL(stream),
|
url: safeCreateObjectURL(stream),
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[userId]: userStream,
|
[userId]: {
|
||||||
|
userId,
|
||||||
|
streams: [...userStreams.streams, streamWithURL],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeStream (
|
function removeStream (
|
||||||
state: StreamsState, payload: RemoveStreamAction['payload'],
|
state: StreamsState, payload: RemoveStreamAction['payload'],
|
||||||
): StreamsState {
|
): StreamsState {
|
||||||
const { userId } = payload
|
const { userId, stream } = payload
|
||||||
const stream = state[userId]
|
const userStreams = state[userId]
|
||||||
if (stream && stream.stream) {
|
if (!userStreams) {
|
||||||
stream.stream.getTracks().forEach(track => track.stop())
|
return state
|
||||||
}
|
|
||||||
if (stream && stream.url) {
|
|
||||||
revokeObjectURL(stream.url)
|
|
||||||
}
|
|
||||||
return omit(state, [userId])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceStream(
|
if (stream) {
|
||||||
state: StreamsState,
|
const streams = userStreams.streams.filter(s => {
|
||||||
payload: MediaStreamPayload,
|
const found = s.stream === stream
|
||||||
): StreamsState {
|
if (found) {
|
||||||
state = removeStream(state, {
|
stream.getTracks().forEach(track => track.stop())
|
||||||
userId: payload.userId,
|
s.url && revokeObjectURL(s.url)
|
||||||
|
}
|
||||||
|
return !found
|
||||||
})
|
})
|
||||||
return addStream(state, payload)
|
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 omit(state, [userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function streams(
|
export default function streams(
|
||||||
@ -73,7 +110,7 @@ export default function streams(
|
|||||||
return removeStream(state, action.payload)
|
return removeStream(state, action.payload)
|
||||||
case MEDIA_STREAM:
|
case MEDIA_STREAM:
|
||||||
if (action.status === 'resolved') {
|
if (action.status === 'resolved') {
|
||||||
return replaceStream(state, action.payload)
|
return addStream(state, action.payload)
|
||||||
} else {
|
} else {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user