From 0534556215e1533dde47358d62e4aa9c911fa766 Mon Sep 17 00:00:00 2001 From: Olivia Pyskoty Date: Thu, 8 Apr 2021 17:54:59 -0400 Subject: [PATCH 1/5] add hook to restart audio track on device change --- .../useRestartAudioTrackOnDeviceChange.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/components/VideoProvider/useRestartAudioTrackOnDeviceChange/useRestartAudioTrackOnDeviceChange.ts diff --git a/src/components/VideoProvider/useRestartAudioTrackOnDeviceChange/useRestartAudioTrackOnDeviceChange.ts b/src/components/VideoProvider/useRestartAudioTrackOnDeviceChange/useRestartAudioTrackOnDeviceChange.ts new file mode 100644 index 000000000..c015613fa --- /dev/null +++ b/src/components/VideoProvider/useRestartAudioTrackOnDeviceChange/useRestartAudioTrackOnDeviceChange.ts @@ -0,0 +1,20 @@ +import { LocalAudioTrack, LocalVideoTrack } from 'twilio-video'; +import { useEffect } from 'react'; + +export default function useRestartAudioTrackOnDeviceChange(localTracks: (LocalAudioTrack | LocalVideoTrack)[]) { + const audioTrack = localTracks.find(track => track.kind === 'audio'); + + useEffect(() => { + const handleDeviceChange = () => { + if (audioTrack?.mediaStreamTrack.readyState === 'ended') { + audioTrack.restart(); + } + }; + + navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange); + + return () => { + navigator.mediaDevices.removeEventListener('devicechange', handleDeviceChange); + }; + }, [audioTrack]); +} From ae91929936b8cd0571212a116a4beec6932ef69d Mon Sep 17 00:00:00 2001 From: Olivia Pyskoty Date: Thu, 8 Apr 2021 17:56:29 -0400 Subject: [PATCH 2/5] add tests for useRestartAudioTrackOnDeviceChange hook --- ...seRestartAudioTrackOnDeviceChange.test.tsx | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/components/VideoProvider/useRestartAudioTrackOnDeviceChange/useRestartAudioTrackOnDeviceChange.test.tsx diff --git a/src/components/VideoProvider/useRestartAudioTrackOnDeviceChange/useRestartAudioTrackOnDeviceChange.test.tsx b/src/components/VideoProvider/useRestartAudioTrackOnDeviceChange/useRestartAudioTrackOnDeviceChange.test.tsx new file mode 100644 index 000000000..76569e08f --- /dev/null +++ b/src/components/VideoProvider/useRestartAudioTrackOnDeviceChange/useRestartAudioTrackOnDeviceChange.test.tsx @@ -0,0 +1,42 @@ +import { renderHook } from '@testing-library/react-hooks'; +import useRestartAudioTrackOnDeviceChange from './useRestartAudioTrackOnDeviceChange'; + +let mockAddEventListener = jest.fn(); +let mockRemoveEventListener = jest.fn(); + +// @ts-ignore +navigator.mediaDevices = { + addEventListener: mockAddEventListener, + removeEventListener: mockRemoveEventListener, +}; + +describe('the useHandleTrackPublicationFailed hook', () => { + afterEach(jest.clearAllMocks); + + it('should not restart the audio track if mediaStreamTrack readyState has not ended', () => { + const localTrack = [{ kind: 'audio', mediaStreamTrack: { readyState: 'live' }, restart: jest.fn() }]; + renderHook(() => useRestartAudioTrackOnDeviceChange(localTrack as any)); + + // call handleDeviceChange function: + mockAddEventListener.mock.calls[0][1](); + + expect(localTrack[0].restart).not.toHaveBeenCalled(); + }); + + it('should restart the audio track if mediaStreamTrack readyState has ended', () => { + const localTrack = [{ kind: 'audio', mediaStreamTrack: { readyState: 'ended' }, restart: jest.fn() }]; + renderHook(() => useRestartAudioTrackOnDeviceChange(localTrack as any)); + + // call handleDeviceChange function: + mockAddEventListener.mock.calls[0][1](); + + expect(localTrack[0].restart).toHaveBeenCalled(); + }); + + it('should remove the event handler when component unmounts', () => { + const { unmount } = renderHook(() => useRestartAudioTrackOnDeviceChange([])); + unmount(); + + expect(mockRemoveEventListener).toHaveBeenCalledWith('devicechange', expect.any(Function)); + }); +}); From ba4098faa9053abde2aa33a6fc2b2166bcc102b1 Mon Sep 17 00:00:00 2001 From: Olivia Pyskoty Date: Thu, 8 Apr 2021 17:57:05 -0400 Subject: [PATCH 3/5] add useRestartAudioTrackOnDeviceChange hook to VideoProvider --- src/components/VideoProvider/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/VideoProvider/index.tsx b/src/components/VideoProvider/index.tsx index ded586b29..0489ae2d1 100644 --- a/src/components/VideoProvider/index.tsx +++ b/src/components/VideoProvider/index.tsx @@ -7,6 +7,7 @@ import AttachVisibilityHandler from './AttachVisibilityHandler/AttachVisibilityH import useHandleRoomDisconnection from './useHandleRoomDisconnection/useHandleRoomDisconnection'; import useHandleTrackPublicationFailed from './useHandleTrackPublicationFailed/useHandleTrackPublicationFailed'; import useLocalTracks from './useLocalTracks/useLocalTracks'; +import useRestartAudioTrackOnDeviceChange from './useRestartAudioTrackOnDeviceChange/useRestartAudioTrackOnDeviceChange'; import useRoom from './useRoom/useRoom'; import useScreenShareToggle from './useScreenShareToggle/useScreenShareToggle'; @@ -72,6 +73,7 @@ export function VideoProvider({ options, children, onError = () => {} }: VideoPr toggleScreenShare ); useHandleTrackPublicationFailed(room, onError); + useRestartAudioTrackOnDeviceChange(localTracks); return ( Date: Thu, 8 Apr 2021 17:57:38 -0400 Subject: [PATCH 4/5] update Video Provider test to include useRestartAudioTrackOnDeviceChange hook --- src/components/VideoProvider/index.test.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/VideoProvider/index.test.tsx b/src/components/VideoProvider/index.test.tsx index 1706452df..96e78d7b6 100644 --- a/src/components/VideoProvider/index.test.tsx +++ b/src/components/VideoProvider/index.test.tsx @@ -4,6 +4,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { Room, TwilioError } from 'twilio-video'; import { VideoProvider } from './index'; import useLocalTracks from './useLocalTracks/useLocalTracks'; +import useRestartAudioTrackOnDeviceChange from './useRestartAudioTrackOnDeviceChange/useRestartAudioTrackOnDeviceChange'; import useRoom from './useRoom/useRoom'; import useHandleRoomDisconnection from './useHandleRoomDisconnection/useHandleRoomDisconnection'; import useHandleTrackPublicationFailed from './useHandleTrackPublicationFailed/useHandleTrackPublicationFailed'; @@ -23,6 +24,7 @@ jest.mock('./useLocalTracks/useLocalTracks', () => ); jest.mock('./useHandleRoomDisconnection/useHandleRoomDisconnection'); jest.mock('./useHandleTrackPublicationFailed/useHandleTrackPublicationFailed'); +jest.mock('./useRestartAudioTrackOnDeviceChange/useRestartAudioTrackOnDeviceChange'); describe('the VideoProvider component', () => { it('should correctly return the Video Context object', () => { @@ -57,6 +59,7 @@ describe('the VideoProvider component', () => { expect.any(Function) ); expect(useHandleTrackPublicationFailed).toHaveBeenCalledWith(mockRoom, expect.any(Function)); + expect(useRestartAudioTrackOnDeviceChange).toHaveBeenCalledWith(result.current.localTracks); }); it('should call the onError function when there is an error', () => { From 42493324e8e2d4c6164252db885a8a8021ff8c50 Mon Sep 17 00:00:00 2001 From: Olivia Pyskoty Date: Fri, 9 Apr 2021 11:21:31 -0400 Subject: [PATCH 5/5] add comment to useRestartAudioTrackOnDeviceChange hook --- .../useRestartAudioTrackOnDeviceChange.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/VideoProvider/useRestartAudioTrackOnDeviceChange/useRestartAudioTrackOnDeviceChange.ts b/src/components/VideoProvider/useRestartAudioTrackOnDeviceChange/useRestartAudioTrackOnDeviceChange.ts index c015613fa..b9a60de71 100644 --- a/src/components/VideoProvider/useRestartAudioTrackOnDeviceChange/useRestartAudioTrackOnDeviceChange.ts +++ b/src/components/VideoProvider/useRestartAudioTrackOnDeviceChange/useRestartAudioTrackOnDeviceChange.ts @@ -1,6 +1,15 @@ import { LocalAudioTrack, LocalVideoTrack } from 'twilio-video'; import { useEffect } from 'react'; +/* + * If a user has published an audio track from an external audio input device and + * disconnects the device, the published audio track will be stopped and the user + * will no longer be heard by other participants. + * + * To prevent this issue, this hook will re-acquire a mediaStreamTrack from the system's + * default audio device when it detects that the published audio device has been disconnected. + */ + export default function useRestartAudioTrackOnDeviceChange(localTracks: (LocalAudioTrack | LocalVideoTrack)[]) { const audioTrack = localTracks.find(track => track.kind === 'audio');