diff --git a/src/components/PopupShare/PopupShare.tsx b/src/components/PopupShare/PopupShare.tsx index b25df0e..21605c9 100644 --- a/src/components/PopupShare/PopupShare.tsx +++ b/src/components/PopupShare/PopupShare.tsx @@ -19,28 +19,3 @@ export const PopupShare: React.FC = ({ setClose, data }) => { useEffect(() => () => setCopy(false), []); - - return ( -
-
- Пригласить на демонстрацию - -
-
- Код подключения - -
-
-
- Ссылка для подключения - -
-
- -
-
- ) -} \ No newline at end of file diff --git a/src/hooks/useStateWithCallback.js b/src/hooks/useStateWithCallback.js new file mode 100644 index 0000000..ab62647 --- /dev/null +++ b/src/hooks/useStateWithCallback.js @@ -0,0 +1,22 @@ +import { useState, useRef, useEffect, useCallback } from "react"; +export const useStateWithCallback = (intialState) => { + const [state, setState] = useState(intialState); + const cbRef = useRef(null); + + const updateState = useCallback((newState, cb) => { + cbRef.current = cb; + + setState((prev) => + typeof newState === "function" ? newState(prev) : newState + ); + }, []); + + useEffect(() => { + if (cbRef.current) { + cbRef.current(state); + cbRef.current = null; + } + }, [state]); + + return [state, updateState]; +}; diff --git a/src/hooks/useWebRTC.js b/src/hooks/useWebRTC.js new file mode 100644 index 0000000..728b38b --- /dev/null +++ b/src/hooks/useWebRTC.js @@ -0,0 +1,401 @@ +import { useEffect, useState, useRef, useCallback } from "react"; +import { ACTIONS } from "../socket/actions"; +import socketInit from "../socket"; +import freeice from "freeice"; +import { useStateWithCallback } from "./useStateWithCallback"; +import { useHistory } from "react-router-dom"; + + +export const useWebRTC = (roomId, user) => { + const history = useHistory(); + const [clients, setClients] = useStateWithCallback([]); + const audioElements = useRef({}); + const [message, setMessages] = useState(""); + const [control, setControl] = useState('') + const [warning, setWarning] = useState(""); + const connections = useRef({}); + const socket = useRef(null); + const localMediaStream = useRef(null); + const clientsRef = useRef(null); + + const addNewClient = useCallback( + (newClient, cb) => { + const lookingFor = clients.find((client) => client.id === newClient.id); + + if (lookingFor === undefined) { + setClients((existingClients) => [...existingClients, newClient], cb); + } + }, + [clients, setClients] + ); + + useEffect(() => { + clientsRef.current = clients; + }, [clients]); + + useEffect(() => { + const initChat = async () => { + socket.current = socketInit(); + console.log(localMediaStream.current) + await captureMedia(); + addNewClient( + { + ...user, + muted: true, + admin: false, + control: false, + }, + () => { + const localElement = audioElements.current[user.id]; + if (localElement) { + localElement.volume = 0; + localElement.srcObject = localMediaStream.current; + } + } + ); + socket.current.on(ACTIONS.MUTE_INFO, ({ userId, isMute, isAdmin }) => { + handleSetMute(isMute, userId); + }); + + socket.current.on(ACTIONS.ADD_PEER, handleNewPeer); + socket.current.on(ACTIONS.REMOVE_PEER, handleRemovePeer); + socket.current.on(ACTIONS.ICE_CANDIDATE, handleIceCandidate); + socket.current.on(ACTIONS.SESSION_DESCRIPTION, setRemoteMedia); + socket.current.on(ACTIONS.MUTE, ({ peerId, userId }) => { + handleSetMute(true, userId); + }); + + socket.current.on("ADMIN-LEFT", ({ isLeft }) => { + setWarning(isLeft); + }); + + socket.current.on("ADMIN-SET", ({ admin }) => { + handleSetAdmin(true, admin); + setControl(admin) + }); + + socket.current.on(ACTIONS.UNMUTE, ({ peerId, userId }) => { + handleSetMute(false, userId); + }); + socket.current.emit(ACTIONS.JOIN, { + roomId, + user, + }); + + socket.current.on("CONTROL-SET", ({ userId }) => { + handleSetControl(userId); + setControl(userId) + }); + + async function captureMedia() { + // Start capturing local audio stream. + localMediaStream.current = await navigator.mediaDevices.getUserMedia({ + audio: true, + }).catch(() => { + alert('Подключите микрофон!') + }); + } + + socket.current.on("REQUEST-CONTROL", ({ userID }) => { + setMessages(userID); + }); + + async function handleNewPeer({ peerId, createOffer, user: remoteUser }) { + if (peerId in connections.current) { + return console.warn( + `You are already connected with ${peerId} (${user.name})` + ); + } + + // Store it to connections + connections.current[peerId] = new RTCPeerConnection({ + iceServers: freeice(), + }); + + // Handle new ice candidate on this peer connection + connections.current[peerId].onicecandidate = (event) => { + socket.current.emit(ACTIONS.RELAY_ICE, { + peerId, + icecandidate: event.candidate, + }); + }; + + // Handle on track event on this connection + connections.current[peerId].ontrack = ({ streams: [remoteStream] }) => { + addNewClient( + { + ...remoteUser, + muted: true, + admin: false, + control: false, + }, + () => { + // get current users mute info + const currentUser = clientsRef.current.find( + (client) => client.id === user.id + ); + if (currentUser) { + socket.current.emit(ACTIONS.MUTE_INFO, { + userId: user.id, + roomId, + isMute: currentUser.muted, + isAdmin: currentUser.admin, + isControl: currentUser.control, + }); + } + if (audioElements.current[remoteUser.id]) { + audioElements.current[remoteUser.id].srcObject = remoteStream; + } else { + let settled = false; + const interval = setInterval(() => { + if (audioElements.current[remoteUser.id]) { + audioElements.current[remoteUser.id].srcObject = + remoteStream; + settled = true; + } + + if (settled) { + clearInterval(interval); + } + }, 300); + } + } + ); + }; + + // Add connection to peer connections track + localMediaStream.current.getTracks().forEach((track) => { + connections.current[peerId].addTrack(track, localMediaStream.current); + }); + + // Create an offer if required + if (createOffer) { + const offer = await connections.current[peerId].createOffer(); + + // Set as local description + await connections.current[peerId].setLocalDescription(offer); + + // send offer to the server + socket.current.emit(ACTIONS.RELAY_SDP, { + peerId, + sessionDescription: offer, + }); + } + } + async function handleRemovePeer({ peerId, userId }) { + console.log(userId); + // Correction: peerID to peerId + if (connections.current[peerId]) { + connections.current[peerId].close(); + } + + delete connections.current[peerId]; + delete audioElements.current[peerId]; + setClients((list) => handleLogout(list, userId)); + } + async function handleIceCandidate({ peerId, icecandidate }) { + if (icecandidate) { + connections.current[peerId].addIceCandidate(icecandidate); + } + } + async function setRemoteMedia({ + peerId, + sessionDescription: remoteSessionDescription, + }) { + connections.current[peerId].setRemoteDescription( + new RTCSessionDescription(remoteSessionDescription) + ); + + // If session descrition is offer then create an answer + if (remoteSessionDescription.type === "offer") { + const connection = connections.current[peerId]; + + const answer = await connection.createAnswer(); + connection.setLocalDescription(answer); + + socket.current.emit(ACTIONS.RELAY_SDP, { + peerId, + sessionDescription: answer, + }); + } + } + async function handleSetMute(mute, userId) { + console.log(clientsRef.current); + const clientIdx = clientsRef.current + .map((client) => client.id) + .indexOf(userId); + const allConnectedClients = JSON.parse( + JSON.stringify(clientsRef.current) + ); + + if (clientIdx > -1) { + allConnectedClients[clientIdx].muted = mute; + setClients(allConnectedClients); + } + } + + async function handleSetControl(userId) { + const clientIdx = clientsRef.current + .map((client) => client.id) + .indexOf(userId); + const allConnectedClients = JSON.parse( + JSON.stringify(clientsRef.current) + ); + for (let i = 0; i < allConnectedClients.length; i++) { + allConnectedClients[i].control = false; + } + + if (clientIdx > -1) { + allConnectedClients[clientIdx].control = true; + setClients(allConnectedClients); + } + } + + async function handleSetAdmin(admin, userId) { + const clientIdx = clientsRef.current + .map((client) => client.id) + .indexOf(userId); + const allConnectedClients = JSON.parse( + JSON.stringify(clientsRef.current) + ); + if (clientIdx > -1) { + allConnectedClients[clientIdx].admin = admin; + allConnectedClients[clientIdx].control = true; + setClients(allConnectedClients); + } + } + }; + + initChat(); + return () => { + if (localMediaStream.current) { + localMediaStream.current.getTracks().forEach((track) => track.stop()); + } + socket.current.emit(ACTIONS.LEAVE, { roomId }); + for (let peerId in connections.current) { + connections.current[peerId].close(); + delete connections.current[peerId]; + delete audioElements.current[peerId]; + } + socket.current.off(ACTIONS.ADD_PEER); + socket.current.off(ACTIONS.REMOVE_PEER); + socket.current.off(ACTIONS.ICE_CANDIDATE); + socket.current.off(ACTIONS.SESSION_DESCRIPTION); + socket.current.off(ACTIONS.MUTE); + socket.current.off(ACTIONS.UNMUTE); + }; + }, []); + + const provideRef = (instance, userId) => { + audioElements.current[userId] = instance; + }; + + const handleAdmin = () => { + let settled = false; + let interval = setInterval(() => { + socket.current.emit("ADMIN-SET", { + roomId, + }); + + settled = true; + if (settled) { + clearInterval(interval); + } + }, 200); + }; + + const handleMute = (isMute, userId) => { + let settled = false; + + if (userId === user.id) { + let interval = setInterval(() => { + if (localMediaStream.current) { + localMediaStream.current.getTracks()[0].enabled = !isMute; + if (isMute) { + socket.current.emit(ACTIONS.MUTE, { + roomId, + userId: user.id, + }); + } else { + socket.current.emit(ACTIONS.UNMUTE, { + roomId, + userId: user.id, + }); + } + settled = true; + } + if (settled) { + clearInterval(interval); + } + }, 200); + } + }; + + const handleControl = (userId) => { + let settled = false; + let interval = setInterval(() => { + socket.current.emit("CONTROL-SET", { + roomId, + userId: userId, + }); + + settled = true; + if (settled) { + clearInterval(interval); + } + }, 200); + }; + + const handleLogout = (list, userId) => { + const isAdmin = list.some((i) => i.admin === true && i.id === userId); + const isControl = list.some((i) => i.control === true && i.id === userId); + console.log(isControl); + if (isAdmin) { + socket.current.emit("ADMIN-LEFT", { + isLeft: true, + }); + } else if (isControl) { + handleChangeControl(list); + } + + return list.filter((c) => c.id !== userId); + }; + + const handleChangeControl = (list) => { + const admin = list.find((i) => i.admin === true); + if (admin) { + handleControl(admin.id); + } else { + return; + } + }; + + const handleReturnControl = () => { + const admin = clients.find((i) => i.admin === true); + if (admin) { + handleControl(admin.id); + } else { + return; + } + }; + + const sendRequset = (user) => { + socket.current.emit("REQUEST-CONTROL", { + userID: user.id, + roomId, + }); + }; + + return { + clients, + provideRef, + warning, + handleMute, + handleControl, + sendRequset, + message, + control, + handleAdmin, + handleReturnControl, + }; +}; diff --git a/src/socket/actions.js b/src/socket/actions.js new file mode 100644 index 0000000..141d380 --- /dev/null +++ b/src/socket/actions.js @@ -0,0 +1,13 @@ +export const ACTIONS = { + JOIN: 'join', + LEAVE: 'leave', + ADD_PEER: 'add-peer', + REMOVE_PEER: 'remove-peer', + RELAY_ICE: 'relay-ice', + RELAY_SDP: 'relay-sdp', + SESSION_DESCRIPTION: 'session-description', + ICE_CANDIDATE: 'ice-candidate', + MUTE: 'mute', + UNMUTE: 'unmute', + MUTE_INFO: 'mute-info', +}; diff --git a/src/socket/index.js b/src/socket/index.js new file mode 100644 index 0000000..0eef359 --- /dev/null +++ b/src/socket/index.js @@ -0,0 +1,13 @@ +import { io } from "socket.io-client"; + +const socketInit = () => { + const options = { + "force new connection": true, + reconnectionAttempts: "Infinity", + timeout: 10000, + transports: ["websocket"], + }; + return io('localhost:5500', options); +}; + +export default socketInit; diff --git a/src/socket/socketExample.js b/src/socket/socketExample.js new file mode 100644 index 0000000..a727e49 --- /dev/null +++ b/src/socket/socketExample.js @@ -0,0 +1,40 @@ +useEffect(() => { + let socket = new WebSocket('wss://stream.graff.tech:13001') + setSocket(socket) + socket.onopen = () => { + socket.send('{"message" : "NEW_USER"}') + setConnected(true) + } + + socket.onmessage = (e) => { + const response = JSON.parse(e.data) + console.log(response, 'res') + switch (response.message) { + case 'NEW_USER': + console.log('user'); + break + + case 'SESS_CREATION': + setData({ id: response.id, port: response.port }) + history.push(`/stream/${response.id}`) + break + + case 'SESS_CONNECT': + setData({ id: response.id, port: response.content }) + break + + case 'SESS_NOT_EXISTS': + break + } + } + + socket.onclose = () => { + socket.close() + setConnected(false) + }; + + socket.onerror = () => { + console.log("WS Error"); + setConnected(false) + }; + }, [])