video first pass
parent
91a81d6699
commit
7bea8c08ca
|
|
@ -47,10 +47,23 @@ export default function App() {
|
|||
--neutral-7: #9ea0a6;
|
||||
--neutral-8: #cbcccc;
|
||||
--neutral-9: #f8f8f2;
|
||||
|
||||
--green: #4db560;
|
||||
}
|
||||
a {
|
||||
color: var(--cyan);
|
||||
}
|
||||
fieldset {
|
||||
margin: 8px;
|
||||
border-radius: 16px;
|
||||
border-style: solid;
|
||||
}
|
||||
legend {
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
border-radius: 16px;
|
||||
padding: 0px 8px;
|
||||
}
|
||||
`}</style>
|
||||
<div style={{
|
||||
background: transparent ? 'rgba(0, 0, 0, 0)' : 'var(--neutral-3)',
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ export default function Router(props: RouterProps) {
|
|||
}}>
|
||||
<div style={{
|
||||
height: '50%',
|
||||
width: '100%'
|
||||
width: '100%',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
<Voice></Voice>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
export function Audio(props: React.AudioHTMLAttributes<HTMLAudioElement> & {
|
||||
srcObject?: MediaStream;
|
||||
}) {
|
||||
|
||||
const [ref, setRef] = useState<HTMLAudioElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref === null)
|
||||
return;
|
||||
if (props.srcObject === undefined || props.srcObject === null)
|
||||
return;
|
||||
ref.srcObject = props.srcObject;
|
||||
}, [props.srcObject, ref]);
|
||||
|
||||
const filteredProps = Object.fromEntries(Object.entries(props).filter(([key, value]) => key !== 'srcObject'));
|
||||
|
||||
return <audio ref={setRef} {...filteredProps}>{props.children}</audio>;
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import { IoMdSettings } from 'react-icons/io';
|
|||
import useHover from "../hooks/useHover";
|
||||
import { useContext } from "react";
|
||||
import { SettingsContext } from "../contexts/EphemeralState/EphemeralState";
|
||||
import { ClientsListContext } from "../contexts/EphemeralState/ClientsListState";
|
||||
import useClientId from "../hooks/useClientId";
|
||||
|
||||
export default function Sidebar() {
|
||||
|
||||
|
|
@ -43,6 +45,10 @@ function TopSidebar() {
|
|||
}
|
||||
|
||||
function MiniProfile() {
|
||||
|
||||
const { clientName } = useContext(ClientsListContext);
|
||||
const { clientId } = useClientId();
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
|
|
@ -62,7 +68,7 @@ function MiniProfile() {
|
|||
<div style={{
|
||||
fontWeight: '400',
|
||||
fontSize: '15px',
|
||||
}}>Valerie</div>
|
||||
}}>{clientId && clientName[clientId]}</div>
|
||||
<div style={{
|
||||
fontWeight: '300',
|
||||
fontSize: '13px',
|
||||
|
|
|
|||
|
|
@ -1,42 +1,238 @@
|
|||
import { useCallback, useContext } from "react"
|
||||
import { PeerContext } from "../contexts/EphemeralState/PeerState";
|
||||
import React from "react";
|
||||
import { MouseEventHandler, ReactNode, useCallback, useContext, useEffect, useState } from "react"
|
||||
import { MdHeadphones, MdMic, MdMicOff, MdPhoneDisabled, MdPhoneInTalk, MdScreenShare, MdSend, MdVideocam } from "react-icons/md";
|
||||
import { ClientsListContext } from "../contexts/EphemeralState/ClientsListState";
|
||||
import { Connection, PeerContext } from "../contexts/EphemeralState/PeerState";
|
||||
import { UserMediaContext } from "../contexts/EphemeralState/UserMediaState";
|
||||
import useChannel from "../hooks/useChannel";
|
||||
import { useApi } from "../lib/useApi";
|
||||
import { IconType } from 'react-icons/lib/cjs/iconBase';
|
||||
import useClientId from "../hooks/useClientId";
|
||||
|
||||
|
||||
|
||||
export default function Voice(props: any) {
|
||||
const { uid } = props;
|
||||
const { connected, peerId, join } = useContext(PeerContext);
|
||||
export default function Voice() {
|
||||
const {
|
||||
connected,
|
||||
peerId,
|
||||
join,
|
||||
leave,
|
||||
connections,
|
||||
inCall,
|
||||
connectedChannel
|
||||
} = useContext(PeerContext);
|
||||
const { channel } = useChannel();
|
||||
const { clientName } = useContext(ClientsListContext);
|
||||
const { clientId } = useClientId();
|
||||
|
||||
const { send } = useApi({});
|
||||
const {
|
||||
mute, unmute, muted, enable, mediaStream
|
||||
} = useContext(UserMediaContext);
|
||||
|
||||
const [connectedVoiceClientIds, setConnectedVoiceClientIds] = useState<string[]>([])
|
||||
|
||||
const { send } = useApi({
|
||||
'voice:list'(data: { participants: Connection[] }) {
|
||||
setConnectedVoiceClientIds(data.participants.map(c => c.clientId));
|
||||
},
|
||||
'voice:join'(data: Connection) {
|
||||
setConnectedVoiceClientIds(ids => ([...ids, data.clientId]));
|
||||
},
|
||||
'voice:leave'(data: Connection) {
|
||||
setConnectedVoiceClientIds(ids => ids.filter(id => data.clientId !== id));
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log(connectedVoiceClientIds);
|
||||
}, [connectedVoiceClientIds])
|
||||
|
||||
useEffect(() => {
|
||||
send('voice:list', { channelId: channel })
|
||||
}, [channel])
|
||||
|
||||
const joinCall = useCallback(() => {
|
||||
if(peerId === null || connected === false || channel === null) return;
|
||||
enable();
|
||||
join(channel);
|
||||
send('voice:join', { peerId, channelId: channel })
|
||||
send('voice:join', { peerId, channelId: channel });
|
||||
}, [connected, peerId, channel]);
|
||||
|
||||
const leaveCall = useCallback(() => {
|
||||
if(peerId === null || connected === false) return;
|
||||
send('voice:leave', { peerId, channelId: channel })
|
||||
leave();
|
||||
send('voice:leave', { peerId, channelId: channel });
|
||||
}, [connected, peerId, channel]);
|
||||
|
||||
const inThisCall = inCall && channel === connectedChannel;
|
||||
|
||||
return <div style={{
|
||||
width: '100%',
|
||||
maxWidth: '500px',
|
||||
margin: '0px auto',
|
||||
background: 'var(--neutral-1)',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
}}>
|
||||
<fieldset>
|
||||
<legend>Peer Info</legend>
|
||||
connected: {connected ? 'true' : 'false'}<br></br>
|
||||
PeerId: {peerId}<br></br>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Actions</legend>
|
||||
<button onClick={joinCall}>Join Call</button>
|
||||
<button onClick={leaveCall}>Leave Call</button>
|
||||
</fieldset>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: '0px',
|
||||
width: '100%',
|
||||
display: 'grid',
|
||||
placeItems: 'center center'
|
||||
}}>
|
||||
<div style={{
|
||||
margin: '0px auto',
|
||||
display: 'inline',
|
||||
}}>
|
||||
{(!inThisCall) ? (
|
||||
<CircleButton
|
||||
icon={MdPhoneInTalk}
|
||||
onClick={joinCall}
|
||||
color="var(--green)"
|
||||
></CircleButton>
|
||||
) : (
|
||||
<>
|
||||
<CircleButton
|
||||
icon={muted ? MdMicOff : MdMic}
|
||||
onClick={() => muted ? unmute() : mute()}
|
||||
inverted={muted}
|
||||
></CircleButton>
|
||||
<CircleButton
|
||||
icon={MdHeadphones}
|
||||
onClick={leaveCall}
|
||||
></CircleButton>
|
||||
<CircleButton
|
||||
icon={MdScreenShare}
|
||||
onClick={leaveCall}
|
||||
></CircleButton>
|
||||
<CircleButton
|
||||
icon={MdVideocam}
|
||||
onClick={leaveCall}
|
||||
></CircleButton>
|
||||
<CircleButton
|
||||
icon={MdPhoneDisabled}
|
||||
onClick={leaveCall}
|
||||
color="var(--red)"
|
||||
></CircleButton>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
placeItems: 'center center',
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}}>
|
||||
|
||||
{connectedVoiceClientIds.length === 0 ? (
|
||||
<span style={{ color: 'var(--neutral-6)', fontWeight: '600' }}>No one is here right now</span>
|
||||
) : (
|
||||
<div style={{
|
||||
|
||||
}}>
|
||||
{connectedVoiceClientIds.map(id => {
|
||||
const connection = connections.find(c => c.clientId === id);
|
||||
const isMe = clientId === id;
|
||||
const stream = (isMe ? mediaStream : connection?.mediaStream) ?? undefined
|
||||
return (
|
||||
<Participant
|
||||
name={clientName[id]}
|
||||
stream={stream}
|
||||
></Participant>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
function Participant(props: {
|
||||
name: string,
|
||||
stream?: MediaStream
|
||||
}) {
|
||||
return (
|
||||
<div style={{
|
||||
width: '200px',
|
||||
height: '150px',
|
||||
display: 'inline-block',
|
||||
placeItems: 'center center',
|
||||
borderRadius: '8px',
|
||||
background: 'var(--neutral-4)',
|
||||
color: 'var(--neutral-8)',
|
||||
fontStyle: '500',
|
||||
margin: '4px',
|
||||
verticalAlign: 'top',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<Video autoPlay muted style={{
|
||||
width: '100%'
|
||||
}} srcObject={props.stream}></Video>
|
||||
{/* <div style={{
|
||||
display: 'grid',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
placeItems: 'center center',
|
||||
}}>
|
||||
{props.name}
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Video(props: React.VideoHTMLAttributes<HTMLVideoElement> & {
|
||||
srcObject?: MediaStream
|
||||
}) {
|
||||
|
||||
const [ref, setRef] = useState<HTMLVideoElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if(ref === null) return;
|
||||
if(props.srcObject === undefined || props.srcObject === null) return;
|
||||
console.log(ref);
|
||||
ref.srcObject = props.srcObject;
|
||||
}, [props.srcObject, ref]);
|
||||
|
||||
const filteredProps = Object.fromEntries(Object.entries(props).filter(([key, value]) => key !== 'srcObject'));
|
||||
|
||||
return <video ref={setRef} {...filteredProps}>{props.children}</video>
|
||||
}
|
||||
|
||||
|
||||
function CircleButton(props: {
|
||||
onClick: MouseEventHandler<HTMLDivElement>,
|
||||
icon: IconType,
|
||||
color?: string,
|
||||
inverted?: boolean,
|
||||
}) {
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'inline-block',
|
||||
width: '56px',
|
||||
height: '64px',
|
||||
padding: '8px',
|
||||
paddingRight: '0px',
|
||||
boxSizing: 'border-box',
|
||||
}}>
|
||||
<div onClick={props.onClick} style={{
|
||||
background: props.inverted ? 'var(--neutral-9)' : (props.color ?? 'var(--neutral-4)'),
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
cursor: 'pointer',
|
||||
display: 'grid',
|
||||
placeItems: 'center center',
|
||||
// paddingLeft: '4px',
|
||||
boxSizing: 'border-box',
|
||||
}}>
|
||||
{React.createElement(
|
||||
props.icon,
|
||||
{
|
||||
size: 24,
|
||||
color: props.inverted ? 'var(--neutral-1)' : 'inherit'
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// MdPhoneInTalk
|
||||
// MdPhoneDisabled
|
||||
|
|
@ -2,6 +2,8 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
|
|||
import { Peer, MediaConnection } from "peerjs";
|
||||
import { UserMediaContext } from "./UserMediaState";
|
||||
import { useApi } from "/@/lib/useApi";
|
||||
import { Audio } from "/@/components/Audio";
|
||||
import { sfx } from "/@/lib/sound";
|
||||
|
||||
export const PeerContext = createContext<{
|
||||
connected: boolean;
|
||||
|
|
@ -9,12 +11,16 @@ export const PeerContext = createContext<{
|
|||
peerId: string | null;
|
||||
join: (channelId: string) => void;
|
||||
leave: () => void;
|
||||
connections: Connection[];
|
||||
connectedChannel: string | null;
|
||||
}>({
|
||||
connected: false,
|
||||
peerId: null,
|
||||
inCall: false,
|
||||
join: () => {},
|
||||
leave: () => {}
|
||||
leave: () => {},
|
||||
connections: [],
|
||||
connectedChannel: null
|
||||
});
|
||||
|
||||
function useCurrent<T>(thing: T) {
|
||||
|
|
@ -27,11 +33,18 @@ function useCurrent<T>(thing: T) {
|
|||
return thingRef.current;
|
||||
}
|
||||
|
||||
interface Connection {
|
||||
export interface Connection {
|
||||
peerId: string;
|
||||
clientId: string;
|
||||
channelId: string;
|
||||
call: any;
|
||||
call: MediaConnection | null;
|
||||
isCaller: boolean;
|
||||
mediaStream: MediaStream | null;
|
||||
connected: boolean;
|
||||
}
|
||||
|
||||
function isCaller(a: string, b:string) {
|
||||
return [a, b].sort()[0] === a;
|
||||
}
|
||||
|
||||
export default function PeerState(props: any) {
|
||||
|
|
@ -42,33 +55,105 @@ export default function PeerState(props: any) {
|
|||
const [connected, setConnected] = useState(false);
|
||||
const [peer, setPeer] = useState<Peer | null>(null);
|
||||
const [peerId, setPeerId] = useState<string | null>(null);
|
||||
|
||||
const [incomingCalls, setIncomingCalls] = useState<MediaConnection[]>([]);
|
||||
const [outgoingCalls, setOutgoingCalls] = useState<string[]>([]);
|
||||
|
||||
const [connections, setConnections] = useState<Connection[]>([]);
|
||||
const [channel, setChannel] = useState<string | null>(null);
|
||||
|
||||
const addIncomingCall = useCurrent(useCallback((call: MediaConnection) => {
|
||||
// TODO filter out incoming calls from the same peerId.
|
||||
setIncomingCalls(incomingCalls => ([...incomingCalls, call]));
|
||||
}, []));
|
||||
|
||||
|
||||
const removeIncomingCalls = (peerIds: string[]) => {
|
||||
setIncomingCalls(calls => calls.filter(call => !peerIds.includes(call.peer)));
|
||||
}
|
||||
|
||||
const updateConnection = (peerId: string, data: Partial<Connection>) => {
|
||||
setConnections(connections => connections.map(connection => {
|
||||
if(connection.peerId !== peerId) return connection;
|
||||
return {
|
||||
...connection,
|
||||
...data
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const removeConnection = (peerId: string) => {
|
||||
setConnections(connections => connections.filter(connection => connection.peerId !== peerId));
|
||||
}
|
||||
|
||||
const destroyConnection = (peerId: string) => {
|
||||
setConnections(connections => connections.filter(connection => {
|
||||
if(connection.peerId !== peerId) return true;
|
||||
if(connection.call) {
|
||||
connection.call.close();
|
||||
removeConnection(peerId);
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const addStream = (id: string, stream: MediaStream) => {
|
||||
updateConnection(id, { mediaStream: stream, connected: true });
|
||||
}
|
||||
|
||||
// accept / reject incoming calls
|
||||
useEffect(() => {
|
||||
console.log(connections);
|
||||
}, [connections]);
|
||||
if(incomingCalls.length === 0) return;
|
||||
if(mediaStream === null) return;
|
||||
const possiblePeerIds = connections.map(c => c.peerId);
|
||||
for(const call of incomingCalls) {
|
||||
if(!possiblePeerIds.includes(call.peer))
|
||||
continue;
|
||||
call.on('stream', (stream) => addStream(call.peer, stream));
|
||||
call.answer(mediaStream);
|
||||
call.on('close', () => removeConnection(call.peer));
|
||||
updateConnection(call.peer, { call });
|
||||
}
|
||||
removeIncomingCalls(possiblePeerIds);
|
||||
}, [incomingCalls, connections, mediaStream])
|
||||
|
||||
// call peers that we should call
|
||||
useEffect(() => {
|
||||
if(outgoingCalls.length === 0) return;
|
||||
if(mediaStream === null) return;
|
||||
if(peer === null) return;
|
||||
for(const id of outgoingCalls) {
|
||||
const call = peer.call(id, mediaStream);
|
||||
call.on('close', () => removeConnection(id));
|
||||
call.on('stream', (stream) => addStream(call.peer, stream));
|
||||
updateConnection(id, { call });
|
||||
}
|
||||
setOutgoingCalls(prev => prev.filter(id => !outgoingCalls.includes(id)));
|
||||
}, [outgoingCalls, mediaStream, peer]);
|
||||
|
||||
const { send } = useApi({
|
||||
'voice:join'(data: any) {
|
||||
if(data.channelId !== channel) return;
|
||||
if(data.peerId === peerId) return;
|
||||
if(peerId === null) return;
|
||||
sfx.joinCall();
|
||||
const newConn: Connection = {
|
||||
call: null,
|
||||
connected: false,
|
||||
clientId: data.clientId,
|
||||
peerId: data.peerId,
|
||||
channelId: data.channelId,
|
||||
isCaller: isCaller(peerId, data.peerId),
|
||||
mediaStream: null
|
||||
};
|
||||
if(newConn.isCaller) {
|
||||
setOutgoingCalls(c => [...c, data.peerId]);
|
||||
}
|
||||
setConnections((connections) => ([
|
||||
...connections,
|
||||
{
|
||||
call: null,
|
||||
clientId: data.clientId,
|
||||
peerId: data.peerId,
|
||||
channelId: data.channelId
|
||||
}
|
||||
newConn
|
||||
]))
|
||||
},
|
||||
'voice:leave'(data: any) {
|
||||
sfx.leaveCall();
|
||||
if(data.channelId !== channel) return;
|
||||
if(data.peerId === peerId) return;
|
||||
setConnections(connections => connections.filter(connection => (
|
||||
|
|
@ -78,69 +163,82 @@ export default function PeerState(props: any) {
|
|||
)));
|
||||
},
|
||||
'voice:list'(data: any) {
|
||||
if(data.uid !== channel) return
|
||||
if(data.uid !== channel) return;
|
||||
if(peerId === null) return;
|
||||
setConnections(connections => {
|
||||
return data.participants
|
||||
.filter((p: any) => p.peerId !== peerId)
|
||||
.map((participant: any) => {
|
||||
const previousCall = null;
|
||||
const caller = isCaller(peerId, participant.peerId);
|
||||
if(caller) {
|
||||
setOutgoingCalls(c => [...c, participant.peerId]);
|
||||
}
|
||||
return {
|
||||
...participant,
|
||||
call: null
|
||||
call: null,
|
||||
isCaller: caller
|
||||
}
|
||||
})
|
||||
})
|
||||
console.log('PEER STATE CONNECTIONS', data);
|
||||
});
|
||||
}
|
||||
}, [channel, peerId]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if(connected) return;
|
||||
if(peer !== null) return;
|
||||
{
|
||||
const peer = new Peer();
|
||||
setPeer(peer);
|
||||
|
||||
const peer = new Peer();
|
||||
setPeer(peer);
|
||||
peer.on('open', (id: string) => {
|
||||
setConnected(true);
|
||||
setPeerId(id);
|
||||
});
|
||||
|
||||
peer.on('open', (id: string) => {
|
||||
setConnected(true);
|
||||
setPeerId(id);
|
||||
});
|
||||
peer.on('close', () => {
|
||||
setConnected(false);
|
||||
setPeerId(null);
|
||||
setPeer(null);
|
||||
});
|
||||
|
||||
peer.on('close', () => {
|
||||
setConnected(false);
|
||||
setPeerId(null);
|
||||
setPeer(null);
|
||||
});
|
||||
|
||||
peer.on('call', (call: MediaConnection) => {
|
||||
addIncomingCall(call);
|
||||
});
|
||||
}, [connected]);
|
||||
|
||||
const dial = useCallback((id: string) => {
|
||||
if(peer === null) return;
|
||||
if(mediaStream === null) return;
|
||||
peer.call(id, mediaStream);
|
||||
}, [peer, mediaStream])
|
||||
peer.on('call', (call: MediaConnection) => {
|
||||
addIncomingCall(call);
|
||||
});
|
||||
}
|
||||
}, [connected, peer]);
|
||||
|
||||
const joinChannel = (channelId: string) => {
|
||||
sfx.joinCall();
|
||||
setChannel(channelId);
|
||||
send('voice:list', { channelId })
|
||||
send('voice:list', { channelId });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if(channel === null) return;
|
||||
console.log('WE JOINED A CHANNEL')
|
||||
}, [channel])
|
||||
const leaveChannel = () => {
|
||||
setChannel(null);
|
||||
}
|
||||
|
||||
const value = useMemo(() => ({
|
||||
connected,
|
||||
peerId,
|
||||
inCall: connections.length === 0,
|
||||
inCall: channel !== null,
|
||||
join: joinChannel,
|
||||
leave: () => {}
|
||||
}), [connected, peerId, connections]);
|
||||
leave: leaveChannel,
|
||||
connections,
|
||||
connectedChannel: channel
|
||||
}), [connected, peerId, connections, channel]);
|
||||
|
||||
return <PeerContext.Provider value={value}>
|
||||
<div>
|
||||
{connections.map(conn => (
|
||||
(conn.mediaStream !== null) && (
|
||||
<div key={conn.peerId}>
|
||||
<Audio autoPlay hidden srcObject={conn.mediaStream}></Audio>
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
{props.children}
|
||||
</PeerContext.Provider>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,22 +5,36 @@ export const UserMediaContext = createContext<{
|
|||
mediaStream: MediaStream | null;
|
||||
enable: () => void;
|
||||
disable: () => void;
|
||||
mute: () => void;
|
||||
unmute: () => void;
|
||||
muted: boolean;
|
||||
enableCamera: () => void;
|
||||
disableCamera: () => void;
|
||||
cameraEnabled: boolean;
|
||||
}>({
|
||||
enabled: false,
|
||||
mediaStream: null,
|
||||
enable: () => {},
|
||||
disable: () => {},
|
||||
mute: () => {},
|
||||
unmute: () => {},
|
||||
muted: false,
|
||||
enableCamera: () => {},
|
||||
disableCamera: () => {},
|
||||
cameraEnabled: false,
|
||||
});
|
||||
|
||||
export default function UserMediaState(props: any) {
|
||||
|
||||
const [mediaStream, setMediaStream] = useState<MediaStream | null>(null);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [muted, setMuted] = useState(false);
|
||||
const [cameraEnabled, setCameraEnabled] = useState(false);
|
||||
|
||||
const enable = useCallback(async () => {
|
||||
const newStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
video: false,
|
||||
video: true,
|
||||
});
|
||||
|
||||
setMediaStream(newStream);
|
||||
|
|
@ -36,14 +50,29 @@ export default function UserMediaState(props: any) {
|
|||
|
||||
setMediaStream(null);
|
||||
setEnabled(false);
|
||||
}, [mediaStream])
|
||||
}, [mediaStream]);
|
||||
|
||||
const mute = () => {
|
||||
if(mediaStream === null) return;
|
||||
mediaStream.getAudioTracks()[0].enabled = false;
|
||||
setMuted(true);
|
||||
}
|
||||
|
||||
const unmute = () => {
|
||||
if(mediaStream === null) return;
|
||||
mediaStream.getAudioTracks()[0].enabled = true;
|
||||
setMuted(false);
|
||||
}
|
||||
|
||||
const value = useMemo(() => ({
|
||||
enabled: false,
|
||||
mediaStream: null,
|
||||
enabled,
|
||||
mediaStream,
|
||||
enable,
|
||||
disable
|
||||
}), []);
|
||||
disable,
|
||||
mute,
|
||||
unmute,
|
||||
muted
|
||||
}), [enabled, mediaStream, enable, disable, muted]);
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ export default function Channel(props: ChannelProps) {
|
|||
peerId: data.peerId,
|
||||
channelId: data.channelId
|
||||
}])
|
||||
sfx.joinCall();
|
||||
},
|
||||
'voice:list'(data: any) {
|
||||
if(type !== 'voice') return;
|
||||
|
|
@ -49,7 +48,6 @@ export default function Channel(props: ChannelProps) {
|
|||
setParticipants(data.participants);
|
||||
},
|
||||
'voice:leave'(data: any) {
|
||||
sfx.leaveCall();
|
||||
setParticipants(participants => participants.filter(p => (
|
||||
p.channelId !== data.channelId ||
|
||||
p.clientId !== data.clientId ||
|
||||
|
|
|
|||
Reference in New Issue