Simplify and improve locality of the device name request logic
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
|||||||
} from "../controls";
|
} from "../controls";
|
||||||
import { getUrlParams } from "../UrlParams";
|
import { getUrlParams } from "../UrlParams";
|
||||||
import { platform } from "../Platform";
|
import { platform } from "../Platform";
|
||||||
|
import { switchWhen } from "../utils/observable";
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -94,17 +95,28 @@ export const iosDeviceMenu$ =
|
|||||||
|
|
||||||
function availableRawDevices$(
|
function availableRawDevices$(
|
||||||
kind: MediaDeviceKind,
|
kind: MediaDeviceKind,
|
||||||
updateAvailableDeviceRequests$: Observable<boolean>,
|
usingNames$: Observable<boolean>,
|
||||||
scope: ObservableScope,
|
scope: ObservableScope,
|
||||||
): Observable<MediaDeviceInfo[]> {
|
): Observable<MediaDeviceInfo[]> {
|
||||||
return updateAvailableDeviceRequests$.pipe(
|
const logError = (e: Error): void =>
|
||||||
startWith(false),
|
logger.error("Error creating MediaDeviceObserver", e);
|
||||||
switchMap((withPermissions) =>
|
const devices$ = createMediaDeviceObserver(kind, logError, false);
|
||||||
createMediaDeviceObserver(
|
const devicesWithNames$ = createMediaDeviceObserver(kind, logError, true);
|
||||||
kind,
|
|
||||||
(e) => logger.error("Error creating MediaDeviceObserver", e),
|
return usingNames$.pipe(
|
||||||
withPermissions,
|
switchMap((withNames) =>
|
||||||
),
|
withNames
|
||||||
|
? // It might be that there is already a media stream running somewhere,
|
||||||
|
// and so we can do without requesting a second one. Only switch to the
|
||||||
|
// device observer that explicitly requests the names if we see that
|
||||||
|
// names are in fact missing from the initial device enumeration.
|
||||||
|
devices$.pipe(
|
||||||
|
switchWhen(
|
||||||
|
(devices, i) => i === 0 && devices.every((d) => !d.label),
|
||||||
|
devicesWithNames$,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: devices$,
|
||||||
),
|
),
|
||||||
startWith([]),
|
startWith([]),
|
||||||
scope.state(),
|
scope.state(),
|
||||||
@@ -147,11 +159,7 @@ 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$(
|
availableRawDevices$("audioinput", this.usingNames$, this.scope);
|
||||||
"audioinput",
|
|
||||||
this.updateAvailableDeviceRequests$,
|
|
||||||
this.scope,
|
|
||||||
);
|
|
||||||
|
|
||||||
public readonly available$ = this.availableRaw$.pipe(
|
public readonly available$ = this.availableRaw$.pipe(
|
||||||
map(buildDeviceMap),
|
map(buildDeviceMap),
|
||||||
@@ -185,7 +193,7 @@ class AudioInput implements MediaDevice<DeviceLabel, SelectedAudioInputDevice> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly updateAvailableDeviceRequests$: Observable<boolean>,
|
private readonly usingNames$: Observable<boolean>,
|
||||||
private readonly scope: ObservableScope,
|
private readonly scope: ObservableScope,
|
||||||
) {
|
) {
|
||||||
this.available$.subscribe((available) => {
|
this.available$.subscribe((available) => {
|
||||||
@@ -199,7 +207,7 @@ class AudioOutput
|
|||||||
{
|
{
|
||||||
public readonly available$ = availableRawDevices$(
|
public readonly available$ = availableRawDevices$(
|
||||||
"audiooutput",
|
"audiooutput",
|
||||||
this.updateAvailableDeviceRequests$,
|
this.usingNames$,
|
||||||
this.scope,
|
this.scope,
|
||||||
).pipe(
|
).pipe(
|
||||||
map((availableRaw) => {
|
map((availableRaw) => {
|
||||||
@@ -240,7 +248,7 @@ class AudioOutput
|
|||||||
}
|
}
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly updateAvailableDeviceRequests$: Observable<boolean>,
|
private readonly usingNames$: Observable<boolean>,
|
||||||
private readonly scope: ObservableScope,
|
private readonly scope: ObservableScope,
|
||||||
) {
|
) {
|
||||||
this.available$.subscribe((available) => {
|
this.available$.subscribe((available) => {
|
||||||
@@ -321,7 +329,7 @@ class ControlledAudioOutput
|
|||||||
class VideoInput implements MediaDevice<DeviceLabel, SelectedDevice> {
|
class VideoInput implements MediaDevice<DeviceLabel, SelectedDevice> {
|
||||||
public readonly available$ = availableRawDevices$(
|
public readonly available$ = availableRawDevices$(
|
||||||
"videoinput",
|
"videoinput",
|
||||||
this.updateAvailableDeviceRequests$,
|
this.usingNames$,
|
||||||
this.scope,
|
this.scope,
|
||||||
).pipe(map(buildDeviceMap));
|
).pipe(map(buildDeviceMap));
|
||||||
|
|
||||||
@@ -338,7 +346,7 @@ class VideoInput implements MediaDevice<DeviceLabel, SelectedDevice> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly updateAvailableDeviceRequests$: Observable<boolean>,
|
private readonly usingNames$: Observable<boolean>,
|
||||||
private readonly scope: ObservableScope,
|
private readonly scope: ObservableScope,
|
||||||
) {
|
) {
|
||||||
// This also has the purpose of subscribing to the available devices
|
// This also has the purpose of subscribing to the available devices
|
||||||
@@ -349,48 +357,43 @@ class VideoInput implements MediaDevice<DeviceLabel, SelectedDevice> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MediaDevices {
|
export class MediaDevices {
|
||||||
private readonly updateAvailableDeviceRequests$ = new Subject<boolean>();
|
private readonly deviceNamesRequest$ = new Subject<void>();
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
* permissions pop-up, so it should only be called when there is a clear user
|
* permissions pop-up, so it should only be called when there is a clear user
|
||||||
* intent to view the device list.
|
* intent to view the device list.
|
||||||
*
|
|
||||||
* This always updates the `available$` devices for each media type with the current value
|
|
||||||
* of `enumerateDevices`.
|
|
||||||
*/
|
*/
|
||||||
public requestDeviceNames(): void {
|
public requestDeviceNames(): void {
|
||||||
void navigator.mediaDevices.enumerateDevices().then((result) => {
|
this.deviceNamesRequest$.next();
|
||||||
// we only actually update the requests$ subject if there are no
|
|
||||||
// devices with a label, because otherwise we already have the permission
|
|
||||||
// to access the devices.
|
|
||||||
this.updateAvailableDeviceRequests$.next(
|
|
||||||
!result.some((device) => device.label),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start using device names as soon as requested. This will cause LiveKit to
|
||||||
|
// briefly request device permissions and acquire media streams for each
|
||||||
|
// device type while calling `enumerateDevices`, which is what browsers want
|
||||||
|
// 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
|
||||||
|
// the first time, the user won't be prompted again until reload of the page.
|
||||||
|
private readonly usingNames$ = this.deviceNamesRequest$.pipe(
|
||||||
|
map(() => true),
|
||||||
|
startWith(false),
|
||||||
|
this.scope.state(),
|
||||||
|
);
|
||||||
|
|
||||||
public readonly audioInput: MediaDevice<
|
public readonly audioInput: MediaDevice<
|
||||||
DeviceLabel,
|
DeviceLabel,
|
||||||
SelectedAudioInputDevice
|
SelectedAudioInputDevice
|
||||||
> = new AudioInput(this.updateAvailableDeviceRequests$, this.scope);
|
> = new AudioInput(this.usingNames$, 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.updateAvailableDeviceRequests$, this.scope);
|
: new AudioOutput(this.usingNames$, this.scope);
|
||||||
|
|
||||||
public readonly videoInput: MediaDevice<DeviceLabel, SelectedDevice> =
|
public readonly videoInput: MediaDevice<DeviceLabel, SelectedDevice> =
|
||||||
new VideoInput(this.updateAvailableDeviceRequests$, this.scope);
|
new VideoInput(this.usingNames$, this.scope);
|
||||||
|
|
||||||
public constructor(private readonly scope: ObservableScope) {
|
public constructor(private readonly scope: ObservableScope) {}
|
||||||
this.updateAvailableDeviceRequests$.subscribe((recompute) => {
|
|
||||||
logger.info(
|
|
||||||
"[MediaDevices] updateAvailableDeviceRequests$ changed:",
|
|
||||||
recompute,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type Observable, defer, finalize, scan, startWith, tap } from "rxjs";
|
import {
|
||||||
|
type Observable,
|
||||||
|
concat,
|
||||||
|
defer,
|
||||||
|
finalize,
|
||||||
|
map,
|
||||||
|
scan,
|
||||||
|
startWith,
|
||||||
|
takeWhile,
|
||||||
|
tap,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
const nothing = Symbol("nothing");
|
const nothing = Symbol("nothing");
|
||||||
|
|
||||||
@@ -39,6 +49,29 @@ export function accumulate<State, Event>(
|
|||||||
events$.pipe(scan(update, initial), startWith(initial));
|
events$.pipe(scan(update, initial), startWith(initial));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const switchSymbol = Symbol("switch");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RxJS operator which behaves like the input Observable (A) until it emits a
|
||||||
|
* value satisfying the given predicate, then behaves like Observable B.
|
||||||
|
*
|
||||||
|
* The switch is immediate; the value that triggers the switch will not be
|
||||||
|
* present in the output.
|
||||||
|
*/
|
||||||
|
export function switchWhen<A, B>(
|
||||||
|
predicate: (a: A, index: number) => boolean,
|
||||||
|
b$: Observable<B>,
|
||||||
|
) {
|
||||||
|
return (a$: Observable<A>): Observable<A | B> =>
|
||||||
|
concat(
|
||||||
|
a$.pipe(
|
||||||
|
map((a, index) => (predicate(a, index) ? switchSymbol : a)),
|
||||||
|
takeWhile((a) => a !== switchSymbol),
|
||||||
|
) as Observable<A>,
|
||||||
|
b$,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the current value of a state Observable without reacting to future
|
* Reads the current value of a state Observable without reacting to future
|
||||||
* changes.
|
* changes.
|
||||||
|
|||||||
Reference in New Issue
Block a user