Compare commits

...
Author SHA1 Message Date
Timo K 2f565fb8e3 maybe helpful logging
Signed-off-by: Timo K <toger5@hotmail.de>
2025-06-13 13:03:25 +02:00
Timo K 98bfab47dc selectedId == PreferredId for controlled output
This skips the usage of the useSelectedId hook that only really makes sense for non controlled audio.

Signed-off-by: Timo K <toger5@hotmail.de>
2025-06-12 13:51:39 +02:00
Timo K 84ff12e4de better hooks for media context
Signed-off-by: Timo K <toger5@hotmail.de>
2025-06-12 13:51:39 +02:00
4 changed files with 72 additions and 37 deletions
+16 -4
View File
@@ -6,6 +6,9 @@ Please see LICENSE in the repository root for full details.
*/
import { BehaviorSubject, Subject } from "rxjs";
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
const logger = rootLogger.getChild("[controlled-media]");
export interface Controls {
canEnterPip(): boolean;
@@ -44,10 +47,14 @@ export interface OutputDevice {
export const setPipEnabled$ = new Subject<boolean>();
// BehaviorSubject since the client might set this before we have subscribed (GroupCallView still in "loading" state)
// We want the devices that have been set during loading to be available immediately once loaded.
export const availableOutputDevices$ = new BehaviorSubject<OutputDevice[]>([]);
export const controlledAvailableOutputDevices$ = new BehaviorSubject<
OutputDevice[]
>([]);
// BehaviorSubject since the client might set this before we have subscribed (GroupCallView still in "loading" state)
// We want the device that has been set during loading to be available immediately once loaded.
export const outputDevice$ = new BehaviorSubject<string | undefined>(undefined);
export const controlledAudioDevice$ = new BehaviorSubject<string | undefined>(
undefined,
);
/**
* This allows the os to mute the call if the user
* presses the volume down button when it is at the minimum volume.
@@ -75,12 +82,17 @@ window.controls = {
setPipEnabled$.next(false);
},
setAvailableAudioDevices(devices: OutputDevice[]): void {
availableOutputDevices$.next(devices);
logger.info("setAvailableAudioDevices called from native:", devices);
controlledAvailableOutputDevices$.next(devices);
},
setAudioDevice(id: string): void {
outputDevice$.next(id);
logger.info("setAudioDevice called from native", id);
controlledAudioDevice$.next(id);
},
setAudioEnabled(enabled: boolean): void {
logger.info("setAudioEnabled called from native:", enabled);
if (!setAudioEnabled$.observed)
throw new Error(
"Output controls are disabled. No setAudioEnabled$ observer",
+37 -26
View File
@@ -18,7 +18,11 @@ import {
} from "react";
import { createMediaDeviceObserver } from "@livekit/components-core";
import { combineLatest, distinctUntilChanged, map, startWith } from "rxjs";
import { useObservable, useObservableEagerState } from "observable-hooks";
import {
useObservable,
useObservableEagerState,
useSubscription,
} from "observable-hooks";
import { logger } from "matrix-js-sdk/lib/logger";
import { deepCompare } from "matrix-js-sdk/lib/utils";
@@ -30,7 +34,10 @@ import {
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
type Setting,
} from "../settings/settings";
import { outputDevice$, availableOutputDevices$ } from "../controls";
import {
controlledAudioDevice$,
controlledAvailableOutputDevices$,
} from "../controls";
import { useUrlParams } from "../UrlParams";
// This hardcoded id is used in EX ios! It can only be changed in coordination with
@@ -135,7 +142,7 @@ function useMediaDeviceHandle(
// useMediaDevices provides no way to request device names.
// Tragically, the only way to get device names out of LiveKit is to specify a
// kind, which then results in multiple permissions requests.
const deviceObserver$ = useMemo(
const deviceObserver$ = useObservable(
() =>
createMediaDeviceObserver(
kind,
@@ -153,8 +160,9 @@ function useMediaDeviceHandle(
),
[kind, requestPermissions],
);
const available = useObservableEagerState(
useMemo(
useObservable(
() =>
deviceObserver$.pipe(
map((availableRaw) => {
@@ -197,7 +205,7 @@ function useMediaDeviceHandle(
const selectedId = useSelectedId(available, preferredId);
const selectedGroupId = useObservableEagerState(
useMemo(
useObservable(
() =>
deviceObserver$.pipe(
map(
@@ -326,9 +334,9 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
function useControlledOutput(): MediaDeviceHandle {
const { available } = useObservableEagerState(
useObservable(() => {
const outputDeviceData$ = availableOutputDevices$.pipe(
const outputDeviceData$ = controlledAvailableOutputDevices$.pipe(
map((devices) => {
const deviceForEarpiece = devices.find((d) => d.forEarpiece);
const hasDeviceForEarpiece = devices.find((d) => d.forEarpiece);
const deviceMapTuple: [string, DeviceLabel][] = devices.map(
({ id, name, isEarpiece, isSpeaker /*,isExternalHeadset*/ }) => {
let deviceLabel: DeviceLabel = { type: "name", name };
@@ -340,35 +348,34 @@ function useControlledOutput(): MediaDeviceHandle {
);
return {
devicesMap: new Map<string, DeviceLabel>(deviceMapTuple),
deviceForEarpiece,
hasDeviceForEarpiece,
};
}),
);
return combineLatest(
[outputDeviceData$, iosDeviceMenu$],
({ devicesMap, deviceForEarpiece }, iosShowEarpiece) => {
({ devicesMap, hasDeviceForEarpiece }, iosShowEarpiece) => {
let available = devicesMap;
if (iosShowEarpiece && !!deviceForEarpiece) {
if (iosShowEarpiece && !!hasDeviceForEarpiece) {
available = new Map([
...devicesMap.entries(),
[EARPIECE_CONFIG_ID, { type: "earpiece" }],
]);
}
return { available, deviceForEarpiece };
return { available, deviceForEarpiece: hasDeviceForEarpiece };
},
);
}),
);
const [preferredId, setPreferredId] = useSetting(audioOutputSetting);
useEffect(() => {
const subscription = outputDevice$.subscribe((id) => {
if (id) setPreferredId(id);
});
return (): void => subscription.unsubscribe();
}, [setPreferredId]);
const selectedId = useSelectedId(available, preferredId);
const [preferredId, setPreferredId] = useSetting(audioOutputSetting);
useSubscription(controlledAudioDevice$, (id) => {
if (id) setPreferredId(id);
});
// const selectedId = useSelectedId(available, preferredId);
const [asEarpiece, setAsEarpiece] = useState(false);
@@ -377,23 +384,27 @@ function useControlledOutput(): MediaDeviceHandle {
// This information is probably only of interest if the earpiece mode has been
// selected - for example, Element X iOS listens to this to determine whether it
// should enable the proximity sensor.
if (selectedId) {
window.controls.onAudioDeviceSelect?.(selectedId);
if (preferredId) {
logger.info(
"[controlled-media] setAudioDeviceSelect called:",
preferredId,
);
window.controls.onAudioDeviceSelect?.(preferredId);
// Call deprecated method for backwards compatibility.
window.controls.onOutputDeviceSelect?.(selectedId);
window.controls.onOutputDeviceSelect?.(preferredId);
}
setAsEarpiece(selectedId === EARPIECE_CONFIG_ID);
}, [selectedId]);
setAsEarpiece(preferredId === EARPIECE_CONFIG_ID);
}, [preferredId]);
return useMemo(
() => ({
available: available,
selectedId,
selectedId: preferredId,
selectedGroupId: undefined,
select: setPreferredId,
useAsEarpiece: asEarpiece,
}),
[available, selectedId, setPreferredId, asEarpiece],
[available, preferredId, setPreferredId, asEarpiece],
);
}
+8 -5
View File
@@ -73,8 +73,6 @@ export function useLivekit(
// Only ever create the room once via useInitial.
const room = useInitial(() => {
logger.info("[LivekitRoom] Create LiveKit room");
let e2ee: E2EEManagerOptions | undefined;
if (e2eeSystem.kind === E2eeType.PER_PARTICIPANT) {
logger.info("Created MatrixKeyProvider (per participant)");
@@ -99,13 +97,18 @@ export function useLivekit(
},
audioCaptureDefaults: {
...defaultLiveKitOptions.audioCaptureDefaults,
deviceId: initialDevices.audioInput.selectedId,
deviceId: controlledAudioDevices
? undefined
: initialDevices.audioInput.selectedId,
},
audioOutput: {
deviceId: initialDevices.audioOutput.selectedId,
deviceId: controlledAudioDevices
? undefined
: initialDevices.audioOutput.selectedId,
},
e2ee,
};
logger.info("[LivekitRoom] Create LiveKit room with options", roomOptions);
// We have to create the room manually here due to a bug inside
// @livekit/components-react. JSON.stringify() is used in deps of a
// useEffect() with an argument that references itself, if E2EE is enabled
@@ -157,7 +160,7 @@ export function useLivekit(
);
const connectionState = useECConnectionState(
initialDevices.audioInput.selectedId,
controlledAudioDevices ? undefined : initialDevices.audioInput.selectedId,
initialMuteStates.audio.enabled,
room,
sfuConfig,
+11 -2
View File
@@ -54,6 +54,7 @@ import {
} from "../livekit/TrackProcessorContext";
import { usePageTitle } from "../usePageTitle";
import { useLatest } from "../useLatest";
import { useUrlParams } from "../UrlParams";
interface Props {
client: MatrixClient;
@@ -99,6 +100,7 @@ export const LobbyView: FC<Props> = ({
[muteStates],
);
const { controlledAudioDevices } = useUrlParams();
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const [settingsTab, setSettingsTab] = useState(defaultSettingsTab);
@@ -132,7 +134,11 @@ export const LobbyView: FC<Props> = ({
// re-open the devices when they change (see below).
const initialAudioOptions = useInitial(
() =>
muteStates.audio.enabled && { deviceId: devices.audioInput.selectedId },
muteStates.audio.enabled && {
deviceId: controlledAudioDevices
? undefined
: devices.audioInput.selectedId,
},
);
const { processor } = useTrackProcessor();
@@ -148,13 +154,16 @@ export const LobbyView: FC<Props> = ({
// which would cause the devices to be re-opened on the next render.
audio: Object.assign({}, initialAudioOptions),
video: muteStates.video.enabled && {
deviceId: devices.videoInput.selectedId,
deviceId: controlledAudioDevices
? undefined
: devices.videoInput.selectedId,
processor: initialProcessor,
},
}),
[
initialAudioOptions,
muteStates.video.enabled,
controlledAudioDevices,
devices.videoInput.selectedId,
initialProcessor,
],