Merge pull request #3353 from element-hq/toger5/device-permissions-request-possible-fix

Skip unnecassary media devices permissions requests (video feed flicker when opening settings)
This commit is contained in:
Robin
2025-06-25 15:56:10 -04:00
committed by GitHub
3 changed files with 84 additions and 11 deletions

View File

@@ -184,6 +184,16 @@ export const LobbyView: FC<Props> = ({
null) as LocalVideoTrack | null, null) as LocalVideoTrack | null,
[tracks], [tracks],
); );
useEffect(() => {
if (videoTrack && videoInputId === undefined) {
// If we have a video track but no videoInputId,
// we have to update the available devices. So that we select the first
// available video input device as the default instead of the `""` id.
devices.requestDeviceNames();
}
}, [devices, videoInputId, videoTrack]);
useTrackProcessorSync(videoTrack); useTrackProcessorSync(videoTrack);
const showSwitchCamera = useShowSwitchCamera( const showSwitchCamera = useShowSwitchCamera(
useObservable( useObservable(

View File

@@ -18,7 +18,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,
@@ -33,10 +33,12 @@ 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.
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 }
@@ -96,13 +98,25 @@ function availableRawDevices$(
usingNames$: Observable<boolean>, usingNames$: Observable<boolean>,
scope: ObservableScope, scope: ObservableScope,
): Observable<MediaDeviceInfo[]> { ): Observable<MediaDeviceInfo[]> {
const logError = (e: Error): void =>
logger.error("Error creating MediaDeviceObserver", e);
const devices$ = createMediaDeviceObserver(kind, logError, false);
const devicesWithNames$ = createMediaDeviceObserver(kind, logError, true);
return usingNames$.pipe( return usingNames$.pipe(
switchMap((usingNames) => switchMap((withNames) =>
createMediaDeviceObserver( withNames
kind, ? // It might be that there is already a media stream running somewhere,
(e) => logger.error("Error creating MediaDeviceObserver", e), // and so we can do without requesting a second one. Only switch to the
usingNames, // 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(),
@@ -181,7 +195,11 @@ class AudioInput implements MediaDevice<DeviceLabel, SelectedAudioInputDevice> {
public constructor( public constructor(
private readonly usingNames$: Observable<boolean>, private readonly usingNames$: 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
@@ -232,7 +250,11 @@ class AudioOutput
public constructor( public constructor(
private readonly usingNames$: Observable<boolean>, private readonly usingNames$: 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,6 +320,9 @@ class ControlledAudioOutput
window.controls.onOutputDeviceSelect?.(device.id); window.controls.onOutputDeviceSelect?.(device.id);
} }
}); });
this.available$.subscribe((available) => {
logger.info("[controlled-output] available devices:", available);
});
} }
} }
@@ -323,7 +348,12 @@ class VideoInput implements MediaDevice<DeviceLabel, SelectedDevice> {
public constructor( public constructor(
private readonly usingNames$: 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.available$.subscribe((available) => {
logger.info("[video-input] available devices:", available);
});
}
} }
export class MediaDevices { export class MediaDevices {

View File

@@ -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.