Remove voice
parent
423fbb6546
commit
2974dda142
|
|
@ -37,14 +37,10 @@ export default observer(
|
|||
const isServerChannel =
|
||||
server ||
|
||||
(target &&
|
||||
(target.channel_type === "TextChannel" ||
|
||||
target.channel_type === "VoiceChannel"));
|
||||
(target.channel_type === "TextChannel"));
|
||||
|
||||
if (typeof iconURL === "undefined") {
|
||||
if (isServerChannel) {
|
||||
if (target?.channel_type === "VoiceChannel") {
|
||||
return <VolumeFull size={size} />;
|
||||
}
|
||||
return <Hash size={size} />;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { VolumeMute, MicrophoneOff } from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { User, API } from "revolt.js";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
|
|
@ -11,11 +9,9 @@ import fallback from "../assets/user.png";
|
|||
import { useClient } from "../../../controllers/client/ClientController";
|
||||
import IconBase, { IconBaseProps } from "../IconBase";
|
||||
|
||||
type VoiceStatus = "muted" | "deaf";
|
||||
interface Props extends IconBaseProps<User> {
|
||||
status?: boolean;
|
||||
override?: string;
|
||||
voice?: VoiceStatus;
|
||||
masquerade?: API.Masquerade;
|
||||
showServerIdentity?: boolean;
|
||||
}
|
||||
|
|
@ -34,22 +30,6 @@ export function useStatusColour(user?: User) {
|
|||
: theme.getVariable("status-invisible");
|
||||
}
|
||||
|
||||
const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: var(--border-radius-half);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
${(props) =>
|
||||
(props.status === "muted" || props.status === "deaf") &&
|
||||
css`
|
||||
background: var(--error);
|
||||
`}
|
||||
`;
|
||||
|
||||
export default observer(
|
||||
(
|
||||
props: Props &
|
||||
|
|
@ -131,18 +111,6 @@ export default observer(
|
|||
fill={useStatusColour(target)}
|
||||
/>
|
||||
)}
|
||||
{props.voice && (
|
||||
<foreignObject x="22" y="22" width="10" height="10">
|
||||
<VoiceIndicator status={props.voice}>
|
||||
{(props.voice === "deaf" && (
|
||||
<VolumeMute size={6} />
|
||||
)) ||
|
||||
(props.voice === "muted" && (
|
||||
<MicrophoneOff size={6} />
|
||||
))}
|
||||
</VoiceIndicator>
|
||||
</foreignObject>
|
||||
)}
|
||||
</IconBase>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ export default function CreateChannel({
|
|||
title={<Text id="app.context_menu.create_channel" />}
|
||||
schema={{
|
||||
name: "text",
|
||||
type: "radio",
|
||||
}}
|
||||
data={{
|
||||
name: {
|
||||
|
|
@ -30,32 +29,10 @@ export default function CreateChannel({
|
|||
<Text id="app.main.servers.channel_name" />
|
||||
) as React.ReactChild,
|
||||
},
|
||||
type: {
|
||||
field: (
|
||||
<Text id="app.main.servers.channel_type" />
|
||||
) as React.ReactChild,
|
||||
choices: [
|
||||
{
|
||||
name: (
|
||||
<Text id="app.main.servers.text_channel" />
|
||||
) as React.ReactChild,
|
||||
value: "Text",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<Text id="app.main.servers.voice_channel" />
|
||||
) as React.ReactChild,
|
||||
value: "Voice",
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
defaults={{
|
||||
type: "Text",
|
||||
}}
|
||||
callback={async ({ name, type }) => {
|
||||
callback={async ({ name }) => {
|
||||
const channel = await target.createChannel({
|
||||
type: type as "Text" | "Voice",
|
||||
type: "Text",
|
||||
name,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ChevronRight, Trash } from "@styled-icons/boxicons-regular";
|
||||
import { Cog, UserVoice } from "@styled-icons/boxicons-solid";
|
||||
import { Cog } from "@styled-icons/boxicons-solid";
|
||||
import { isFirefox } from "react-device-detect";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Channel, Message, Server, User, API, Permission, UserPermission, Member } from "revolt.js";
|
||||
|
|
@ -163,8 +163,7 @@ export default function ContextMenus() {
|
|||
case "mark_as_read":
|
||||
{
|
||||
if (
|
||||
data.channel.channel_type === "SavedMessages" ||
|
||||
data.channel.channel_type === "VoiceChannel"
|
||||
data.channel.channel_type === "SavedMessages"
|
||||
)
|
||||
return;
|
||||
|
||||
|
|
@ -574,8 +573,7 @@ export default function ContextMenus() {
|
|||
const user = uid ? client.users.get(uid) : undefined;
|
||||
const serverChannel =
|
||||
targetChannel &&
|
||||
(targetChannel.channel_type === "TextChannel" ||
|
||||
targetChannel.channel_type === "VoiceChannel")
|
||||
(targetChannel.channel_type === "TextChannel")
|
||||
? targetChannel
|
||||
: undefined;
|
||||
|
||||
|
|
@ -961,7 +959,7 @@ export default function ContextMenus() {
|
|||
pushDivider();
|
||||
|
||||
if (channel) {
|
||||
if (channel.channel_type !== "VoiceChannel") {
|
||||
if (channel.channel_type) {
|
||||
generateAction(
|
||||
{
|
||||
action: "open_notification_options",
|
||||
|
|
@ -998,7 +996,6 @@ export default function ContextMenus() {
|
|||
});
|
||||
break;
|
||||
case "TextChannel":
|
||||
case "VoiceChannel":
|
||||
if (
|
||||
channelPermissions &
|
||||
Permission.InviteOthers
|
||||
|
|
@ -1275,7 +1272,6 @@ export default function ContextMenus() {
|
|||
<MenuItem
|
||||
data={{ action: "set_status" }}
|
||||
disabled={!isOnline}>
|
||||
<UserVoice size={18} />
|
||||
<Text id={`app.context_menu.custom_status`} />
|
||||
{client.user!.status?.text && (
|
||||
<IconButton>
|
||||
|
|
@ -1293,4 +1289,4 @@ export default function ContextMenus() {
|
|||
<CMNotifications />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,192 +0,0 @@
|
|||
import EventEmitter from "eventemitter3";
|
||||
|
||||
import {
|
||||
RtpCapabilities,
|
||||
RtpParameters,
|
||||
} from "mediasoup-client/lib/RtpParameters";
|
||||
import { DtlsParameters } from "mediasoup-client/lib/Transport";
|
||||
|
||||
import {
|
||||
AuthenticationResult,
|
||||
Room,
|
||||
TransportInitDataTuple,
|
||||
WSCommandType,
|
||||
WSErrorCode,
|
||||
ProduceType,
|
||||
ConsumerData,
|
||||
} from "./Types";
|
||||
|
||||
interface SignalingEvents {
|
||||
open: (event: Event) => void;
|
||||
close: (event: CloseEvent) => void;
|
||||
error: (event: Event) => void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: (data: any) => void;
|
||||
}
|
||||
|
||||
export default class Signaling extends EventEmitter<SignalingEvents> {
|
||||
ws?: WebSocket;
|
||||
index: number;
|
||||
pending: Map<number, (data: unknown) => void>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.index = 0;
|
||||
this.pending = new Map();
|
||||
}
|
||||
|
||||
connected(): boolean {
|
||||
return (
|
||||
this.ws !== undefined &&
|
||||
this.ws.readyState !== WebSocket.CLOSING &&
|
||||
this.ws.readyState !== WebSocket.CLOSED
|
||||
);
|
||||
}
|
||||
|
||||
connect(address: string): Promise<void> {
|
||||
this.disconnect();
|
||||
this.ws = new WebSocket(address);
|
||||
this.ws.onopen = (e) => this.emit("open", e);
|
||||
this.ws.onclose = (e) => this.emit("close", e);
|
||||
this.ws.onerror = (e) => this.emit("error", e);
|
||||
this.ws.onmessage = (e) => this.parseData(e);
|
||||
|
||||
let finished = false;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.once("open", () => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.once("error", () => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
reject();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (
|
||||
this.ws !== undefined &&
|
||||
this.ws.readyState !== WebSocket.CLOSED &&
|
||||
this.ws.readyState !== WebSocket.CLOSING
|
||||
)
|
||||
this.ws.close(1000);
|
||||
}
|
||||
|
||||
private parseData(event: MessageEvent) {
|
||||
if (typeof event.data !== "string") return;
|
||||
const json = JSON.parse(event.data);
|
||||
const entry = this.pending.get(json.id);
|
||||
if (entry === undefined) {
|
||||
this.emit("data", json);
|
||||
return;
|
||||
}
|
||||
|
||||
entry(json);
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
sendRequest(type: string, data?: any): Promise<any> {
|
||||
if (this.ws === undefined || this.ws.readyState !== WebSocket.OPEN)
|
||||
return Promise.reject({ error: WSErrorCode.NotConnected });
|
||||
|
||||
const ws = this.ws;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.index >= 2 ** 32) this.index = 0;
|
||||
while (this.pending.has(this.index)) this.index++;
|
||||
const onClose = (e: CloseEvent) => {
|
||||
reject({
|
||||
error: e.code,
|
||||
message: e.reason,
|
||||
});
|
||||
};
|
||||
|
||||
const finishedFn = (data: any) => {
|
||||
this.removeListener("close", onClose);
|
||||
if (data.error)
|
||||
reject({
|
||||
error: data.error,
|
||||
message: data.message,
|
||||
data: data.data,
|
||||
});
|
||||
resolve(data.data);
|
||||
};
|
||||
|
||||
this.pending.set(this.index, finishedFn);
|
||||
this.once("close", onClose);
|
||||
const json = {
|
||||
id: this.index,
|
||||
type,
|
||||
data,
|
||||
};
|
||||
ws.send(`${JSON.stringify(json)}\n`);
|
||||
this.index++;
|
||||
});
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
authenticate(token: string, roomId: string): Promise<AuthenticationResult> {
|
||||
return this.sendRequest(WSCommandType.Authenticate, { token, roomId });
|
||||
}
|
||||
|
||||
async roomInfo(): Promise<Room> {
|
||||
const room = await this.sendRequest(WSCommandType.RoomInfo);
|
||||
return {
|
||||
id: room.id,
|
||||
videoAllowed: room.videoAllowed,
|
||||
users: new Map(Object.entries(room.users)),
|
||||
};
|
||||
}
|
||||
|
||||
initializeTransports(
|
||||
rtpCapabilities: RtpCapabilities,
|
||||
): Promise<TransportInitDataTuple> {
|
||||
return this.sendRequest(WSCommandType.InitializeTransports, {
|
||||
mode: "SplitWebRTC",
|
||||
rtpCapabilities,
|
||||
});
|
||||
}
|
||||
|
||||
connectTransport(
|
||||
id: string,
|
||||
dtlsParameters: DtlsParameters,
|
||||
): Promise<void> {
|
||||
return this.sendRequest(WSCommandType.ConnectTransport, {
|
||||
id,
|
||||
dtlsParameters,
|
||||
});
|
||||
}
|
||||
|
||||
async startProduce(
|
||||
type: ProduceType,
|
||||
rtpParameters: RtpParameters,
|
||||
): Promise<string> {
|
||||
const result = await this.sendRequest(WSCommandType.StartProduce, {
|
||||
type,
|
||||
rtpParameters,
|
||||
});
|
||||
return result.producerId;
|
||||
}
|
||||
|
||||
stopProduce(type: ProduceType): Promise<void> {
|
||||
return this.sendRequest(WSCommandType.StopProduce, { type });
|
||||
}
|
||||
|
||||
startConsume(userId: string, type: ProduceType): Promise<ConsumerData> {
|
||||
return this.sendRequest(WSCommandType.StartConsume, { type, userId });
|
||||
}
|
||||
|
||||
stopConsume(consumerId: string): Promise<void> {
|
||||
return this.sendRequest(WSCommandType.StopConsume, { id: consumerId });
|
||||
}
|
||||
|
||||
setConsumerPause(consumerId: string, paused: boolean): Promise<void> {
|
||||
return this.sendRequest(WSCommandType.SetConsumerPause, {
|
||||
id: consumerId,
|
||||
paused,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
import { Consumer } from "mediasoup-client/lib/Consumer";
|
||||
import {
|
||||
MediaKind,
|
||||
RtpCapabilities,
|
||||
RtpParameters,
|
||||
} from "mediasoup-client/lib/RtpParameters";
|
||||
import { SctpParameters } from "mediasoup-client/lib/SctpParameters";
|
||||
import {
|
||||
DtlsParameters,
|
||||
IceCandidate,
|
||||
IceParameters,
|
||||
} from "mediasoup-client/lib/Transport";
|
||||
|
||||
export enum WSEventType {
|
||||
UserJoined = "UserJoined",
|
||||
UserLeft = "UserLeft",
|
||||
|
||||
UserStartProduce = "UserStartProduce",
|
||||
UserStopProduce = "UserStopProduce",
|
||||
}
|
||||
|
||||
export enum WSCommandType {
|
||||
Authenticate = "Authenticate",
|
||||
RoomInfo = "RoomInfo",
|
||||
|
||||
InitializeTransports = "InitializeTransports",
|
||||
ConnectTransport = "ConnectTransport",
|
||||
|
||||
StartProduce = "StartProduce",
|
||||
StopProduce = "StopProduce",
|
||||
|
||||
StartConsume = "StartConsume",
|
||||
StopConsume = "StopConsume",
|
||||
SetConsumerPause = "SetConsumerPause",
|
||||
}
|
||||
|
||||
export enum WSErrorCode {
|
||||
NotConnected = 0,
|
||||
NotFound = 404,
|
||||
|
||||
TransportConnectionFailure = 601,
|
||||
|
||||
ProducerFailure = 611,
|
||||
ProducerNotFound = 614,
|
||||
|
||||
ConsumerFailure = 621,
|
||||
ConsumerNotFound = 624,
|
||||
}
|
||||
|
||||
export enum WSCloseCode {
|
||||
// Sent when the received data is not a string, or is unparseable
|
||||
InvalidData = 1003,
|
||||
Unauthorized = 4001,
|
||||
RoomClosed = 4004,
|
||||
// Sent when a client tries to send an opcode in the wrong state
|
||||
InvalidState = 1002,
|
||||
ServerError = 1011,
|
||||
}
|
||||
|
||||
export interface VoiceError {
|
||||
error: WSErrorCode | WSCloseCode;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type ProduceType = "audio"; //| "video" | "saudio" | "svideo";
|
||||
|
||||
export interface AuthenticationResult {
|
||||
userId: string;
|
||||
roomId: string;
|
||||
rtpCapabilities: RtpCapabilities;
|
||||
}
|
||||
|
||||
export interface Room {
|
||||
id: string;
|
||||
videoAllowed: boolean;
|
||||
users: Map<string, VoiceUser>;
|
||||
}
|
||||
|
||||
export interface VoiceUser {
|
||||
audio?: boolean;
|
||||
//video?: boolean,
|
||||
//saudio?: boolean,
|
||||
//svideo?: boolean,
|
||||
}
|
||||
|
||||
export interface ConsumerList {
|
||||
audio?: Consumer;
|
||||
//video?: Consumer,
|
||||
//saudio?: Consumer,
|
||||
//svideo?: Consumer,
|
||||
}
|
||||
|
||||
export interface TransportInitData {
|
||||
id: string;
|
||||
iceParameters: IceParameters;
|
||||
iceCandidates: IceCandidate[];
|
||||
dtlsParameters: DtlsParameters;
|
||||
sctpParameters: SctpParameters | undefined;
|
||||
}
|
||||
|
||||
export interface TransportInitDataTuple {
|
||||
sendTransport: TransportInitData;
|
||||
recvTransport: TransportInitData;
|
||||
}
|
||||
|
||||
export interface ConsumerData {
|
||||
id: string;
|
||||
producerId: string;
|
||||
kind: MediaKind;
|
||||
rtpParameters: RtpParameters;
|
||||
}
|
||||
|
|
@ -1,340 +0,0 @@
|
|||
import EventEmitter from "eventemitter3";
|
||||
import * as mediasoupClient from "mediasoup-client";
|
||||
import { types } from "mediasoup-client";
|
||||
|
||||
import { Device, Producer, Transport } from "mediasoup-client/lib/types";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
|
||||
import Signaling from "./Signaling";
|
||||
import {
|
||||
ProduceType,
|
||||
WSEventType,
|
||||
VoiceError,
|
||||
VoiceUser,
|
||||
ConsumerList,
|
||||
WSErrorCode,
|
||||
} from "./Types";
|
||||
|
||||
const UnsupportedError = types.UnsupportedError;
|
||||
|
||||
interface VoiceEvents {
|
||||
ready: () => void;
|
||||
error: (error: Error) => void;
|
||||
close: (error?: VoiceError) => void;
|
||||
|
||||
startProduce: (type: ProduceType) => void;
|
||||
stopProduce: (type: ProduceType) => void;
|
||||
|
||||
userJoined: (userId: string) => void;
|
||||
userLeft: (userId: string) => void;
|
||||
|
||||
userStartProduce: (userId: string, type: ProduceType) => void;
|
||||
userStopProduce: (userId: string, type: ProduceType) => void;
|
||||
}
|
||||
|
||||
export default class VoiceClient extends EventEmitter<VoiceEvents> {
|
||||
private _supported: boolean;
|
||||
|
||||
device?: Device;
|
||||
signaling: Signaling;
|
||||
|
||||
sendTransport?: Transport;
|
||||
recvTransport?: Transport;
|
||||
|
||||
isDeaf?: boolean;
|
||||
|
||||
userId?: string;
|
||||
roomId?: string;
|
||||
participants: Map<string, VoiceUser>;
|
||||
consumers: Map<string, ConsumerList>;
|
||||
|
||||
audioProducer?: Producer;
|
||||
constructor() {
|
||||
super();
|
||||
this._supported = mediasoupClient.detectDevice() !== undefined;
|
||||
this.signaling = new Signaling();
|
||||
|
||||
this.participants = new Map();
|
||||
this.consumers = new Map();
|
||||
|
||||
this.isDeaf = false;
|
||||
|
||||
const state = useApplicationState();
|
||||
|
||||
this.signaling.on(
|
||||
"data",
|
||||
(json) => {
|
||||
const data = json.data;
|
||||
switch (json.type) {
|
||||
case WSEventType.UserJoined: {
|
||||
this.participants.set(data.id, {});
|
||||
state.settings.sounds.playSound("call_join");
|
||||
this.emit("userJoined", data.id);
|
||||
break;
|
||||
}
|
||||
case WSEventType.UserLeft: {
|
||||
this.participants.delete(data.id);
|
||||
state.settings.sounds.playSound("call_leave");
|
||||
this.emit("userLeft", data.id);
|
||||
|
||||
if (this.recvTransport) this.stopConsume(data.id);
|
||||
break;
|
||||
}
|
||||
case WSEventType.UserStartProduce: {
|
||||
const user = this.participants.get(data.id);
|
||||
if (user === undefined) return;
|
||||
switch (data.type) {
|
||||
case "audio":
|
||||
user.audio = true;
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid produce type ${data.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.recvTransport)
|
||||
this.startConsume(data.id, data.type);
|
||||
this.emit("userStartProduce", data.id, data.type);
|
||||
break;
|
||||
}
|
||||
case WSEventType.UserStopProduce: {
|
||||
const user = this.participants.get(data.id);
|
||||
if (user === undefined) return;
|
||||
switch (data.type) {
|
||||
case "audio":
|
||||
user.audio = false;
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid produce type ${data.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.recvTransport)
|
||||
this.stopConsume(data.id, data.type);
|
||||
this.emit("userStopProduce", data.id, data.type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
this,
|
||||
);
|
||||
|
||||
this.signaling.on(
|
||||
"error",
|
||||
() => {
|
||||
this.emit("error", new Error("Signaling error"));
|
||||
},
|
||||
this,
|
||||
);
|
||||
|
||||
this.signaling.on(
|
||||
"close",
|
||||
(error) => {
|
||||
this.disconnect(
|
||||
{
|
||||
error: error.code,
|
||||
message: error.reason,
|
||||
},
|
||||
true,
|
||||
);
|
||||
},
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
supported() {
|
||||
return this._supported;
|
||||
}
|
||||
throwIfUnsupported() {
|
||||
if (!this._supported) throw new UnsupportedError("RTC not supported");
|
||||
}
|
||||
|
||||
connect(address: string, roomId: string) {
|
||||
this.throwIfUnsupported();
|
||||
this.device = new Device();
|
||||
this.roomId = roomId;
|
||||
return this.signaling.connect(address);
|
||||
}
|
||||
|
||||
disconnect(error?: VoiceError, ignoreDisconnected?: boolean) {
|
||||
if (!this.signaling.connected() && !ignoreDisconnected) return;
|
||||
this.signaling.disconnect();
|
||||
this.participants = new Map();
|
||||
this.consumers = new Map();
|
||||
this.userId = undefined;
|
||||
this.roomId = undefined;
|
||||
|
||||
this.audioProducer = undefined;
|
||||
|
||||
if (this.sendTransport) this.sendTransport.close();
|
||||
if (this.recvTransport) this.recvTransport.close();
|
||||
this.sendTransport = undefined;
|
||||
this.recvTransport = undefined;
|
||||
|
||||
this.emit("close", error);
|
||||
}
|
||||
|
||||
async authenticate(token: string) {
|
||||
this.throwIfUnsupported();
|
||||
if (this.device === undefined || this.roomId === undefined)
|
||||
throw new ReferenceError("Voice Client is in an invalid state");
|
||||
const result = await this.signaling.authenticate(token, this.roomId);
|
||||
const [room] = await Promise.all([
|
||||
this.signaling.roomInfo(),
|
||||
this.device.load({ routerRtpCapabilities: result.rtpCapabilities }),
|
||||
]);
|
||||
|
||||
this.userId = result.userId;
|
||||
this.participants = room.users;
|
||||
}
|
||||
|
||||
async initializeTransports() {
|
||||
this.throwIfUnsupported();
|
||||
if (this.device === undefined)
|
||||
throw new ReferenceError("Voice Client is in an invalid state");
|
||||
const initData = await this.signaling.initializeTransports(
|
||||
this.device.rtpCapabilities,
|
||||
);
|
||||
|
||||
this.sendTransport = this.device.createSendTransport(
|
||||
initData.sendTransport,
|
||||
);
|
||||
this.recvTransport = this.device.createRecvTransport(
|
||||
initData.recvTransport,
|
||||
);
|
||||
|
||||
const connectTransport = (transport: Transport) => {
|
||||
transport.on("connect", ({ dtlsParameters }, callback, errback) => {
|
||||
this.signaling
|
||||
.connectTransport(transport.id, dtlsParameters)
|
||||
.then(callback)
|
||||
.catch(errback);
|
||||
});
|
||||
};
|
||||
|
||||
connectTransport(this.sendTransport);
|
||||
connectTransport(this.recvTransport);
|
||||
|
||||
this.sendTransport.on("produce", (parameters, callback, errback) => {
|
||||
const type = parameters.appData.type;
|
||||
if (
|
||||
parameters.kind === "audio" &&
|
||||
type !== "audio" &&
|
||||
type !== "saudio"
|
||||
)
|
||||
return errback();
|
||||
if (
|
||||
parameters.kind === "video" &&
|
||||
type !== "video" &&
|
||||
type !== "svideo"
|
||||
)
|
||||
return errback();
|
||||
this.signaling
|
||||
.startProduce(type, parameters.rtpParameters)
|
||||
.then((id) => callback({ id }))
|
||||
.catch(errback);
|
||||
});
|
||||
|
||||
this.emit("ready");
|
||||
for (const user of this.participants) {
|
||||
if (user[1].audio && user[0] !== this.userId)
|
||||
this.startConsume(user[0], "audio");
|
||||
}
|
||||
}
|
||||
|
||||
private async startConsume(userId: string, type: ProduceType) {
|
||||
if (this.recvTransport === undefined)
|
||||
throw new Error("Receive transport undefined");
|
||||
const consumers = this.consumers.get(userId) || {};
|
||||
const consumerParams = await this.signaling.startConsume(userId, type);
|
||||
const consumer = await this.recvTransport.consume(consumerParams);
|
||||
switch (type) {
|
||||
case "audio":
|
||||
consumers.audio = consumer;
|
||||
}
|
||||
|
||||
const mediaStream = new MediaStream([consumer.track]);
|
||||
const audio = new Audio();
|
||||
audio.srcObject = mediaStream;
|
||||
await this.signaling.setConsumerPause(consumer.id, false);
|
||||
audio.play();
|
||||
this.consumers.set(userId, consumers);
|
||||
}
|
||||
|
||||
private async stopConsume(userId: string, type?: ProduceType) {
|
||||
const consumers = this.consumers.get(userId);
|
||||
if (consumers === undefined) return;
|
||||
if (type === undefined) {
|
||||
if (consumers.audio !== undefined) consumers.audio.close();
|
||||
this.consumers.delete(userId);
|
||||
} else {
|
||||
switch (type) {
|
||||
case "audio": {
|
||||
if (consumers.audio !== undefined) {
|
||||
consumers.audio.close();
|
||||
this.signaling.stopConsume(consumers.audio.id);
|
||||
}
|
||||
consumers.audio = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.consumers.set(userId, consumers);
|
||||
}
|
||||
}
|
||||
|
||||
async startProduce(track: MediaStreamTrack, type: ProduceType) {
|
||||
if (this.sendTransport === undefined)
|
||||
throw new Error("Send transport undefined");
|
||||
const producer = await this.sendTransport.produce({
|
||||
track,
|
||||
appData: { type },
|
||||
});
|
||||
|
||||
switch (type) {
|
||||
case "audio":
|
||||
this.audioProducer = producer;
|
||||
break;
|
||||
}
|
||||
|
||||
const participant = this.participants.get(this.userId || "");
|
||||
if (participant !== undefined) {
|
||||
participant[type] = true;
|
||||
this.participants.set(this.userId || "", participant);
|
||||
}
|
||||
|
||||
this.emit("startProduce", type);
|
||||
}
|
||||
|
||||
async stopProduce(type: ProduceType) {
|
||||
let producer;
|
||||
switch (type) {
|
||||
case "audio":
|
||||
producer = this.audioProducer;
|
||||
this.audioProducer = undefined;
|
||||
break;
|
||||
}
|
||||
|
||||
if (producer !== undefined) {
|
||||
producer.close();
|
||||
this.emit("stopProduce", type);
|
||||
}
|
||||
|
||||
const participant = this.participants.get(this.userId || "");
|
||||
if (participant !== undefined) {
|
||||
participant[type] = false;
|
||||
this.participants.set(this.userId || "", participant);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.signaling.stopProduce(type);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line
|
||||
if ((error as any).error === WSErrorCode.ProducerNotFound) return;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
import { action, makeAutoObservable, runInAction } from "mobx";
|
||||
import { Channel, Nullable, toNullable } from "revolt.js";
|
||||
|
||||
import type { ProduceType, VoiceUser } from "./Types";
|
||||
import type VoiceClient from "./VoiceClient";
|
||||
|
||||
export enum VoiceStatus {
|
||||
LOADING = 0,
|
||||
UNAVAILABLE,
|
||||
ERRORED,
|
||||
READY = 3,
|
||||
CONNECTING = 4,
|
||||
UNLOADED = 5,
|
||||
AUTHENTICATING,
|
||||
RTC_CONNECTING,
|
||||
CONNECTED,
|
||||
// RECONNECTING
|
||||
}
|
||||
|
||||
// This is an example of how to implement MobX state.
|
||||
// * Note for better implementation:
|
||||
// * MobX state should be implemented on the VoiceClient itself.
|
||||
class VoiceStateReference {
|
||||
client?: VoiceClient;
|
||||
connecting?: boolean;
|
||||
|
||||
status: VoiceStatus;
|
||||
roomId: Nullable<string>;
|
||||
participants: Map<string, VoiceUser>;
|
||||
|
||||
constructor() {
|
||||
this.roomId = null;
|
||||
this.status = VoiceStatus.UNLOADED;
|
||||
this.participants = new Map();
|
||||
|
||||
this.syncState = this.syncState.bind(this);
|
||||
this.connect = this.connect.bind(this);
|
||||
this.disconnect = this.disconnect.bind(this);
|
||||
|
||||
makeAutoObservable(this, {
|
||||
client: false,
|
||||
connecting: false,
|
||||
});
|
||||
}
|
||||
|
||||
// This takes information from the voice
|
||||
// client and applies it to the state here.
|
||||
@action syncState() {
|
||||
if (!this.client) return;
|
||||
this.roomId = toNullable(this.client.roomId);
|
||||
this.participants.clear();
|
||||
this.client.participants.forEach((v, k) => this.participants.set(k, v));
|
||||
}
|
||||
|
||||
// This imports and constructs the voice client.
|
||||
@action async loadVoice() {
|
||||
if (this.status !== VoiceStatus.UNLOADED) return;
|
||||
this.status = VoiceStatus.LOADING;
|
||||
|
||||
try {
|
||||
const { default: VoiceClient } = await import("./VoiceClient");
|
||||
const client = new VoiceClient();
|
||||
|
||||
client.on("startProduce", this.syncState);
|
||||
client.on("stopProduce", this.syncState);
|
||||
client.on("userJoined", this.syncState);
|
||||
client.on("userLeft", this.syncState);
|
||||
client.on("userStartProduce", this.syncState);
|
||||
client.on("userStopProduce", this.syncState);
|
||||
|
||||
runInAction(() => {
|
||||
if (!client.supported()) {
|
||||
this.status = VoiceStatus.UNAVAILABLE;
|
||||
} else {
|
||||
this.status = VoiceStatus.READY;
|
||||
this.client = client;
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to load voice library!", err);
|
||||
runInAction(() => {
|
||||
this.status = VoiceStatus.UNAVAILABLE;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to a voice channel.
|
||||
@action async connect(channel: Channel) {
|
||||
if (!this.client?.supported()) throw new Error("RTC is unavailable");
|
||||
|
||||
this.connecting = true;
|
||||
this.status = VoiceStatus.CONNECTING;
|
||||
|
||||
try {
|
||||
const call = await channel.joinCall();
|
||||
|
||||
await this.client.connect(
|
||||
channel.client.configuration!.features.voso.ws,
|
||||
channel._id,
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
this.status = VoiceStatus.AUTHENTICATING;
|
||||
});
|
||||
|
||||
await this.client.authenticate(call.token);
|
||||
this.syncState();
|
||||
|
||||
runInAction(() => {
|
||||
this.status = VoiceStatus.RTC_CONNECTING;
|
||||
});
|
||||
|
||||
await this.client.initializeTransports();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
runInAction(() => {
|
||||
this.status = VoiceStatus.READY;
|
||||
});
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
this.status = VoiceStatus.CONNECTED;
|
||||
this.connecting = false;
|
||||
});
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
// Disconnect from current channel.
|
||||
@action disconnect() {
|
||||
this.connecting = false;
|
||||
this.status = VoiceStatus.READY;
|
||||
|
||||
this.client?.disconnect();
|
||||
this.syncState();
|
||||
}
|
||||
|
||||
isProducing(type: ProduceType) {
|
||||
switch (type) {
|
||||
case "audio":
|
||||
return this.client?.audioProducer !== undefined;
|
||||
}
|
||||
}
|
||||
|
||||
isDeaf() {
|
||||
if (!this.client) return false;
|
||||
|
||||
return this.client.isDeaf;
|
||||
}
|
||||
|
||||
async startDeafen() {
|
||||
if (!this.client) return console.log("No client object"); // ! TODO: let the user know
|
||||
if (this.client.isDeaf) return;
|
||||
|
||||
this.client.isDeaf = true;
|
||||
this.client?.consumers.forEach((consumer) => {
|
||||
consumer.audio?.pause();
|
||||
});
|
||||
|
||||
this.syncState();
|
||||
}
|
||||
async stopDeafen() {
|
||||
if (!this.client) return console.log("No client object"); // ! TODO: let the user know
|
||||
if (!this.client.isDeaf) return;
|
||||
|
||||
this.client.isDeaf = false;
|
||||
this.client?.consumers.forEach((consumer) => {
|
||||
consumer.audio?.resume();
|
||||
});
|
||||
|
||||
this.syncState();
|
||||
}
|
||||
|
||||
async startProducing(type: ProduceType) {
|
||||
switch (type) {
|
||||
case "audio": {
|
||||
if (this.client?.audioProducer !== undefined)
|
||||
return console.log("No audio producer."); // ! TODO: let the user know
|
||||
|
||||
if (navigator.mediaDevices === undefined)
|
||||
return console.log("No media devices."); // ! TODO: let the user know
|
||||
|
||||
const mediaDevice =
|
||||
window.localStorage.getItem("audioInputDevice");
|
||||
|
||||
const mediaStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: mediaDevice ? { deviceId: mediaDevice } : true,
|
||||
});
|
||||
|
||||
await this.client?.startProduce(
|
||||
mediaStream.getAudioTracks()[0],
|
||||
"audio",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.syncState();
|
||||
}
|
||||
|
||||
async stopProducing(type: ProduceType) {
|
||||
await this.client?.stopProduce(type);
|
||||
|
||||
this.syncState();
|
||||
}
|
||||
}
|
||||
|
||||
export const voiceState = new VoiceStateReference();
|
||||
|
|
@ -30,7 +30,6 @@ export const DEFAULT_STATES: {
|
|||
DirectMessage: "all",
|
||||
Group: "all",
|
||||
TextChannel: undefined!,
|
||||
VoiceChannel: undefined!,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import { PageHeader } from "../../components/ui/Header";
|
|||
import { useClient } from "../../controllers/client/ClientController";
|
||||
import ChannelHeader from "./ChannelHeader";
|
||||
import { MessageArea } from "./messaging/MessageArea";
|
||||
import VoiceHeader from "./voice/VoiceHeader";
|
||||
|
||||
const ChannelMain = styled.div.attrs({ "data-component": "channel" })`
|
||||
flex-grow: 1;
|
||||
|
|
@ -126,9 +125,6 @@ export const Channel = observer(
|
|||
}
|
||||
|
||||
const channel = client.channels.get(id)!;
|
||||
if (channel.channel_type === "VoiceChannel") {
|
||||
return <VoiceChannel channel={channel} />;
|
||||
}
|
||||
|
||||
return <TextChannel channel={channel} />;
|
||||
},
|
||||
|
|
@ -191,7 +187,6 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
|
|||
<ChannelMain>
|
||||
<ErrorBoundary section="renderer">
|
||||
<ChannelContent>
|
||||
<VoiceHeader id={channel._id} />
|
||||
<NewMessages channel={channel} last_id={lastId} />
|
||||
<MessageArea channel={channel} last_id={lastId} />
|
||||
<TypingIndicator channel={channel} />
|
||||
|
|
@ -208,15 +203,6 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
|
|||
);
|
||||
});
|
||||
|
||||
function VoiceChannel({ channel }: { channel: ChannelI }) {
|
||||
return (
|
||||
<>
|
||||
<ChannelHeader channel={channel} />
|
||||
<VoiceHeader id={channel._id} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ChannelPlaceholder() {
|
||||
return (
|
||||
<PlaceholderBase>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import { IconButton } from "@revoltchat/ui";
|
|||
import { chainedDefer, defer } from "../../../lib/defer";
|
||||
import { internalEmit } from "../../../lib/eventEmitter";
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
import { voiceState, VoiceStatus } from "../../../lib/vortex/VoiceState";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { SIDEBAR_MEMBERS } from "../../../mobx/stores/Layout";
|
||||
|
|
@ -132,7 +131,6 @@ export default function HeaderActions({ channel }: ChannelHeaderProps) {
|
|||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
<VoiceActions channel={channel} />
|
||||
{(channel.channel_type === "Group" ||
|
||||
channel.channel_type === "TextChannel") && (
|
||||
<IconButton onClick={openMembers}>
|
||||
|
|
|
|||
|
|
@ -1,179 +0,0 @@
|
|||
import {
|
||||
BarChartAlt2,
|
||||
Microphone,
|
||||
MicrophoneOff,
|
||||
PhoneOff,
|
||||
VolumeFull,
|
||||
VolumeMute,
|
||||
} from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import styled from "styled-components/macro";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useMemo } from "preact/hooks";
|
||||
|
||||
import { Button } from "@revoltchat/ui";
|
||||
|
||||
import { voiceState, VoiceStatus } from "../../../lib/vortex/VoiceState";
|
||||
|
||||
import Tooltip from "../../../components/common/Tooltip";
|
||||
import UserIcon from "../../../components/common/user/UserIcon";
|
||||
import { useClient } from "../../../controllers/client/ClientController";
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const VoiceBase = styled.div`
|
||||
margin-top: 48px;
|
||||
padding: 20px;
|
||||
background: var(--secondary-background);
|
||||
flex-grow: 1;
|
||||
|
||||
.status {
|
||||
flex: 1 0;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
align-items: center;
|
||||
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
gap: 6px;
|
||||
|
||||
color: var(--success);
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--primary-background);
|
||||
|
||||
svg {
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.participants {
|
||||
margin: 40px 20px;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
gap: 16px;
|
||||
|
||||
div:hover img {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.disconnected {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default observer(({ id }: Props) => {
|
||||
if (voiceState.roomId !== id) return null;
|
||||
|
||||
const client = useClient();
|
||||
const self = client.users.get(client.user!._id);
|
||||
|
||||
const keys = Array.from(voiceState.participants.keys());
|
||||
const users = useMemo(() => {
|
||||
return keys.map((key) => client.users.get(key));
|
||||
// eslint-disable-next-line
|
||||
}, [keys]);
|
||||
|
||||
return (
|
||||
<VoiceBase>
|
||||
<div className="participants">
|
||||
{users && users.length !== 0
|
||||
? users.map((user, index) => {
|
||||
const user_id = keys![index];
|
||||
return (
|
||||
<div key={user_id}>
|
||||
<UserIcon
|
||||
size={80}
|
||||
target={user}
|
||||
status={false}
|
||||
voice={
|
||||
client.user?._id === user_id &&
|
||||
voiceState.isDeaf()
|
||||
? "deaf"
|
||||
: voiceState.participants!.get(
|
||||
user_id,
|
||||
)?.audio
|
||||
? undefined
|
||||
: "muted"
|
||||
}
|
||||
onClick={() =>
|
||||
modalController.push({
|
||||
type: "user_profile",
|
||||
user_id,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: self !== undefined && (
|
||||
<div key={self._id} className="disconnected">
|
||||
<UserIcon
|
||||
size={80}
|
||||
target={self}
|
||||
status={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="status">
|
||||
<BarChartAlt2 size={16} />
|
||||
{voiceState.status === VoiceStatus.CONNECTED && (
|
||||
<Text id="app.main.channel.voice.connected" />
|
||||
)}
|
||||
</div>
|
||||
<div className="actions">
|
||||
<Tooltip content={"Leave call"} placement={"top"}>
|
||||
<Button palette="error" onClick={voiceState.disconnect}>
|
||||
<PhoneOff width={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{voiceState.isProducing("audio") ? (
|
||||
<Tooltip content={"Mute microphone"} placement={"top"}>
|
||||
<Button
|
||||
onClick={() => voiceState.stopProducing("audio")}>
|
||||
<Microphone width={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip content={"Unmute microphone"} placement={"top"}>
|
||||
<Button
|
||||
onClick={() => voiceState.startProducing("audio")}>
|
||||
<MicrophoneOff width={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{voiceState.isDeaf() ? (
|
||||
<Tooltip content={"Undeafen"} placement={"top"}>
|
||||
<Button onClick={() => voiceState.stopDeafen()}>
|
||||
<VolumeMute width={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip content={"Deafen"} placement={"top"}>
|
||||
<Button onClick={() => voiceState.startDeafen()}>
|
||||
<VolumeFull width={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</VoiceBase>
|
||||
);
|
||||
});
|
||||
|
|
@ -12,7 +12,6 @@ import { Text } from "preact-i18n";
|
|||
import { IconButton } from "@revoltchat/ui";
|
||||
|
||||
import { stopPropagation } from "../../lib/stopPropagation";
|
||||
import { voiceState } from "../../lib/vortex/VoiceState";
|
||||
|
||||
import UserIcon from "../../components/common/user/UserIcon";
|
||||
import UserStatus from "../../components/common/user/UserStatus";
|
||||
|
|
@ -32,20 +31,6 @@ export const Friend = observer(({ user }: Props) => {
|
|||
subtext = <UserStatus user={user} />;
|
||||
actions.push(
|
||||
<>
|
||||
<IconButton
|
||||
shape="circle"
|
||||
className={classNames(styles.button, styles.success)}
|
||||
onClick={(ev) =>
|
||||
stopPropagation(
|
||||
ev,
|
||||
user
|
||||
.openDM()
|
||||
.then(voiceState.connect)
|
||||
.then((x) => history.push(`/channel/${x._id}`)),
|
||||
)
|
||||
}>
|
||||
<PhoneCall size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
shape="circle"
|
||||
className={styles.button}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ import { APP_VERSION } from "../../version";
|
|||
import { GenericSettings } from "./GenericSettings";
|
||||
import { Account } from "./panes/Account";
|
||||
import { Appearance } from "./panes/Appearance";
|
||||
import { Audio } from "./panes/Audio";
|
||||
import { ExperimentsPage } from "./panes/Experiments";
|
||||
import { Feedback } from "./panes/Feedback";
|
||||
import { Languages } from "./panes/Languages";
|
||||
|
|
@ -155,14 +154,6 @@ export default observer(() => {
|
|||
icon: <CheckShield size={20} />,
|
||||
title: <Text id="app.settings.pages.sessions.title" />,
|
||||
},
|
||||
{
|
||||
category: (
|
||||
<Text id="app.settings.categories.client_settings" />
|
||||
),
|
||||
id: "audio",
|
||||
icon: <Speaker size={20} />,
|
||||
title: <Text id="app.settings.pages.audio.title" />,
|
||||
},
|
||||
{
|
||||
id: "appearance",
|
||||
icon: <Palette size={20} />,
|
||||
|
|
@ -229,9 +220,6 @@ export default observer(() => {
|
|||
<Route path="/settings/plugins">
|
||||
<PluginsPage />
|
||||
</Route>
|
||||
<Route path="/settings/audio">
|
||||
<Audio />
|
||||
</Route>
|
||||
<Route path="/settings/notifications">
|
||||
<Notifications />
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -1,272 +0,0 @@
|
|||
import styles from "./Panes.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { Button, Category, ComboBox, Tip } from "@revoltchat/ui";
|
||||
|
||||
import { stopPropagation } from "../../../lib/stopPropagation";
|
||||
import { voiceState } from "../../../lib/vortex/VoiceState";
|
||||
|
||||
import { I18nError } from "../../../context/Locale";
|
||||
|
||||
import opusSVG from "../assets/opus_logo.svg";
|
||||
|
||||
{
|
||||
/*import OpusSVG from "../assets/opus_logo.svg";*/
|
||||
}
|
||||
|
||||
const constraints = { audio: true };
|
||||
|
||||
// TODO: do not rewrite this code until voice is rewritten!
|
||||
|
||||
export function Audio() {
|
||||
const [mediaStream, setMediaStream] = useState<MediaStream | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [mediaDevices, setMediaDevices] = useState<
|
||||
MediaDeviceInfo[] | undefined
|
||||
>(undefined);
|
||||
const [permission, setPermission] = useState<PermissionState | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [error, setError] = useState<DOMException | undefined>(undefined);
|
||||
|
||||
const askOrGetPermission = async () => {
|
||||
try {
|
||||
const result = await navigator.mediaDevices.getUserMedia(
|
||||
constraints,
|
||||
);
|
||||
|
||||
setMediaStream(result);
|
||||
} catch (err) {
|
||||
// The user has blocked the permission
|
||||
setError(err as DOMException);
|
||||
}
|
||||
|
||||
try {
|
||||
const { state } = await navigator.permissions.query({
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore: very few browsers accept this `PermissionName`, but it has been drafted in https://www.w3.org/TR/permissions/#powerful-features-registry
|
||||
name: "microphone",
|
||||
});
|
||||
|
||||
setPermission(state);
|
||||
} catch (err) {
|
||||
// the browser might not support `query` functionnality or `PermissionName`
|
||||
// nothing to do
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (mediaStream) {
|
||||
// close microphone access on unmount
|
||||
mediaStream.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [mediaStream]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mediaStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.mediaDevices.enumerateDevices().then(
|
||||
(devices) => {
|
||||
setMediaDevices(devices);
|
||||
},
|
||||
(err) => {
|
||||
setError(err as DOMException);
|
||||
},
|
||||
);
|
||||
}, [mediaStream]);
|
||||
|
||||
const handleAskForPermission = (
|
||||
ev: JSX.TargetedMouseEvent<HTMLElement>,
|
||||
) => {
|
||||
stopPropagation(ev);
|
||||
setError(undefined);
|
||||
askOrGetPermission();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.audio}>
|
||||
<Tip palette="warning">
|
||||
<span>
|
||||
We are currently{" "}
|
||||
<a
|
||||
style={{ color: "inherit", fontWeight: "600" }}
|
||||
href="https://github.com/revoltchat/frontend/issues/14"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
rebuilding the client
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a
|
||||
style={{ color: "inherit", fontWeight: "600" }}
|
||||
href="https://trello.com/c/Ay6KdiOV/1-voice-overhaul-and-video-calling"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
the voice server
|
||||
</a>{" "}
|
||||
from scratch.
|
||||
<br />
|
||||
<br />
|
||||
The old voice should work in most cases, but it may
|
||||
inexplicably not connect in some scenarios and / or
|
||||
exhibit weird behaviour.
|
||||
</span>
|
||||
</Tip>
|
||||
|
||||
{!permission && (
|
||||
<Tip palette="error">
|
||||
<Text id="app.settings.pages.audio.tip_grant_permission" />
|
||||
</Tip>
|
||||
)}
|
||||
|
||||
{error && permission === "prompt" && (
|
||||
<Tip palette="error">
|
||||
<Text id="app.settings.pages.audio.tip_retry" />
|
||||
<a onClick={handleAskForPermission}>
|
||||
<Text id="app.settings.pages.audio.button_retry" />
|
||||
</a>
|
||||
.
|
||||
</Tip>
|
||||
)}
|
||||
|
||||
<div className={styles.audioRow}>
|
||||
<div className={styles.select}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.audio.input_device" />
|
||||
</h3>
|
||||
<div className={styles.audioBox}>
|
||||
<ComboBox
|
||||
value={
|
||||
window.localStorage.getItem(
|
||||
"audioInputDevice",
|
||||
) ?? 0
|
||||
}
|
||||
onChange={(e) =>
|
||||
changeAudioDevice(
|
||||
e.currentTarget.value,
|
||||
"input",
|
||||
)
|
||||
}>
|
||||
{mediaDevices
|
||||
?.filter(
|
||||
(device) =>
|
||||
device.kind === "audioinput",
|
||||
)
|
||||
.map((device) => {
|
||||
return (
|
||||
<option
|
||||
value={device.deviceId}
|
||||
key={device.deviceId}>
|
||||
{device.label || (
|
||||
<Text id="app.settings.pages.audio.device_label_NA" />
|
||||
)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</ComboBox>
|
||||
{/*TOFIX: add logic to sound notches*/}
|
||||
{/*<div className={styles.notches}>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>*/}
|
||||
{!permission && (
|
||||
<Button
|
||||
compact
|
||||
onClick={(e: any) =>
|
||||
handleAskForPermission(e)
|
||||
}
|
||||
palette="error">
|
||||
<Text id="app.settings.pages.audio.button_grant" />
|
||||
</Button>
|
||||
)}
|
||||
{error && error.name === "NotAllowedError" && (
|
||||
<Category>
|
||||
<I18nError error="AudioPermissionBlock" />
|
||||
</Category>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.select}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.audio.output_device" />
|
||||
</h3>
|
||||
{/* TOFIX: create audio output combobox*/}
|
||||
<ComboBox
|
||||
value={
|
||||
window.localStorage.getItem(
|
||||
"audioOutputDevice",
|
||||
) ?? 0
|
||||
}
|
||||
onChange={(e) =>
|
||||
changeAudioDevice(
|
||||
e.currentTarget.value,
|
||||
"output",
|
||||
)
|
||||
}>
|
||||
{mediaDevices
|
||||
?.filter(
|
||||
(device) => device.kind === "audiooutput",
|
||||
)
|
||||
.map((device) => {
|
||||
return (
|
||||
<option
|
||||
value={device.deviceId}
|
||||
key={device.deviceId}>
|
||||
{device.label || (
|
||||
<Text id="app.settings.pages.audio.device_label_NA" />
|
||||
)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</ComboBox>
|
||||
{/*<div className={styles.notches}>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>*/}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div className={styles.opus}>
|
||||
<img height="20" src={opusSVG} draggable={false} />
|
||||
Audio codec powered by Opus
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function changeAudioDevice(deviceId: string, deviceType: string) {
|
||||
if (deviceType === "input") {
|
||||
window.localStorage.setItem("audioInputDevice", deviceId);
|
||||
if (voiceState.isProducing("audio")) {
|
||||
voiceState.stopProducing("audio");
|
||||
voiceState.startProducing("audio");
|
||||
}
|
||||
} else if (deviceType === "output") {
|
||||
window.localStorage.setItem("audioOutputDevice", deviceId);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue