Skip to content

Commit

Permalink
Reconnect after a restart mid-call (#221)
Browse files Browse the repository at this point in the history
Closes #220.
  • Loading branch information
jeremija committed May 17, 2021
1 parent 90a37a7 commit 0a9ac6a
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 16 deletions.
95 changes: 86 additions & 9 deletions src/client/actions/CallActions.test.ts
Expand Up @@ -7,9 +7,10 @@ import * as SocketActions from './SocketActions'
import * as constants from '../constants'
import socket from '../socket'
import { bindActionCreators, createStore, AnyAction, combineReducers, applyMiddleware } from 'redux'
import reducers from '../reducers'
import { middlewares } from '../middlewares'

import { Nicknames } from '../reducers/nicknames'

jest.useFakeTimers()

describe('CallActions', () => {
Expand All @@ -20,20 +21,49 @@ describe('CallActions', () => {
return [...state, action]
}

let nicknamesState: Nicknames = {
[constants.ME]: 'local-user',
}

let mediaState: {
dialState: constants.DialState
} = {
dialState: constants.DIAL_STATE_HUNG_UP,
}

afterEach(() => {
nicknamesState = {
[constants.ME]: 'local-user',
}

mediaState = {
dialState: constants.DIAL_STATE_HUNG_UP,
}
})

function nicknames() {
return nicknamesState
}

function media() {
return mediaState
}

const configureStore = () => createStore(
combineReducers({...reducers, allActions }),
combineReducers({ media, nicknames, allActions }),
applyMiddleware(...middlewares),
)
let store: ReturnType<typeof configureStore>

beforeEach(() => {
store = createStore(
combineReducers({ allActions }),
applyMiddleware(...middlewares),
)
type Store = ReturnType<typeof configureStore>

let store: Store

const setup = () =>{
store = configureStore()

callActions = bindActionCreators(CallActions, store.dispatch);
(SocketActions.handshake as jest.Mock).mockReturnValue(jest.fn())
})
}

afterEach(() => {
jest.runAllTimers()
Expand All @@ -43,6 +73,8 @@ describe('CallActions', () => {
describe('init', () => {

it('dispatches init action when connected', async () => {
setup()

const promise = callActions.init()
socket.emit('connect', undefined)
await promise
Expand All @@ -59,6 +91,8 @@ describe('CallActions', () => {
})

it('calls dispatches disconnect message on disconnect', async () => {
setup()

const promise = callActions.init()
socket.emit('connect', undefined)
socket.emit('disconnect', undefined)
Expand All @@ -85,11 +119,54 @@ describe('CallActions', () => {
})

it('dispatches alert when failed to get media stream', async () => {
setup()

const promise = callActions.init()
socket.emit('connect', undefined)
await promise
})

describe('connect after in-call (server restart)', () => {
beforeEach(() => {
mediaState = {
dialState: constants.DIAL_STATE_IN_CALL,
}

setup()
})

it('destroys all peers and initiates peer reconnection', async() => {
const promise = callActions.init()

socket.emit('connect', undefined)

await promise

expect(store.getState().allActions.slice(1)).toEqual([{
payload: {
id: jasmine.any(String),
message: 'Connected to server socket',
type: 'warning',
},
type: constants.NOTIFY,
}, {
type: constants.SOCKET_CONNECTED,
}, {
type: constants.PEER_REMOVE_ALL,
}, {
payload: {
id: jasmine.any(String),
message: 'Reconnecting to peer(s)...',
type: 'info',
},
type: constants.NOTIFY,
}, {
status: 'pending',
type: constants.DIAL,
}])
})
})

})

})
33 changes: 31 additions & 2 deletions src/client/actions/CallActions.ts
@@ -1,9 +1,10 @@
import { GetAsyncAction, makeAction } from '../async'
import { DIAL, HANG_UP, SOCKET_CONNECTED, SOCKET_DISCONNECTED, SOCKET_EVENT_HANG_UP, SOCKET_EVENT_USERS } from '../constants'
import { DIAL, HANG_UP, ME, SOCKET_CONNECTED, SOCKET_DISCONNECTED, SOCKET_EVENT_HANG_UP, SOCKET_EVENT_USERS } from '../constants'
import socket from '../socket'
import store, { ThunkResult } from '../store'
import { config } from '../window'
import * as NotifyActions from './NotifyActions'
import { removeAllPeers } from './PeerActions'
import * as SocketActions from './SocketActions'

const { callId, peerId } = config
Expand All @@ -24,11 +25,39 @@ const disconnected = (): DisconnectedAction => ({
type: SOCKET_DISCONNECTED,
})

export const init = (): ThunkResult<Promise<void>> => async dispatch => {
export const init = (): ThunkResult<Promise<void>> => async (
dispatch, getState,
) => {
return new Promise(resolve => {
socket.on('connect', () => {
dispatch(NotifyActions.warning('Connected to server socket'))
dispatch(connected())

const state = getState()
const nickname = state.nicknames[ME]

// Redial if the previous state was in-call, for example if the server
// was restarted and websocket connection lost.
if (state.media.dialState === 'in-call') {
// Destroy all peers so we can start anew. It usually takes some time
// for the peer connections to realize it has been disconnected so we
// destroy all peers first so we can have a clean state. But we don't
// want to call hangUp because that would remove the a/v streams.
dispatch(removeAllPeers())

dispatch(NotifyActions.info('Reconnecting to peer(s)...'))

// restarted mid-call.
dispatch(
dial({
nickname,
}),
)
.catch(() => {
dispatch(NotifyActions.error('Dial timed out.'))
})
}

resolve()
})
socket.on('disconnect', () => {
Expand Down
11 changes: 10 additions & 1 deletion src/client/actions/PeerActions.ts
Expand Up @@ -221,11 +221,20 @@ export interface RemovePeerAction {
payload: { peerId: string }
}

export interface RemoveAllPeersAction {
type: 'PEER_REMOVE_ALL'
}

export const removePeer = (peerId: string): RemovePeerAction => ({
type: constants.PEER_REMOVE,
payload: { peerId },
})

export const removeAllPeers = (): RemoveAllPeersAction => ({
type: constants.PEER_REMOVE_ALL,
})

export type PeerAction =
AddPeerAction |
RemovePeerAction
RemovePeerAction |
RemoveAllPeersAction
1 change: 1 addition & 0 deletions src/client/constants.ts
Expand Up @@ -40,6 +40,7 @@ export const NICKNAME_REMOVE = 'NICKNAME_REMOVE'

export const PEER_ADD = 'PEER_ADD'
export const PEER_REMOVE = 'PEER_REMOVE'
export const PEER_REMOVE_ALL = 'PEER_REMOVE_ALL'

// this data channel must have the same name as the one on the server-side,
// when SFU is used.
Expand Down
14 changes: 10 additions & 4 deletions src/client/reducers/peers.ts
Expand Up @@ -149,6 +149,12 @@ export function handleLocalMediaTrackEnable(
return state
}

export function removeAllPeers(state: PeersState): PeersState {
forEach(state, peer => peer.destroy())

return defaultState
}

export default function peers(
state = defaultState,
action:
Expand All @@ -172,10 +178,10 @@ export default function peers(
camera: undefined,
desktop: undefined,
}
setTimeout(() => {
forEach(state, peer => peer.destroy())
})
return defaultState

return removeAllPeers(state)
case constants.PEER_REMOVE_ALL:
return removeAllPeers(state)
case constants.STREAM_REMOVE:
return handleRemoveLocalStream(state, action)
case constants.MEDIA_STREAM:
Expand Down

0 comments on commit 0a9ac6a

Please sign in to comment.