diff --git a/src/App.jsx b/src/App.jsx index 4563752..0c58009 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -17,6 +17,7 @@ function App() { const remoteVideoRef = useRef(null); // helper functions const saveSocket = useCallback(function saveSocket(s) { + console.log('[App] save socket', s); socket.current = s; }, []); // const loadLocalVideo = useCallback(function loadLocalVideo(stream) {}) diff --git a/src/hooks/useMedia.js b/src/hooks/useMedia.js new file mode 100644 index 0000000..649837d --- /dev/null +++ b/src/hooks/useMedia.js @@ -0,0 +1,75 @@ +import { useEffect, useState } from "react"; +import { setPeerConnection, setStream, peerConnection } from "../utils/media"; +import { useSelector } from "react-redux"; + +const configuration = { + iceServers: [ + { + urls: [ + "stun:stunserver.org", + "stun:stun.voiparound.com", + "stun:stun.voipbuster.com", + "stun:stun.voipstunt.com", + "stun:stun.voxgratia.org", + ], + }, + ], + // sdpSemantics: "plan-b", +}; + +function useMedia(videoRef, socket) { + console.log("[useMedia] socket--", socket); + const { role } = useSelector((state) => state.media); + + useEffect(() => { + const initPeerConnection = async () => { + if (peerConnection == null) { + console.log("create total new peer connection...."); + const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + // audio: true, + }); + + // save local stream + videoRef.current.srcObject = stream; + setStream(stream); + + // create peer connection + const peerConnection = new RTCPeerConnection(configuration); + // add tracks + stream.getTracks().forEach((track) => { + peerConnection.addTrack(track, stream); + }); + + // save peer connection + setPeerConnection(peerConnection); + } + + function onCandidate(event) { + if (event.candidate) { + console.log("iceCandidate", event.candidate); + // emit candidate + socket.current.emit( + `candidate::${role == "" ? "initiator" : role}`, + event.candidate + ); + } + } + console.log("[useMedia] set onCandidate..."); + peerConnection.onicecandidate = onCandidate; + // peerConnection.addEventListener("icecandidate", (event) => { + // if (event.candidate) { + // console.log("iceCandidate", event.candidate); + // // emit candidate + // socket.emit( + // `candidate::${role == "" ? "initiator" : role}`, + // event.candidate + // ); + // } + // }); + }; + initPeerConnection(); + }, [role, socket]); +} + +export default useMedia; diff --git a/src/pages/Room/Layout.jsx b/src/pages/Room/Layout.jsx index 577f44a..92ff517 100644 --- a/src/pages/Room/Layout.jsx +++ b/src/pages/Room/Layout.jsx @@ -3,14 +3,13 @@ import { useEffect, useRef, useState, useContext } from "react"; import { Modal } from "../../components/Modal"; import VideoWrapper from "./VideoWrapper"; import VideoActions from "./Actions"; -import { initSocket } from "../../utils/socket"; +import { initSocket, registerMoreEvents } from "../../utils/socket"; import { GlobalContext } from "../../App"; import ParticipantsList from "./ParticipantsList"; -// import { initWebRtc } from "../peer.tools"; -// import { useDispatch, useSelector } from "react-redux"; -// import { setPeerConnection } from "../../store/index"; +import { useDispatch, useSelector } from "react-redux"; export default function Room() { + const dispatch = useDispatch(); const { socket, saveSocket } = useContext(GlobalContext); const { roomId } = useParams(); const dialogRef = useRef(); @@ -24,13 +23,13 @@ export default function Room() { if (username.value == "") { dialogRef.current.open(); } - saveSocket(initSocket("https://10.168.1.141:8000", { token: roomId })); - - // async function prepare() { - // const peerConnection = await initWebRtc(); - // dispatch(setPeerConnection(peerConnection)); - // } - // prepare(); + const socket = initSocket( + "https://10.168.1.141:8000", + { token: roomId }, + dispatch + ); + registerMoreEvents(socket); + saveSocket(socket); }, []); function handleSetName() { diff --git a/src/pages/Room/VideoWrapper.jsx b/src/pages/Room/VideoWrapper.jsx index 3310073..f848165 100644 --- a/src/pages/Room/VideoWrapper.jsx +++ b/src/pages/Room/VideoWrapper.jsx @@ -22,45 +22,20 @@ import { ArrowDownTrayIcon, } from "@heroicons/react/24/solid"; // -import { useEffect, useRef, useState } from "react"; -import { initWebRtc } from "../peer.tools"; -import { useDispatch, useSelector } from "react-redux"; -import { handleStartNegotiation } from "../../utils/socket"; import { useContext } from "react"; import { GlobalContext } from "../../App"; +import useMedia from "../../hooks/useMedia"; -export default function Room({}) { +export default function VideoWrapper({}) { const { localVideoRef, remoteVideoRef, socket } = useContext(GlobalContext); - const localStream = useRef(); - const remoteStream = useRef(); - - useEffect(() => { - setTimeout(() => { - console.log("localVideoRef", localVideoRef.current); - console.log("remoteVideoRef", remoteVideoRef.current); - }, 2000); - async function prepare() { - // peerConnection cannot be saved to redux store - // dispatch(setPeerConnection(peerConnection)); is NOT allowed - const peerConnection = await initWebRtc({ - ref: localVideoRef, - stream: null, - }); - console.log("socket", socket); - // TODO:此处而言, negotiation::start 注册太晚了... 在 socket.js 中注册又太早 - socket.current.on("negotiation::start", () => { - console.log("start negotiation"); - }); - - console.log("peerConnection -->", peerConnection); - } - prepare(); - }, []); + // 初始化 peer connection + console.log('[VideoWrapper] ', socket); + useMedia(localVideoRef, socket); function togglePlay() { - selfVideoRef.current.paused - ? selfVideoRef.current.play() - : selfVideoRef.current.pause(); + localVideoRef.current.paused + ? localVideoRef.current.play() + : localVideoRef.current.pause(); } return (
diff --git a/src/pages/peer.tools.js b/src/pages/peer.tools.js index 785d9f5..5badd96 100644 --- a/src/pages/peer.tools.js +++ b/src/pages/peer.tools.js @@ -1,29 +1,7 @@ -function sendOfferToPeer(socket, offer) { - console.log("Sending offer to peer"); - socket.emit("offer", { offer }); -} - -function sendAnswerToPeer(socket, answer) { - console.log("Sending answer to peer"); - socket.emit("answer", { answer }); -} - -function sendIceCandidateToPeer(socket, iceCandidate) { - console.log("Sending ICE candidate to peer"); - socket.emit("ice-candidate", { iceCandidate }); -} - -function receiveOffer(socket, offer, peerConnection) { - console.log("Receiving offer from peer"); - peerConnection.setRemoteDescription(offer); -} +import { setPeerConnection } from "../utils/media"; -function receiveAnswer(socket, answer, peerConnection) { - console.log("Receiving answer from peer"); - peerConnection.setRemoteDescription(answer); -} - -export async function initWebRtc(selfVideo) { +export async function initWebRtc(selfVideo, socket) { + console.log("init webrtc ", selfVideo); const stream = await navigator.mediaDevices.getUserMedia({ video: true, // audio: true, @@ -49,9 +27,38 @@ export async function initWebRtc(selfVideo) { peerConnection.addEventListener("icecandidate", (event) => { if (event.candidate) { console.log("iceCandidate", event.candidate); + // inject + socket.emit("candidate::initiator", event.candidate); } }); + setPeerConnection(peerConnection); // const offer = await peerConnection.createOffer(); // peerConnection.setLocalDescription(offer); - return peerConnection; + + // return peerConnection; } + +// function sendOfferToPeer(socket, offer) { +// console.log("Sending offer to peer"); +// socket.emit("offer", { offer }); +// } + +// function sendAnswerToPeer(socket, answer) { +// console.log("Sending answer to peer"); +// socket.emit("answer", { answer }); +// } + +// function sendIceCandidateToPeer(socket, iceCandidate) { +// console.log("Sending ICE candidate to peer"); +// socket.emit("ice-candidate", { iceCandidate }); +// } + +// function receiveOffer(socket, offer, peerConnection) { +// console.log("Receiving offer from peer"); +// peerConnection.setRemoteDescription(offer); +// } + +// function receiveAnswer(socket, answer, peerConnection) { +// console.log("Receiving answer from peer"); +// peerConnection.setRemoteDescription(answer); +// } diff --git a/src/pages/useMedia.js b/src/pages/useMedia.js deleted file mode 100644 index 0327ea1..0000000 --- a/src/pages/useMedia.js +++ /dev/null @@ -1,41 +0,0 @@ -import { useState } from "react"; - -function useMedia() { - const [mediaStream, setMediaStream] = useState(null); - - useEffect(() => { - navigator.mediaDevices - .getUserMedia({ video: true, audio: true }) - .then((mediaStream) => { - setMediaStream(mediaStream); - }) - .catch((error) => { - console.error("Error accessing media devices.", error); - }); - }, []); - - return mediaStream; -} - -async function requireUserMedia(options) { - if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { - console.error("getUserMedia is not supported by your browser"); - return; - } - const stream = await navigator.mediaDevices.getUserMedia(options); -} - -export default useMedia; - -// const socket = {}; -// socket.on('connect', handleConnected); -// socket.on('disconnect', handleDisconnected); -// socket.on('error', handleError); -// socket.on('room:join', handleJoinRoom); -// socket.on('room:leave', handleLeaveRoom); -// socket.on('room:message', handleMessage); -// socket.on('user:join', handleUserJoin); -// socket.on('user:leave', handleUserLeave); -// socket.on('offer', handleOffer); -// socket.on('answer', handleAnwser); -// socket.on('candidate', handleCandidate); diff --git a/src/store/index.js b/src/store/index.js index 6be5ed5..ce31f6c 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,10 +1,13 @@ import { configureStore, createSlice } from "@reduxjs/toolkit"; +import { createOffer } from '../utils/media' // media 相关 API // getSenders() // replaceTrack() // getTracks() // setSinkId() - change audio output +// getCapabilities() +// pc.addIceCandidate() // Keywords: // Switching cameras in WebRTC @@ -18,7 +21,8 @@ const mediaSlice = createSlice({ videoDeviceId: "default", // 当前选中的视频设备id audioInputDeviceId: "default", // 当前选中的音频输入设备id audioOutputDeviceId: "default", // 当前选中的音频输出设备id - + role: "", // 当前角色,用于区分 offer/answer + offer: null, // 用于获取媒体流的约束条件,会随着选择而更新, replaceTrack() // 更换tracks的时候,是需要重新negotiation的, 也就是需要重新 createOffer constraints: { @@ -56,6 +60,13 @@ const mediaSlice = createSlice({ break; } }, + setRole(state, action) { + state.role = action.payload; + }, + setOffer(state, action) { + state.offer = action.payload; + state.role = 'initiator'; // <=== 这里设置角色 + }, }, }); @@ -65,6 +76,13 @@ const store = configureStore({ }, }); +export function setOfferCreator() { + return async (dispatch) => { + const offer = await createOffer(); + dispatch(mediaSlice.actions.setOffer(offer)); + }; +} + export default store; -export const { setLocalStream, addRemoteStream, setDevice } = +export const { setLocalStream, addRemoteStream, setDevice, setRole, setOffer } = mediaSlice.actions; diff --git a/src/utils/media.js b/src/utils/media.js index 6a41591..ffac6d9 100644 --- a/src/utils/media.js +++ b/src/utils/media.js @@ -2,7 +2,7 @@ // variables - those variables cannot be put into redux // let remoteStream = null; -let peerConnection = null; +export let peerConnection = null; let localStream = null; let remoteStreams = []; @@ -33,5 +33,17 @@ export function setStream(source, stream) { } export function setPeerConnection(pc) { + console.log("setPeerConnection", pc); peerConnection = pc; } + +export async function createOffer() { + if (peerConnection) { + const offer = await peerConnection.createOffer(); + peerConnection.setLocalDescription(offer); + console.log("setLocalDescription", offer); + return offer; + } + console.log("peerConnection is NULL"); + return null; +} diff --git a/src/utils/socket.js b/src/utils/socket.js index 4d5a2c4..8ad280a 100644 --- a/src/utils/socket.js +++ b/src/utils/socket.js @@ -1,4 +1,5 @@ import { io } from "socket.io-client"; +import { setRole, setOfferCreator } from "../store/index"; /** * 1. c ---> signal server 发现没有房间,创建房间 --> 通知 c1 你是 发起者;若发现有房间,则通知 c 你是加入者,并发送 offer @@ -6,7 +7,7 @@ import { io } from "socket.io-client"; * 3. c ---> c是加入者,向 signal server 发送 answer --> signal server 查看房间是否有人,有人则转发 answer,否则丢弃 answer,并返回 --> 告知失败 */ -export const initSocket = (url, { token }) => { +export const initSocket = (url, { token }, dispatch) => { const socket = io(url, { auth: { token, @@ -17,17 +18,15 @@ export const initSocket = (url, { token }) => { socket.on("connect", handleConnected); socket.on("disconnect", handleDisconnected); socket.on("error", handleError); + socket.on("waiting", handleWaiting); socket.on("negotiation::start", () => { console.log("negotiation::start"); + dispatch(setOfferCreator()); }); - // socket.on("negotiation::start", handleStartNegotiation.bind(socket)); - socket.on("waiting", handleWaiting.bind(socket)); return socket; }; -function dispatch(str) { - return str; -} +export function registerMoreEvents(socket) {} function handleConnected() { console.log("connected"); @@ -41,10 +40,6 @@ function handleError(error) { console.error("Error", error); } -function handleJoinRoom(room) { - console.log("join room", room); -} - export async function handleStartNegotiation(socket, pc) { console.log("start negotiation", socket); const offer = await pc.createOffer(); @@ -53,5 +48,5 @@ export async function handleStartNegotiation(socket, pc) { } function handleWaiting() { - console.log("waiting"); + console.log("waiting for participant(s)"); }