Compare commits

...

7 Commits

Author SHA1 Message Date
Robert Long
2c3ebd4c03 Add audio output setting when available 2022-02-22 18:32:51 -08:00
Robert Long
81a763f17f Avoid duplicate sessions across devices/browsers 2022-02-18 16:23:37 -08:00
Robert Long
1ab7d27ba9 Finish user avatars 2022-02-18 16:02:27 -08:00
Robert Long
e76a805c8f Better logging of to device events / usernames 2022-02-17 14:08:53 -08:00
Robert Long
9fc4af2bd7 Add version to console, rageshake, and settings modal 2022-02-16 11:29:43 -08:00
Robert Long
0f3a7f9fd9 Prevent scroll in call view 2022-02-16 11:17:33 -08:00
Robert Long
1cc634509b Add protocol to copied room url 2022-02-16 10:52:07 -08:00
22 changed files with 387 additions and 141 deletions

View File

@@ -24,6 +24,9 @@ yarn link
cd .. cd ..
cd matrix-video-chat cd matrix-video-chat
export VITE_APP_VERSION=$(git describe --tags --abbrev=0)
yarn link matrix-js-sdk yarn link matrix-js-sdk
yarn link matrix-react-sdk yarn link matrix-react-sdk
yarn install yarn install

View File

@@ -56,4 +56,5 @@
width: 90px; width: 90px;
height: 90px; height: 90px;
border-radius: 90px; border-radius: 90px;
font-size: 48px;
} }

View File

@@ -182,12 +182,21 @@ export function ClientProvider({ children }) {
}, [history]); }, [history]);
useEffect(() => { useEffect(() => {
if ("BroadcastChannel" in window) { if (client) {
const loadTime = Date.now(); const loadTime = Date.now();
const broadcastChannel = new BroadcastChannel("matrix-video-chat");
function onMessage({ data }) { const onToDeviceEvent = (event) => {
if (data.load !== undefined && data.load > loadTime) { if (event.getType() !== "org.matrix.call_duplicate_session") {
return;
}
const content = event.getContent();
if (content.session_id === client.getSessionId()) {
return;
}
if (content.timestamp > loadTime) {
if (client) { if (client) {
client.stopClient(); client.stopClient();
} }
@@ -199,13 +208,18 @@ export function ClientProvider({ children }) {
), ),
})); }));
} }
} };
broadcastChannel.addEventListener("message", onMessage); client.on("toDeviceEvent", onToDeviceEvent);
broadcastChannel.postMessage({ load: loadTime });
client.sendToDevice("org.matrix.call_duplicate_session", {
[client.getUserId()]: {
"*": { session_id: client.getSessionId(), timestamp: loadTime },
},
});
return () => { return () => {
broadcastChannel.removeEventListener("message", onMessage); client.removeListener("toDeviceEvent", onToDeviceEvent);
}; };
} }
}, [client]); }, [client]);

View File

@@ -43,14 +43,7 @@ export function UserMenuContainer({ preventNavigation }) {
displayName || (userName ? userName.replace("@", "") : undefined) displayName || (userName ? userName.replace("@", "") : undefined)
} }
/> />
{modalState.isOpen && ( {modalState.isOpen && <ProfileModal client={client} {...modalProps} />}
<ProfileModal
client={client}
isAuthenticated={isAuthenticated}
isPasswordlessUser={isPasswordlessUser}
{...modalProps}
/>
)}
</> </>
); );
} }

4
src/icons/Edit.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.64856 7.35501C2.65473 7.31601 2.67231 7.27972 2.69908 7.25069L8.40377 1.06442C8.47865 0.983217 8.60518 0.978093 8.68638 1.05297L9.8626 2.13763C9.9438 2.21251 9.94893 2.33904 9.87405 2.42024L4.16936 8.60651C4.1426 8.63554 4.10783 8.656 4.06946 8.6653L2.66781 9.00511C2.52911 9.03873 2.40084 8.92044 2.42315 8.77948L2.64856 7.35501Z" fill="white"/>
<path d="M1.75 9.44346C1.33579 9.44346 1 9.77925 1 10.1935C1 10.6077 1.33579 10.9435 1.75 10.9435L10.75 10.9435C11.1642 10.9435 11.5 10.6077 11.5 10.1935C11.5 9.77925 11.1642 9.44346 10.75 9.44346L1.75 9.44346Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 689 B

View File

@@ -0,0 +1,78 @@
import { useObjectRef } from "@react-aria/utils";
import React, { useEffect } from "react";
import { useCallback } from "react";
import { useState } from "react";
import { forwardRef } from "react";
import { Avatar } from "../Avatar";
import { Button } from "../button";
import classNames from "classnames";
import { ReactComponent as EditIcon } from "../icons/Edit.svg";
import styles from "./AvatarInputField.module.css";
export const AvatarInputField = forwardRef(
(
{ id, label, className, avatarUrl, displayName, onRemoveAvatar, ...rest },
ref
) => {
const [removed, setRemoved] = useState(false);
const [objUrl, setObjUrl] = useState(null);
const fileInputRef = useObjectRef(ref);
useEffect(() => {
const onChange = (e) => {
if (e.target.files.length > 0) {
setObjUrl(URL.createObjectURL(e.target.files[0]));
setRemoved(false);
} else {
setObjUrl(null);
}
};
fileInputRef.current.addEventListener("change", onChange);
return () => {
if (fileInputRef.current) {
fileInputRef.current.removeEventListener("change", onChange);
}
};
});
const onPressRemoveAvatar = useCallback(() => {
setRemoved(true);
onRemoveAvatar();
}, [onRemoveAvatar]);
return (
<div className={classNames(styles.avatarInputField, className)}>
<div className={styles.avatarContainer}>
<Avatar
size="xl"
src={removed ? null : objUrl || avatarUrl}
fallback={displayName.slice(0, 1).toUpperCase()}
/>
<input
id={id}
accept="image/png, image/jpeg"
ref={fileInputRef}
type="file"
className={styles.fileInput}
role="button"
aria-label={label}
{...rest}
/>
<label htmlFor={id} className={styles.fileInputButton}>
<EditIcon />
</label>
</div>
<Button
className={styles.removeButton}
variant="icon"
onPress={onPressRemoveAvatar}
>
Remove
</Button>
</div>
);
}
);

View File

@@ -0,0 +1,41 @@
.avatarInputField {
display: flex;
flex-direction: column;
justify-content: center;
}
.avatarContainer {
position: relative;
margin-bottom: 8px;
}
.fileInput {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.fileInput:focus + .fileInputButton {
outline: auto;
}
.fileInputButton {
position: absolute;
bottom: 11px;
right: -4px;
background-color: var(--bgColor4);
width: 20px;
height: 20px;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.removeButton {
color: #0dbd8b;
}

View File

@@ -27,6 +27,8 @@ import { InspectorContextProvider } from "./room/GroupCallInspector";
rageshake.init(); rageshake.init();
console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
if (import.meta.env.VITE_CUSTOM_THEME) { if (import.meta.env.VITE_CUSTOM_THEME) {
const style = document.documentElement.style; const style = document.documentElement.style;
style.setProperty("--primaryColor", import.meta.env.VITE_PRIMARY_COLOR); style.setProperty("--primaryColor", import.meta.env.VITE_PRIMARY_COLOR);

View File

@@ -120,12 +120,12 @@ export function getRoomUrl(roomId) {
const [localPart, host] = roomId.replace("#", "").split(":"); const [localPart, host] = roomId.replace("#", "").split(":");
if (host !== defaultHomeserverHost) { if (host !== defaultHomeserverHost) {
return `${window.location.host}/room/${roomId}`; return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
} else { } else {
return `${window.location.host}/${localPart}`; return `${window.location.protocol}//${window.location.host}/${localPart}`;
} }
} else { } else {
return `${window.location.host}/room/${roomId}`; return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
} }
} }

View File

@@ -3,22 +3,25 @@ import { Button } from "../button";
import { useProfile } from "./useProfile"; import { useProfile } from "./useProfile";
import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Modal, ModalContent } from "../Modal"; import { Modal, ModalContent } from "../Modal";
import { AvatarInputField } from "../input/AvatarInputField";
import styles from "./ProfileModal.module.css";
export function ProfileModal({ export function ProfileModal({ client, ...rest }) {
client,
isAuthenticated,
isPasswordlessUser,
...rest
}) {
const { onClose } = rest; const { onClose } = rest;
const { const {
success, success,
error, error,
loading, loading,
displayName: initialDisplayName, displayName: initialDisplayName,
avatarUrl,
saveProfile, saveProfile,
} = useProfile(client); } = useProfile(client);
const [displayName, setDisplayName] = useState(initialDisplayName || ""); const [displayName, setDisplayName] = useState(initialDisplayName || "");
const [removeAvatar, setRemoveAvatar] = useState(false);
const onRemoveAvatar = useCallback(() => {
setRemoveAvatar(true);
}, []);
const onChangeDisplayName = useCallback( const onChangeDisplayName = useCallback(
(e) => { (e) => {
@@ -37,9 +40,10 @@ export function ProfileModal({
saveProfile({ saveProfile({
displayName, displayName,
avatar: avatar && avatar.size > 0 ? avatar : undefined, avatar: avatar && avatar.size > 0 ? avatar : undefined,
removeAvatar: removeAvatar && (!avatar || avatar.size === 0),
}); });
}, },
[saveProfile] [saveProfile, removeAvatar]
); );
useEffect(() => { useEffect(() => {
@@ -52,6 +56,16 @@ export function ProfileModal({
<Modal title="Profile" isDismissable {...rest}> <Modal title="Profile" isDismissable {...rest}>
<ModalContent> <ModalContent>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<FieldRow className={styles.avatarFieldRow}>
<AvatarInputField
id="avatar"
name="avatar"
label="Avatar"
avatarUrl={avatarUrl}
displayName={displayName}
onRemoveAvatar={onRemoveAvatar}
/>
</FieldRow>
<FieldRow> <FieldRow>
<InputField <InputField
id="userId" id="userId"
@@ -75,16 +89,6 @@ export function ProfileModal({
onChange={onChangeDisplayName} onChange={onChangeDisplayName}
/> />
</FieldRow> </FieldRow>
{isAuthenticated && (
<FieldRow>
<InputField
type="file"
id="avatar"
name="avatar"
label="Avatar"
/>
</FieldRow>
)}
{error && ( {error && (
<FieldRow> <FieldRow>
<ErrorMessage>{error.message}</ErrorMessage> <ErrorMessage>{error.message}</ErrorMessage>

View File

@@ -0,0 +1,3 @@
.avatarFieldRow {
justify-content: center;
}

View File

@@ -44,7 +44,7 @@ export function useProfile(client) {
}, [client]); }, [client]);
const saveProfile = useCallback( const saveProfile = useCallback(
async ({ displayName, avatar }) => { async ({ displayName, avatar, removeAvatar }) => {
if (client) { if (client) {
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
@@ -58,7 +58,9 @@ export function useProfile(client) {
let mxcAvatarUrl; let mxcAvatarUrl;
if (avatar) { if (removeAvatar) {
await client.setAvatarUrl("");
} else if (avatar) {
mxcAvatarUrl = await client.uploadContent(avatar); mxcAvatarUrl = await client.uploadContent(avatar);
await client.setAvatarUrl(mxcAvatarUrl); await client.setAvatarUrl(mxcAvatarUrl);
} }
@@ -66,7 +68,9 @@ export function useProfile(client) {
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
displayName, displayName,
avatarUrl: mxcAvatarUrl avatarUrl: removeAvatar
? null
: mxcAvatarUrl
? getAvatarUrl(client, mxcAvatarUrl) ? getAvatarUrl(client, mxcAvatarUrl)
: prev.avatarUrl, : prev.avatarUrl,
loading: false, loading: false,

View File

@@ -59,8 +59,8 @@ function getUserName(userId) {
const match = userId.match(/@([^\:]+):/); const match = userId.match(/@([^\:]+):/);
return match && match.length > 0 return match && match.length > 0
? match[1].replace("-", " ").replace("W", "") ? match[1].replace("-", " ").replace(/\W/g, "")
: userId.replace("W", ""); : userId.replace(/\W/g, "");
} }
function formatContent(type, content) { function formatContent(type, content) {
@@ -231,7 +231,7 @@ function reducer(state, action) {
), ),
}; };
} }
case "receive_to_device_event": { case "received_voip_event": {
const event = action.event; const event = action.event;
const eventsByUserId = { ...state.eventsByUserId }; const eventsByUserId = { ...state.eventsByUserId };
const fromId = event.getSender(); const fromId = event.getSender();
@@ -338,8 +338,8 @@ function useGroupCallState(client, groupCall, pollCallStats) {
// dispatch({ type: "call_hangup", call }); // dispatch({ type: "call_hangup", call });
// } // }
function onToDeviceEvent(event) { function onReceivedVoipEvent(event) {
dispatch({ type: "receive_to_device_event", event }); dispatch({ type: "received_voip_event", event });
} }
function onSendVoipEvent(event) { function onSendVoipEvent(event) {
@@ -351,7 +351,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
groupCall.on("send_voip_event", onSendVoipEvent); groupCall.on("send_voip_event", onSendVoipEvent);
//client.on("state", onCallsChanged); //client.on("state", onCallsChanged);
//client.on("hangup", onCallHangup); //client.on("hangup", onCallHangup);
client.on("toDeviceEvent", onToDeviceEvent); client.on("received_voip_event", onReceivedVoipEvent);
onUpdateRoomState(); onUpdateRoomState();
@@ -361,7 +361,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
groupCall.removeListener("send_voip_event", onSendVoipEvent); groupCall.removeListener("send_voip_event", onSendVoipEvent);
//client.removeListener("state", onCallsChanged); //client.removeListener("state", onCallsChanged);
//client.removeListener("hangup", onCallHangup); //client.removeListener("hangup", onCallHangup);
client.removeListener("toDeviceEvent", onToDeviceEvent); client.removeListener("received_voip_event", onReceivedVoipEvent);
}; };
}, [client, groupCall]); }, [client, groupCall]);

View File

@@ -21,6 +21,8 @@ import { Avatar } from "../Avatar";
import { UserMenuContainer } from "../UserMenuContainer"; import { UserMenuContainer } from "../UserMenuContainer";
import { useRageshakeRequestModal } from "../settings/rageshake"; import { useRageshakeRequestModal } from "../settings/rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal"; import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { usePreventScroll } from "@react-aria/overlays";
import { useMediaHandler } from "../settings/useMediaHandler";
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices; const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
// There is currently a bug in Safari our our code with cloning and sending MediaStreams // There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -47,8 +49,11 @@ export function InCallView({
showInspector, showInspector,
roomId, roomId,
}) { }) {
usePreventScroll();
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0); const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
const { audioOutput } = useMediaHandler();
const items = useMemo(() => { const items = useMemo(() => {
const participants = []; const participants = [];
@@ -157,6 +162,7 @@ export function InCallView({
item={item} item={item}
getAvatar={renderAvatar} getAvatar={renderAvatar}
showName={items.length > 2 || item.focused} showName={items.length > 2 || item.focused}
audioOutputDevice={audioOutput}
{...rest} {...rest}
/> />
)} )}

View File

@@ -10,11 +10,11 @@ import { OverflowMenu } from "./OverflowMenu";
import { UserMenuContainer } from "../UserMenuContainer"; import { UserMenuContainer } from "../UserMenuContainer";
import { Body, Link } from "../typography/Typography"; import { Body, Link } from "../typography/Typography";
import { Avatar } from "../Avatar"; import { Avatar } from "../Avatar";
import { getAvatarUrl } from "../matrix-utils";
import { useProfile } from "../profile/useProfile"; import { useProfile } from "../profile/useProfile";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer"; import { ResizeObserver } from "@juggle/resize-observer";
import { useLocationNavigation } from "../useLocationNavigation"; import { useLocationNavigation } from "../useLocationNavigation";
import { useMediaHandler } from "../settings/useMediaHandler";
export function LobbyView({ export function LobbyView({
client, client,
@@ -32,7 +32,8 @@ export function LobbyView({
roomId, roomId,
}) { }) {
const { stream } = useCallFeed(localCallFeed); const { stream } = useCallFeed(localCallFeed);
const videoRef = useMediaStream(stream, true); const { audioOutput } = useMediaHandler();
const videoRef = useMediaStream(stream, audioOutput, true);
const { displayName, avatarUrl } = useProfile(client); const { displayName, avatarUrl } = useProfile(client);
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver }); const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
const avatarSize = (previewBounds.height - 66) / 2; const avatarSize = (previewBounds.height - 66) / 2;
@@ -86,7 +87,7 @@ export function LobbyView({
borderRadius: avatarSize, borderRadius: avatarSize,
fontSize: Math.round(avatarSize / 2), fontSize: Math.round(avatarSize / 2),
}} }}
src={avatarUrl && getAvatarUrl(client, avatarUrl, 96)} src={avatarUrl}
fallback={displayName.slice(0, 1).toUpperCase()} fallback={displayName.slice(0, 1).toUpperCase()}
/> />
</div> </div>

View File

@@ -75,7 +75,6 @@ export function OverflowMenu({
{...settingsModalProps} {...settingsModalProps}
setShowInspector={setShowInspector} setShowInspector={setShowInspector}
showInspector={showInspector} showInspector={showInspector}
client={client}
/> />
)} )}
{inviteModalState.isOpen && ( {inviteModalState.isOpen && (

View File

@@ -21,6 +21,7 @@ import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView"; import { RoomAuthView } from "./RoomAuthView";
import { GroupCallLoader } from "./GroupCallLoader"; import { GroupCallLoader } from "./GroupCallLoader";
import { GroupCallView } from "./GroupCallView"; import { GroupCallView } from "./GroupCallView";
import { MediaHandlerProvider } from "../settings/useMediaHandler";
export function RoomPage() { export function RoomPage() {
const { loading, isAuthenticated, error, client, isPasswordlessUser } = const { loading, isAuthenticated, error, client, isPasswordlessUser } =
@@ -47,16 +48,18 @@ export function RoomPage() {
} }
return ( return (
<GroupCallLoader client={client} roomId={roomId} viaServers={viaServers}> <MediaHandlerProvider client={client}>
{(groupCall) => ( <GroupCallLoader client={client} roomId={roomId} viaServers={viaServers}>
<GroupCallView {(groupCall) => (
client={client} <GroupCallView
roomId={roomId} client={client}
groupCall={groupCall} roomId={roomId}
isPasswordlessUser={isPasswordlessUser} groupCall={groupCall}
simpleGrid={simpleGrid} isPasswordlessUser={isPasswordlessUser}
/> simpleGrid={simpleGrid}
)} />
</GroupCallLoader> )}
</GroupCallLoader>
</MediaHandlerProvider>
); );
} }

View File

@@ -11,13 +11,9 @@ import { useMediaHandler } from "./useMediaHandler";
import { FieldRow, InputField } from "../input/Input"; import { FieldRow, InputField } from "../input/Input";
import { Button } from "../button"; import { Button } from "../button";
import { useDownloadDebugLog } from "./rageshake"; import { useDownloadDebugLog } from "./rageshake";
import { Body } from "../typography/Typography";
export function SettingsModal({ export function SettingsModal({ setShowInspector, showInspector, ...rest }) {
client,
setShowInspector,
showInspector,
...rest
}) {
const { const {
audioInput, audioInput,
audioInputs, audioInputs,
@@ -25,7 +21,10 @@ export function SettingsModal({
videoInput, videoInput,
videoInputs, videoInputs,
setVideoInput, setVideoInput,
} = useMediaHandler(client); audioOutput,
audioOutputs,
setAudioOutput,
} = useMediaHandler();
const downloadDebugLog = useDownloadDebugLog(); const downloadDebugLog = useDownloadDebugLog();
@@ -55,6 +54,17 @@ export function SettingsModal({
<Item key={deviceId}>{label}</Item> <Item key={deviceId}>{label}</Item>
))} ))}
</SelectInput> </SelectInput>
{audioOutputs.length > 0 && (
<SelectInput
label="Speaker"
selectedKey={audioOutput}
onSelectionChange={setAudioOutput}
>
{audioOutputs.map(({ deviceId, label }) => (
<Item key={deviceId}>{label}</Item>
))}
</SelectInput>
)}
</TabItem> </TabItem>
<TabItem <TabItem
title={ title={
@@ -82,6 +92,11 @@ export function SettingsModal({
</> </>
} }
> >
<FieldRow>
<Body className={styles.fieldRowText}>
Version: {import.meta.env.VITE_APP_VERSION || "dev"}
</Body>
</FieldRow>
<FieldRow> <FieldRow>
<InputField <InputField
id="showInspector" id="showInspector"

View File

@@ -6,3 +6,7 @@
.tabContainer { .tabContainer {
margin: 27px 16px; margin: 27px 16px;
} }
.fieldRowText {
margin-bottom: 0;
}

View File

@@ -41,7 +41,7 @@ export function useSubmitRageshake() {
opts.description || "User did not supply any additional text." opts.description || "User did not supply any additional text."
); );
body.append("app", "matrix-video-chat"); body.append("app", "matrix-video-chat");
body.append("version", "dev"); body.append("version", import.meta.env.VITE_APP_VERSION || "dev");
body.append("user_agent", userAgent); body.append("user_agent", userAgent);
body.append("installed_pwa", false); body.append("installed_pwa", false);
body.append("touch_input", touchInput); body.append("touch_input", touchInput);

View File

@@ -1,72 +0,0 @@
import { useState, useEffect, useCallback } from "react";
export function useMediaHandler(client) {
const [{ audioInput, videoInput, audioInputs, videoInputs }, setState] =
useState(() => {
const mediaHandler = client.getMediaHandler();
return {
audioInput: mediaHandler.audioInput,
videoInput: mediaHandler.videoInput,
audioInputs: [],
videoInputs: [],
};
});
useEffect(() => {
const mediaHandler = client.getMediaHandler();
function updateDevices() {
navigator.mediaDevices.enumerateDevices().then((devices) => {
const audioInputs = devices.filter(
(device) => device.kind === "audioinput"
);
const videoInputs = devices.filter(
(device) => device.kind === "videoinput"
);
setState(() => ({
audioInput: mediaHandler.audioInput,
videoInput: mediaHandler.videoInput,
audioInputs,
videoInputs,
}));
});
}
updateDevices();
mediaHandler.on("local_streams_changed", updateDevices);
navigator.mediaDevices.addEventListener("devicechange", updateDevices);
return () => {
mediaHandler.removeListener("local_streams_changed", updateDevices);
navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
};
}, []);
const setAudioInput = useCallback(
(deviceId) => {
setState((prevState) => ({ ...prevState, audioInput: deviceId }));
client.getMediaHandler().setAudioInput(deviceId);
},
[client]
);
const setVideoInput = useCallback(
(deviceId) => {
setState((prevState) => ({ ...prevState, videoInput: deviceId }));
client.getMediaHandler().setVideoInput(deviceId);
},
[client]
);
return {
audioInput,
audioInputs,
setAudioInput,
videoInput,
videoInputs,
setVideoInput,
};
}

View File

@@ -0,0 +1,143 @@
import React, {
useState,
useEffect,
useCallback,
useMemo,
useContext,
createContext,
} from "react";
const MediaHandlerContext = createContext();
export function MediaHandlerProvider({ client, children }) {
const [
{
audioInput,
videoInput,
audioInputs,
videoInputs,
audioOutput,
audioOutputs,
},
setState,
] = useState(() => {
const mediaHandler = client.getMediaHandler();
return {
audioInput: mediaHandler.audioInput,
videoInput: mediaHandler.videoInput,
audioOutput: undefined,
audioInputs: [],
videoInputs: [],
audioOutputs: [],
};
});
useEffect(() => {
const mediaHandler = client.getMediaHandler();
function updateDevices() {
navigator.mediaDevices.enumerateDevices().then((devices) => {
const audioInputs = devices.filter(
(device) => device.kind === "audioinput"
);
const videoInputs = devices.filter(
(device) => device.kind === "videoinput"
);
const audioOutputs = devices.filter(
(device) => device.kind === "audiooutput"
);
let audioOutput = undefined;
const audioOutputPreference = localStorage.getItem(
"matrix-audio-output"
);
if (
audioOutputPreference &&
audioOutputs.some(
(device) => device.deviceId === audioOutputPreference
)
) {
audioOutput = audioOutputPreference;
}
setState({
audioInput: mediaHandler.audioInput,
videoInput: mediaHandler.videoInput,
audioOutput,
audioInputs,
audioOutputs,
videoInputs,
});
});
}
updateDevices();
mediaHandler.on("local_streams_changed", updateDevices);
navigator.mediaDevices.addEventListener("devicechange", updateDevices);
return () => {
mediaHandler.removeListener("local_streams_changed", updateDevices);
navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
};
}, [client]);
const setAudioInput = useCallback(
(deviceId) => {
setState((prevState) => ({ ...prevState, audioInput: deviceId }));
client.getMediaHandler().setAudioInput(deviceId);
},
[client]
);
const setVideoInput = useCallback(
(deviceId) => {
setState((prevState) => ({ ...prevState, videoInput: deviceId }));
client.getMediaHandler().setVideoInput(deviceId);
},
[client]
);
const setAudioOutput = useCallback((deviceId) => {
localStorage.setItem("matrix-audio-output", deviceId);
setState((prevState) => ({ ...prevState, audioOutput: deviceId }));
}, []);
const context = useMemo(
() => ({
audioInput,
audioInputs,
setAudioInput,
videoInput,
videoInputs,
setVideoInput,
audioOutput,
audioOutputs,
setAudioOutput,
}),
[
audioInput,
audioInputs,
setAudioInput,
videoInput,
videoInputs,
setVideoInput,
audioOutput,
audioOutputs,
setAudioOutput,
]
);
return (
<MediaHandlerContext.Provider value={context}>
{children}
</MediaHandlerContext.Provider>
);
}
export function useMediaHandler() {
return useContext(MediaHandlerContext);
}