Only rerequest permissions if we do not yet get labels when enumerating

This commit is contained in:
Timo
2025-06-20 18:32:51 +02:00
committed by Robin
parent 8f841dfb59
commit ab4eadf58f
2 changed files with 66 additions and 23 deletions

View File

@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
import { import {
combineLatest, combineLatest,
filter, filter,
identity,
map, map,
merge, merge,
of, of,
@@ -18,7 +19,7 @@ import {
type Observable, type Observable,
} from "rxjs"; } from "rxjs";
import { createMediaDeviceObserver } from "@livekit/components-core"; import { createMediaDeviceObserver } from "@livekit/components-core";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
import { import {
audioInput as audioInputSetting, audioInput as audioInputSetting,
@@ -37,6 +38,7 @@ import { platform } from "../Platform";
// This hardcoded id is used in EX ios! It can only be changed in coordination with // This hardcoded id is used in EX ios! It can only be changed in coordination with
// the ios swift team. // the ios swift team.
const EARPIECE_CONFIG_ID = "earpiece-id"; const EARPIECE_CONFIG_ID = "earpiece-id";
const logger = rootLogger.getChild("[MediaDevices]");
export type DeviceLabel = export type DeviceLabel =
| { type: "name"; name: string } | { type: "name"; name: string }
@@ -93,15 +95,15 @@ export const iosDeviceMenu$ =
function availableRawDevices$( function availableRawDevices$(
kind: MediaDeviceKind, kind: MediaDeviceKind,
usingNames$: Observable<boolean>, recomputeDevicesWithPermissions$: Observable<boolean>,
scope: ObservableScope, scope: ObservableScope,
): Observable<MediaDeviceInfo[]> { ): Observable<MediaDeviceInfo[]> {
return usingNames$.pipe( return recomputeDevicesWithPermissions$.pipe(
switchMap((usingNames) => switchMap((recomputeDevicesWithPermissions) =>
createMediaDeviceObserver( createMediaDeviceObserver(
kind, kind,
(e) => logger.error("Error creating MediaDeviceObserver", e), (e) => logger.error("Error creating MediaDeviceObserver", e),
usingNames, recomputeDevicesWithPermissions,
), ),
), ),
startWith([]), startWith([]),
@@ -145,7 +147,11 @@ function selectDevice$<Label>(
class AudioInput implements MediaDevice<DeviceLabel, SelectedAudioInputDevice> { class AudioInput implements MediaDevice<DeviceLabel, SelectedAudioInputDevice> {
private readonly availableRaw$: Observable<MediaDeviceInfo[]> = private readonly availableRaw$: Observable<MediaDeviceInfo[]> =
availableRawDevices$("audioinput", this.usingNames$, this.scope); availableRawDevices$(
"audioinput",
this.recomputeDevicesWithPermissions$,
this.scope,
);
public readonly available$ = this.availableRaw$.pipe( public readonly available$ = this.availableRaw$.pipe(
map(buildDeviceMap), map(buildDeviceMap),
@@ -179,9 +185,13 @@ class AudioInput implements MediaDevice<DeviceLabel, SelectedAudioInputDevice> {
} }
public constructor( public constructor(
private readonly usingNames$: Observable<boolean>, private readonly recomputeDevicesWithPermissions$: Observable<boolean>,
private readonly scope: ObservableScope, private readonly scope: ObservableScope,
) {} ) {
this.available$.subscribe((available) => {
logger.info("[audio-input] available devices:", available);
});
}
} }
class AudioOutput class AudioOutput
@@ -189,7 +199,7 @@ class AudioOutput
{ {
public readonly available$ = availableRawDevices$( public readonly available$ = availableRawDevices$(
"audiooutput", "audiooutput",
this.usingNames$, this.recomputeDevicesWithPermissions$,
this.scope, this.scope,
).pipe( ).pipe(
map((availableRaw) => { map((availableRaw) => {
@@ -230,9 +240,13 @@ class AudioOutput
} }
public constructor( public constructor(
private readonly usingNames$: Observable<boolean>, private readonly recomputeDevicesWithPermissions$: Observable<boolean>,
private readonly scope: ObservableScope, private readonly scope: ObservableScope,
) {} ) {
this.available$.subscribe((available) => {
logger.info("[audio-output] available devices:", available);
});
}
} }
class ControlledAudioOutput class ControlledAudioOutput
@@ -298,13 +312,16 @@ class ControlledAudioOutput
window.controls.onOutputDeviceSelect?.(device.id); window.controls.onOutputDeviceSelect?.(device.id);
} }
}); });
this.available$.subscribe((available) => {
logger.info("[controlled-output] available devices:", available);
});
} }
} }
class VideoInput implements MediaDevice<DeviceLabel, SelectedDevice> { class VideoInput implements MediaDevice<DeviceLabel, SelectedDevice> {
public readonly available$ = availableRawDevices$( public readonly available$ = availableRawDevices$(
"videoinput", "videoinput",
this.usingNames$, this.recomputeDevicesWithPermissions$,
this.scope, this.scope,
).pipe(map(buildDeviceMap)); ).pipe(map(buildDeviceMap));
@@ -321,13 +338,18 @@ class VideoInput implements MediaDevice<DeviceLabel, SelectedDevice> {
} }
public constructor( public constructor(
private readonly usingNames$: Observable<boolean>, private readonly recomputeDevicesWithPermissions$: Observable<boolean>,
private readonly scope: ObservableScope, private readonly scope: ObservableScope,
) {} ) {
// This also has the purpose of subscribing to the available devices
this.available$.subscribe((available) => {
logger.info("[video-input] available devices:", available);
});
}
} }
export class MediaDevices { export class MediaDevices {
private readonly deviceNamesRequest$ = new Subject<void>(); private readonly requests$ = new Subject<boolean>();
/** /**
* Requests that the media devices be populated with the names of each * Requests that the media devices be populated with the names of each
* available device, rather than numbered identifiers. This may invoke a * available device, rather than numbered identifiers. This may invoke a
@@ -335,7 +357,9 @@ export class MediaDevices {
* intent to view the device list. * intent to view the device list.
*/ */
public requestDeviceNames(): void { public requestDeviceNames(): void {
this.deviceNamesRequest$.next(); void navigator.mediaDevices.enumerateDevices().then((result) => {
this.requests$.next(!result.some((device) => device.label));
});
} }
// Start using device names as soon as requested. This will cause LiveKit to // Start using device names as soon as requested. This will cause LiveKit to
@@ -344,26 +368,33 @@ export class MediaDevices {
// you to do to receive device names in lieu of a more explicit permissions // you to do to receive device names in lieu of a more explicit permissions
// API. This flag never resets to false, because once permissions are granted // API. This flag never resets to false, because once permissions are granted
// the first time, the user won't be prompted again until reload of the page. // the first time, the user won't be prompted again until reload of the page.
private readonly usingNames$ = this.deviceNamesRequest$.pipe( private readonly recomputeDevicesWithPermissions$ = this.requests$.pipe(
map(() => true),
startWith(false), startWith(false),
this.scope.state(), identity,
this.scope.stateNonDistinct(),
); );
public readonly audioInput: MediaDevice< public readonly audioInput: MediaDevice<
DeviceLabel, DeviceLabel,
SelectedAudioInputDevice SelectedAudioInputDevice
> = new AudioInput(this.usingNames$, this.scope); > = new AudioInput(this.recomputeDevicesWithPermissions$, this.scope);
public readonly audioOutput: MediaDevice< public readonly audioOutput: MediaDevice<
AudioOutputDeviceLabel, AudioOutputDeviceLabel,
SelectedAudioOutputDevice SelectedAudioOutputDevice
> = getUrlParams().controlledAudioDevices > = getUrlParams().controlledAudioDevices
? new ControlledAudioOutput(this.scope) ? new ControlledAudioOutput(this.scope)
: new AudioOutput(this.usingNames$, this.scope); : new AudioOutput(this.recomputeDevicesWithPermissions$, this.scope);
public readonly videoInput: MediaDevice<DeviceLabel, SelectedDevice> = public readonly videoInput: MediaDevice<DeviceLabel, SelectedDevice> =
new VideoInput(this.usingNames$, this.scope); new VideoInput(this.recomputeDevicesWithPermissions$, this.scope);
public constructor(private readonly scope: ObservableScope) {} public constructor(private readonly scope: ObservableScope) {
this.recomputeDevicesWithPermissions$.subscribe((recompute) => {
logger.info(
"[MediaDevices] recomputeDevicesWithPermissions$ changed:",
recompute,
);
});
}
} }

View File

@@ -38,6 +38,9 @@ export class ObservableScope {
shareReplay({ bufferSize: 1, refCount: false }), shareReplay({ bufferSize: 1, refCount: false }),
); );
private readonly stateNonDistinctImpl: MonoTypeOperator = (o$) =>
o$.pipe(this.bind(), shareReplay({ bufferSize: 1, refCount: false }));
/** /**
* Transforms an Observable into a hot state Observable which replays its * Transforms an Observable into a hot state Observable which replays its
* latest value upon subscription, skips updates with identical values, and * latest value upon subscription, skips updates with identical values, and
@@ -47,6 +50,15 @@ export class ObservableScope {
return this.stateImpl; return this.stateImpl;
} }
/**
* Transforms an Observable into a hot state Observable which replays its
* latest value upon subscription, skips updates with identical values, and
* is bound to this scope.
*/
public stateNonDistinct(): MonoTypeOperator {
return this.stateNonDistinctImpl;
}
/** /**
* Ends the scope, causing any bound Observables to complete. * Ends the scope, causing any bound Observables to complete.
*/ */