Merge pull request #1065 from robintown/resist-fingerprinting
Make Element Call work in Firefox's resist fingerprinting mode
This commit is contained in:
commit
e93dfb54d2
4 changed files with 104 additions and 107 deletions
|
@ -47,7 +47,7 @@ export async function findDeviceByName(
|
||||||
*
|
*
|
||||||
* @return The available media devices
|
* @return The available media devices
|
||||||
*/
|
*/
|
||||||
export async function getDevices(): Promise<MediaDeviceInfo[]> {
|
export async function getNamedDevices(): Promise<MediaDeviceInfo[]> {
|
||||||
// First get the devices without their labels, to learn what kinds of streams
|
// First get the devices without their labels, to learn what kinds of streams
|
||||||
// we can request
|
// we can request
|
||||||
let devices: MediaDeviceInfo[];
|
let devices: MediaDeviceInfo[];
|
||||||
|
|
|
@ -34,7 +34,7 @@ import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||||
import { useMediaHandler } from "../settings/useMediaHandler";
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||||
import { findDeviceByName, getDevices } from "../media-utils";
|
import { findDeviceByName, getNamedDevices } from "../media-utils";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -102,7 +102,7 @@ export function GroupCallView({
|
||||||
// Get the available devices so we can match the selected device
|
// Get the available devices so we can match the selected device
|
||||||
// to its ID. This involves getting a media stream (see docs on
|
// to its ID. This involves getting a media stream (see docs on
|
||||||
// the function) so we only do it once and re-use the result.
|
// the function) so we only do it once and re-use the result.
|
||||||
const devices = await getDevices();
|
const devices = await getNamedDevices();
|
||||||
|
|
||||||
const { audioInput, videoInput } = ev.detail
|
const { audioInput, videoInput } = ev.detail
|
||||||
.data as unknown as JoinCallData;
|
.data as unknown as JoinCallData;
|
||||||
|
|
|
@ -57,7 +57,9 @@ export const SettingsModal = (props: Props) => {
|
||||||
audioOutput,
|
audioOutput,
|
||||||
audioOutputs,
|
audioOutputs,
|
||||||
setAudioOutput,
|
setAudioOutput,
|
||||||
|
useDeviceNames,
|
||||||
} = useMediaHandler();
|
} = useMediaHandler();
|
||||||
|
useDeviceNames();
|
||||||
|
|
||||||
const [spatialAudio, setSpatialAudio] = useSpatialAudio();
|
const [spatialAudio, setSpatialAudio] = useSpatialAudio();
|
||||||
const [showInspector, setShowInspector] = useShowInspector();
|
const [showInspector, setShowInspector] = useShowInspector();
|
||||||
|
|
|
@ -32,7 +32,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { MediaHandlerEvent } from "matrix-js-sdk/src/webrtc/mediaHandler";
|
|
||||||
import React, {
|
import React, {
|
||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
@ -41,18 +40,26 @@ import React, {
|
||||||
useContext,
|
useContext,
|
||||||
createContext,
|
createContext,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
|
import { getNamedDevices } from "../media-utils";
|
||||||
|
|
||||||
export interface MediaHandlerContextInterface {
|
export interface MediaHandlerContextInterface {
|
||||||
audioInput: string;
|
audioInput: string | undefined;
|
||||||
audioInputs: MediaDeviceInfo[];
|
audioInputs: MediaDeviceInfo[];
|
||||||
setAudioInput: (deviceId: string) => void;
|
setAudioInput: (deviceId: string) => void;
|
||||||
videoInput: string;
|
videoInput: string | undefined;
|
||||||
videoInputs: MediaDeviceInfo[];
|
videoInputs: MediaDeviceInfo[];
|
||||||
setVideoInput: (deviceId: string) => void;
|
setVideoInput: (deviceId: string) => void;
|
||||||
audioOutput: string;
|
audioOutput: string | undefined;
|
||||||
audioOutputs: MediaDeviceInfo[];
|
audioOutputs: MediaDeviceInfo[];
|
||||||
setAudioOutput: (deviceId: string) => void;
|
setAudioOutput: (deviceId: string) => void;
|
||||||
|
/**
|
||||||
|
* A hook which requests for devices to be named. This requires media
|
||||||
|
* permissions.
|
||||||
|
*/
|
||||||
|
useDeviceNames: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MediaHandlerContext =
|
const MediaHandlerContext =
|
||||||
|
@ -70,10 +77,10 @@ function getMediaPreferences(): MediaPreferences {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(mediaPreferences);
|
return JSON.parse(mediaPreferences);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return undefined;
|
return {};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,112 +110,98 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element {
|
||||||
audioOutputs,
|
audioOutputs,
|
||||||
},
|
},
|
||||||
setState,
|
setState,
|
||||||
] = useState(() => {
|
] = useState(() => ({
|
||||||
const mediaPreferences = getMediaPreferences();
|
audioInput: undefined as string | undefined,
|
||||||
const mediaHandler = client.getMediaHandler();
|
videoInput: undefined as string | undefined,
|
||||||
|
audioOutput: undefined as string | undefined,
|
||||||
|
audioInputs: [] as MediaDeviceInfo[],
|
||||||
|
videoInputs: [] as MediaDeviceInfo[],
|
||||||
|
audioOutputs: [] as MediaDeviceInfo[],
|
||||||
|
}));
|
||||||
|
|
||||||
mediaHandler.restoreMediaSettings(
|
// A ref counting the number of components currently mounted that want
|
||||||
mediaPreferences?.audioInput,
|
// to know device names
|
||||||
mediaPreferences?.videoInput
|
const numComponentsWantingNames = useRef(0);
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
const updateDevices = useCallback(
|
||||||
// @ts-ignore, ignore that audioInput is a private members of mediaHandler
|
async (initial: boolean) => {
|
||||||
audioInput: mediaHandler.audioInput,
|
// Only request device names if components actually want them, because it
|
||||||
// @ts-ignore, ignore that videoInput is a private members of mediaHandler
|
// could trigger an extra permission pop-up
|
||||||
videoInput: mediaHandler.videoInput,
|
const devices = await (numComponentsWantingNames.current > 0
|
||||||
audioOutput: undefined,
|
? getNamedDevices()
|
||||||
audioInputs: [],
|
: navigator.mediaDevices.enumerateDevices());
|
||||||
videoInputs: [],
|
const mediaPreferences = getMediaPreferences();
|
||||||
audioOutputs: [],
|
|
||||||
};
|
const audioInputs = devices.filter((d) => d.kind === "audioinput");
|
||||||
});
|
const videoInputs = devices.filter((d) => d.kind === "videoinput");
|
||||||
|
const audioOutputs = devices.filter((d) => d.kind === "audiooutput");
|
||||||
|
|
||||||
|
const audioInput = (
|
||||||
|
mediaPreferences.audioInput === undefined
|
||||||
|
? audioInputs.at(0)
|
||||||
|
: audioInputs.find(
|
||||||
|
(d) => d.deviceId === mediaPreferences.audioInput
|
||||||
|
) ?? audioInputs.at(0)
|
||||||
|
)?.deviceId;
|
||||||
|
const videoInput = (
|
||||||
|
mediaPreferences.videoInput === undefined
|
||||||
|
? videoInputs.at(0)
|
||||||
|
: videoInputs.find(
|
||||||
|
(d) => d.deviceId === mediaPreferences.videoInput
|
||||||
|
) ?? videoInputs.at(0)
|
||||||
|
)?.deviceId;
|
||||||
|
const audioOutput =
|
||||||
|
mediaPreferences.audioOutput === undefined
|
||||||
|
? undefined
|
||||||
|
: audioOutputs.find(
|
||||||
|
(d) => d.deviceId === mediaPreferences.audioOutput
|
||||||
|
)?.deviceId;
|
||||||
|
|
||||||
|
updateMediaPreferences({ audioInput, videoInput, audioOutput });
|
||||||
|
setState({
|
||||||
|
audioInput,
|
||||||
|
videoInput,
|
||||||
|
audioOutput,
|
||||||
|
audioInputs,
|
||||||
|
videoInputs,
|
||||||
|
audioOutputs,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
initial ||
|
||||||
|
audioInput !== mediaPreferences.audioInput ||
|
||||||
|
videoInput !== mediaPreferences.videoInput
|
||||||
|
) {
|
||||||
|
client.getMediaHandler().setMediaInputs(audioInput, videoInput);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[client, setState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const useDeviceNames = useCallback(() => {
|
||||||
|
// This is a little weird from React's perspective as it looks like a
|
||||||
|
// dynamic hook, but it works
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
useEffect(() => {
|
||||||
|
numComponentsWantingNames.current++;
|
||||||
|
if (numComponentsWantingNames.current === 1) updateDevices(false);
|
||||||
|
return () => void numComponentsWantingNames.current--;
|
||||||
|
}, []);
|
||||||
|
}, [updateDevices]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mediaHandler = client.getMediaHandler();
|
updateDevices(true);
|
||||||
|
const onDeviceChange = () => updateDevices(false);
|
||||||
function updateDevices(): void {
|
navigator.mediaDevices.addEventListener("devicechange", onDeviceChange);
|
||||||
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
|
||||||
const mediaPreferences = getMediaPreferences();
|
|
||||||
|
|
||||||
const audioInputs = devices.filter(
|
|
||||||
(device) => device.kind === "audioinput"
|
|
||||||
);
|
|
||||||
const audioConnected = audioInputs.some(
|
|
||||||
// @ts-ignore
|
|
||||||
(device) => device.deviceId === mediaHandler.audioInput
|
|
||||||
);
|
|
||||||
// @ts-ignore
|
|
||||||
let audioInput = mediaHandler.audioInput;
|
|
||||||
|
|
||||||
if (!audioConnected && audioInputs.length > 0) {
|
|
||||||
audioInput = audioInputs[0].deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const videoInputs = devices.filter(
|
|
||||||
(device) => device.kind === "videoinput"
|
|
||||||
);
|
|
||||||
const videoConnected = videoInputs.some(
|
|
||||||
// @ts-ignore
|
|
||||||
(device) => device.deviceId === mediaHandler.videoInput
|
|
||||||
);
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
let videoInput = mediaHandler.videoInput;
|
|
||||||
|
|
||||||
if (!videoConnected && videoInputs.length > 0) {
|
|
||||||
videoInput = videoInputs[0].deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioOutputs = devices.filter(
|
|
||||||
(device) => device.kind === "audiooutput"
|
|
||||||
);
|
|
||||||
let audioOutput = undefined;
|
|
||||||
|
|
||||||
if (
|
|
||||||
mediaPreferences &&
|
|
||||||
audioOutputs.some(
|
|
||||||
(device) => device.deviceId === mediaPreferences.audioOutput
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
audioOutput = mediaPreferences.audioOutput;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
// @ts-ignore
|
|
||||||
(mediaHandler.videoInput && mediaHandler.videoInput !== videoInput) ||
|
|
||||||
// @ts-ignore
|
|
||||||
mediaHandler.audioInput !== audioInput
|
|
||||||
) {
|
|
||||||
mediaHandler.setMediaInputs(audioInput, videoInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMediaPreferences({ audioInput, videoInput, audioOutput });
|
|
||||||
|
|
||||||
setState({
|
|
||||||
audioInput,
|
|
||||||
videoInput,
|
|
||||||
audioOutput,
|
|
||||||
audioInputs,
|
|
||||||
videoInputs,
|
|
||||||
audioOutputs,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
updateDevices();
|
|
||||||
|
|
||||||
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, updateDevices);
|
|
||||||
navigator.mediaDevices.addEventListener("devicechange", updateDevices);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
mediaHandler.removeListener(
|
navigator.mediaDevices.removeEventListener(
|
||||||
MediaHandlerEvent.LocalStreamsChanged,
|
"devicechange",
|
||||||
updateDevices
|
onDeviceChange
|
||||||
);
|
);
|
||||||
navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
|
client.getMediaHandler().stopAllStreams();
|
||||||
mediaHandler.stopAllStreams();
|
|
||||||
};
|
};
|
||||||
}, [client]);
|
}, [client, updateDevices]);
|
||||||
|
|
||||||
const setAudioInput: (deviceId: string) => void = useCallback(
|
const setAudioInput: (deviceId: string) => void = useCallback(
|
||||||
(deviceId: string) => {
|
(deviceId: string) => {
|
||||||
|
@ -245,6 +238,7 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element {
|
||||||
audioOutput,
|
audioOutput,
|
||||||
audioOutputs,
|
audioOutputs,
|
||||||
setAudioOutput,
|
setAudioOutput,
|
||||||
|
useDeviceNames,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
audioInput,
|
audioInput,
|
||||||
|
@ -256,6 +250,7 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element {
|
||||||
audioOutput,
|
audioOutput,
|
||||||
audioOutputs,
|
audioOutputs,
|
||||||
setAudioOutput,
|
setAudioOutput,
|
||||||
|
useDeviceNames,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue