Skip to content

Commit f69c753

Browse files
committed
add earpice mode
1 parent f9b04ae commit f69c753

File tree

3 files changed

+55
-16
lines changed

3 files changed

+55
-16
lines changed

docs/controls.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ A few aspects of Element Call's interface can be controlled through a global API
1212

1313
These functions must be used in conjunction with the `controlledOutput` URL parameter in order to have any effect.
1414

15-
- `controls.setOutputDevices(devices: { id: string, name: string }[]): void` Sets the list of available audio outputs.
15+
- `controls.setOutputDevices(devices: { id: string, name: string, forEarpiece?: boolean }[]): void` Sets the list of available audio outputs. `forEarpiece` is used on ios only.
16+
It flags the device that should be used if the user selects earpice mode. This should be the main (stereo loudspeaker) of the device.
1617
- `controls.onOutputDeviceSelect: ((id: string) => void) | undefined` Callback called whenever the user or application selects a new audio output.
1718
- `controls.setOutputEnabled(enabled: boolean)` Enables/disables all audio output from the application. This can be useful for temporarily pausing audio while the controlling application is switching output devices. Output is enabled by default.

src/controls.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface Controls {
1919
export interface OutputDevice {
2020
id: string;
2121
name: string;
22+
forEarpiece?: boolean;
2223
}
2324

2425
export const setPipEnabled$ = new Subject<boolean>();

src/livekit/MediaDevicesContext.tsx

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ interface InputDevices {
7171
export interface MediaDevices extends Omit<InputDevices, "usingNames"> {
7272
audioOutput: MediaDevice;
7373
}
74+
function useShowEarpiece(): boolean {
75+
const [alwaysShowIphoneEarpice] = useSetting(alwaysShowIphoneEarpieceSetting);
76+
const m = useMemo(
77+
() =>
78+
(navigator.userAgent.match("iPhone")?.length ?? 0) > 0 ||
79+
alwaysShowIphoneEarpice,
80+
[alwaysShowIphoneEarpice],
81+
);
82+
return m;
83+
}
7484

7585
function useMediaDevice(
7686
kind: MediaDeviceKind,
@@ -79,7 +89,7 @@ function useMediaDevice(
7989
): MediaDevice {
8090
// Make sure we don't needlessly reset to a device observer without names,
8191
// once permissions are already given
82-
const [alwaysShowIphoneEarpice] = useSetting(alwaysShowIphoneEarpieceSetting);
92+
const showEarpiece = useShowEarpiece();
8393
const hasRequestedPermissions = useRef(false);
8494
const requestPermissions = usingNames || hasRequestedPermissions.current;
8595
hasRequestedPermissions.current ||= usingNames;
@@ -119,8 +129,6 @@ function useMediaDevice(
119129
// recognizes.
120130
// We also create this if we do not have any available devices, so that
121131
// we can use the default or the earpiece.
122-
const showEarpiece =
123-
navigator.userAgent.match("iPhone") || alwaysShowIphoneEarpice;
124132
if (
125133
kind === "audiooutput" &&
126134
!available.has("") &&
@@ -144,7 +152,7 @@ function useMediaDevice(
144152
return available;
145153
}),
146154
),
147-
[alwaysShowIphoneEarpice, deviceObserver$, kind],
155+
[deviceObserver$, kind, showEarpiece],
148156
),
149157
);
150158

@@ -298,20 +306,31 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
298306
};
299307

300308
function useControlledOutput(): MediaDevice {
309+
const showEarpiece = useShowEarpiece();
310+
301311
const available = useObservableEagerState(
302312
useObservable(() =>
303313
setOutputDevices$.pipe(
304314
startWith<OutputDevice[]>([]),
305-
map(
306-
(devices) =>
307-
new Map<string, DeviceLabel>(
308-
devices.map(({ id, name }) => [id, { type: "name", name }]),
309-
),
310-
),
315+
map((devices) => {
316+
const devicesMap = new Map<string, DeviceLabel>(
317+
devices.map(({ id, name }) => [id, { type: "name", name }]),
318+
);
319+
if (showEarpiece)
320+
devicesMap.set(EARPIECE_CONFIG_ID, { type: "earpiece" });
321+
return devicesMap;
322+
}),
311323
),
312324
),
313325
);
314-
const [preferredId, select] = useSetting(audioOutputSetting);
326+
const earpiceDevice = useObservableEagerState(
327+
setOutputDevices$.pipe(
328+
map((devices) => devices.find((d) => d.forEarpiece)),
329+
),
330+
);
331+
332+
const [preferredId, setPreferredId] = useSetting(audioOutputSetting);
333+
315334
const selectedId = useMemo(() => {
316335
if (available.size) {
317336
// If the preferred device is available, use it. Or if every available
@@ -327,19 +346,37 @@ function useControlledOutput(): MediaDevice {
327346
}
328347
return undefined;
329348
}, [available, preferredId]);
349+
330350
useEffect(() => {
331-
if (selectedId !== undefined)
332-
window.controls.onOutputDeviceSelect?.(selectedId);
351+
if (selectedId === EARPIECE_CONFIG_ID)
352+
if (selectedId !== undefined)
353+
window.controls.onOutputDeviceSelect?.(selectedId);
333354
}, [selectedId]);
334355

356+
const [asEarpice, setAsEarpiece] = useState(false);
357+
358+
const select = useCallback(
359+
(id: string) => {
360+
if (id === EARPIECE_CONFIG_ID) {
361+
setAsEarpiece(true);
362+
if (earpiceDevice) setPreferredId(earpiceDevice.id);
363+
} else {
364+
setAsEarpiece(false);
365+
setPreferredId(id);
366+
}
367+
},
368+
[earpiceDevice, setPreferredId],
369+
);
370+
335371
return useMemo(
336372
() => ({
337-
available,
373+
available: available,
338374
selectedId,
339375
selectedGroupId: undefined,
340376
select,
377+
useAsEarpiece: asEarpice,
341378
}),
342-
[available, selectedId, select],
379+
[available, selectedId, select, asEarpice],
343380
);
344381
}
345382

0 commit comments

Comments
 (0)