From fac6b2a4c46dc2b89c62cfd03f6027edfcc1aaf6 Mon Sep 17 00:00:00 2001 From: Thomas Frivold Date: Mon, 8 Jan 2024 15:23:50 +0100 Subject: [PATCH] Move onDeviceChange event listener to local media slice --- src/lib/core/redux/slices/localMedia.ts | 28 ++++++++++++------- .../core/redux/tests/store/localMedia.spec.ts | 7 ++++- src/lib/react/useLocalMedia/index.ts | 2 -- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/lib/core/redux/slices/localMedia.ts b/src/lib/core/redux/slices/localMedia.ts index ba599de9..ad67fef7 100644 --- a/src/lib/core/redux/slices/localMedia.ts +++ b/src/lib/core/redux/slices/localMedia.ts @@ -4,7 +4,7 @@ import { createAppAsyncThunk, createAppThunk } from "../../redux/thunk"; import { RootState } from "../../redux/store"; import { createReactor, startAppListening } from "../../redux/listenerMiddleware"; import { doAppJoin, selectAppWantsToJoin } from "./app"; -import debounce from "~/lib/utils/debounce"; +import debounce from "../../../utils/debounce"; export type LocalMediaOptions = { audio: boolean; @@ -143,7 +143,7 @@ export const localMediaSlice = createSlice({ status: "starting", }; }) - .addCase(doStartLocalMedia.fulfilled, (state, { payload: { stream } }) => { + .addCase(doStartLocalMedia.fulfilled, (state, { payload: { stream, onDeviceChange } }) => { let cameraDeviceId = undefined; let cameraEnabled = false; let microphoneDeviceId = undefined; @@ -170,6 +170,7 @@ export const localMediaSlice = createSlice({ currentMicrophoneDeviceId: microphoneDeviceId, cameraEnabled, microphoneEnabled, + onDeviceChange, }; }) .addCase(doStartLocalMedia.rejected, (state, action) => { @@ -448,11 +449,6 @@ export const doSwitchLocalStream = createAppAsyncThunk( export const doStartLocalMedia = createAppAsyncThunk( "localMedia/doStartLocalMedia", async (payload: LocalMediaOptions | MediaStream, { getState, dispatch, rejectWithValue }) => { - // Resolve if existing stream is passed - if ("getTracks" in payload) { - return Promise.resolve({ stream: payload }); - } - const onDeviceChange = debounce( () => { dispatch(doUpdateDeviceList()); @@ -460,10 +456,17 @@ export const doStartLocalMedia = createAppAsyncThunk( { delay: 500 } ); - global.navigator.mediaDevices && global.navigator.mediaDevices.addEventListener("devicechange", onDeviceChange); + if (global.navigator.mediaDevices) { + global.navigator.mediaDevices.addEventListener("devicechange", onDeviceChange); + } + + // Resolve if existing stream is passed + if ("getTracks" in payload) { + return Promise.resolve({ stream: payload, onDeviceChange }); + } if (!(payload.audio || payload.video)) { - return { stream: new MediaStream() }; + return { stream: new MediaStream(), onDeviceChange }; } else { dispatch(doSetLocalMediaOptions({ options: payload })); } @@ -482,7 +485,7 @@ export const doStartLocalMedia = createAppAsyncThunk( videoId: payload.video, }); - return { stream }; + return { stream, onDeviceChange }; } catch (error) { return rejectWithValue(error); } @@ -492,6 +495,7 @@ export const doStartLocalMedia = createAppAsyncThunk( export const doStopLocalMedia = createAppThunk(() => (dispatch, getState) => { const screenshareStream = selectScreenshareStream(getState()); const stream = selectLocalMediaStream(getState()); + const onDeviceChange = selectLocalMediaRaw(getState()).onDeviceChange; screenshareStream?.getTracks().forEach((track) => { track.stop(); @@ -501,6 +505,10 @@ export const doStopLocalMedia = createAppThunk(() => (dispatch, getState) => { track.stop(); }); + if (onDeviceChange && global.navigator.mediaDevices) { + global.navigator.mediaDevices.removeEventListener("devicechange", onDeviceChange); + } + dispatch(localMediaStopped()); }); diff --git a/src/lib/core/redux/tests/store/localMedia.spec.ts b/src/lib/core/redux/tests/store/localMedia.spec.ts index a173164c..9aa5b0c0 100644 --- a/src/lib/core/redux/tests/store/localMedia.spec.ts +++ b/src/lib/core/redux/tests/store/localMedia.spec.ts @@ -56,7 +56,11 @@ describe("actions", () => { const after = store.getState().localMedia; - expect(diff(before, after)).toEqual({ status: "started", stream: existingStream }); + expect(diff(before, after)).toEqual({ + status: "started", + stream: existingStream, + onDeviceChange: expect.any(Function), + }); }); }); @@ -91,6 +95,7 @@ describe("actions", () => { stream: newStream, devices: expect.any(Object), options: { audio: true, video: true }, + onDeviceChange: expect.any(Function), }); }); }); diff --git a/src/lib/react/useLocalMedia/index.ts b/src/lib/react/useLocalMedia/index.ts index a4beeba4..d81c9172 100644 --- a/src/lib/react/useLocalMedia/index.ts +++ b/src/lib/react/useLocalMedia/index.ts @@ -6,13 +6,11 @@ import { doStopLocalMedia, doToggleCameraEnabled, doToggleMicrophoneEnabled, - doUpdateDeviceList, } from "../../core/redux/slices/localMedia"; import { LocalMediaState, UseLocalMediaOptions, UseLocalMediaResult } from "./types"; import { selectLocalMediaState } from "./selector"; import { createStore, observeStore, Store } from "../../core/redux/store"; import { createServices } from "../../services"; -import debounce from "../../utils/debounce"; const initialState: LocalMediaState = { cameraDeviceError: null,