Показать сообщение отдельно
  #4 (permalink)  
Старый 09.05.2023, 06:54
Аватар для Kiten
Интересующийся
Отправить личное сообщение для Kiten Посмотреть профиль Найти все сообщения от Kiten
 
Регистрация: 18.05.2018
Сообщений: 16

Причин было несколько.
Во-первых, при завершении сеанса WebRTC нужно закрыть peerConnection и остановить видеопоток.
Также я использовал рефы для прямого доступа к объектам из разных функций.
И наконец, использованная мной конструкция была устаревшей, хотя она приводится в официальной документации WebRTC. Более эффективный способ называется "Perfect negotiation pattern" и описан здесь:
https://developer.mozilla.org/en-US/...ct_negotiation
Вот работающий код:
// MDN perfect negotiation pattern
import React, {
  ChangeEvent,
  Fragment,
  useEffect,
  useRef,
  useState,
} from "react";
import {
  Box,
  Card,
  CardContent,
  CardHeader,
  Snackbar,
  Stack,
  TextField,
  Typography,
} from "@mui/material";
import { Socket, io } from "socket.io-client";
import { XButton1, XButton2 } from "./Buttons";
import { rtcConfig } from "../../rtcConfig";

interface Signal {
  operation: "offer" | "answer" | "ice" | null;
  source?: string;
  target?: string;
  data?: any;
}

const VITE_SOCK_API = import.meta.env.VITE_SOCK_API;

const constraints = { audio: true, video: true };

export default function RoomContainer({ id }: { id: string }) {
  const partnerVideo = useRef<HTMLVideoElement>(null);
  const peerRef = useRef<RTCPeerConnection>();
  const socketRef = useRef<Socket | null>();
  const userStream = useRef<any | null>();
  const [message, setMessage] = useState<string>("");
  const [value, setValue] = useState<string>("");

  useEffect(() => {
    navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
      userStream.current = stream;
      socketRef.current = io(VITE_SOCK_API);
      socketRef.current.emit("join", id);
      socketRef.current.on(id, (arg: Signal) => {
        if (arg.operation === "offer") handleRecieveCall(arg);
        if (arg.operation === "answer") handleAnswer(arg);
        if (arg.operation === "ice") handleNewICECandidateMsg(arg);
      });
    });
  }, []);

  function handleRecieveCall(arg: Signal) {
    peerRef.current = createPeer();
    const desc = new RTCSessionDescription(arg.data);
    peerRef.current
      .setRemoteDescription(desc)
      .then(() => {
        userStream.current
          .getTracks()
          .forEach((track: MediaStreamTrack) =>
            peerRef.current?.addTrack(track, userStream.current)
          );
      })
      .then(() => {
        return peerRef.current?.createAnswer();
      })
      .then((answer) => {
        return peerRef.current?.setLocalDescription(answer);
      })
      .then(() => {
        const payload: Signal = {
          operation: "answer",
          target: arg.source,
          source: id,
          data: peerRef.current?.localDescription,
        };
        socketRef.current?.emit("server", payload);
      });
  }

  function callUser() {
    peerRef.current = createPeer();
    userStream.current
      .getTracks()
      .forEach((track: MediaStreamTrack) =>
        peerRef.current?.addTrack(track, userStream.current)
      );
  }

  function createPeer() {
    const peer = new RTCPeerConnection(rtcConfig);
    peer.onicecandidate = handleICECandidateEvent;
    peer.ontrack = handleTrackEvent;
    peer.onnegotiationneeded = () => handleNegotiationNeededEvent();
    peer.oniceconnectionstatechange = handleICEConnectionStateChangeEvent;
    peer.onsignalingstatechange = handleSignalingStateChangeEvent;
    return peer;
  }
  

  function handleICEConnectionStateChangeEvent() {
    console.log("Ice connection state change")
    switch (peerRef.current?.iceConnectionState) {
      case "closed":
      case "failed":
        closeVideoCall();
        break;
    }
  }

  function handleSignalingStateChangeEvent() {
    console.log("Signalling state change")
    switch (peerRef.current?.signalingState) {
      case "closed":
        closeVideoCall();
        break;
    }
  }

  function handleAnswer(message: Signal) {
    const desc = new RTCSessionDescription(message.data);
    if (peerRef.current)
      peerRef.current.setRemoteDescription(desc).catch((e) => console.log(e));
  }

  function handleICECandidateEvent(e: RTCPeerConnectionIceEvent) {
    if (e.candidate) {
      const payload: Signal = {
        operation: "ice",
        target: value,
        data: e.candidate,
      };
      socketRef.current?.emit("server", payload);
    }
  }

  function handleNegotiationNeededEvent() {
    if (peerRef.current)
      peerRef.current
        .createOffer()
        .then((offer) => {
          return peerRef.current?.setLocalDescription(offer);
        })
        .then(() => {
          const payload: Signal = {
            operation: "offer",
            target: value,
            source: id,
            data: peerRef.current?.localDescription,
          };
          socketRef.current?.emit("server", payload);
        })
        .catch((e) => console.log(e));
  }

  function handleNewICECandidateMsg(incoming: Signal) {
    const candidate = new RTCIceCandidate(incoming.data);

    if (peerRef.current)
      peerRef.current.addIceCandidate(candidate).catch((e) => console.log(e));
  }

  function handleTrackEvent(e: RTCTrackEvent) {
    if (partnerVideo.current) partnerVideo.current.srcObject = e.streams[0];
  }

  function closeVideoCall() {
    if (peerRef.current) {
      peerRef.current.ontrack = null;
      peerRef.current.onicecandidate = null;
      peerRef.current.oniceconnectionstatechange = null;
      peerRef.current.onsignalingstatechange = null;
      peerRef.current.onicegatheringstatechange = null;
      peerRef.current.onnegotiationneeded = null;
      peerRef.current.close();
      peerRef.current = undefined;
    }
    if (userStream.current)
      userStream.current
        .getTracks()
        .forEach(function (track: MediaStreamTrack) {
          track.stop();
        });
    userStream.current = null;
    if (partnerVideo.current) partnerVideo.current.srcObject = null;
  }

  return (
    <Fragment>
      <Card>
        <CardHeader
          title="Connect and open media channel"
          sx={{ borderBottom: "1px solid #ddd" }}
        />
        <CardContent>
          <Stack spacing={2}>
            <Typography
              variant="h5"
              sx={{
                cursor: "pointer",
                border: "1px solid #ddd",
                borderRadius: "5px",
                p: 2,
              }}
              onClick={() => {
                if (id) {
                  navigator.clipboard.writeText(id);
                  setMessage("Copied");
                }
              }}
            >
              {id}
            </Typography>
            <TextField
              label="Connect other client"
              variant="outlined"
              value={value}
              onChange={(e: ChangeEvent<HTMLInputElement>) =>
                setValue(e.target.value)
              }
            />
            <Stack direction="row" spacing={2}>
              <XButton1
                onClick={async () => {
                  callUser();
                }}
              >
                Connect
              </XButton1>
            </Stack>
            <Box
              sx={{
                width: "100%",
                pb: "65%",
                position: "relative",
                border: "1px solid #ddd",
              }}
            >
              <video
                ref={partnerVideo}
                autoPlay
                playsInline
                style={{
                  position: "absolute",
                  left: 0,
                  top: 0,
                  width: "100%",
                  height: "100%",
                }}
              />
            </Box>
          </Stack>
        </CardContent>
      </Card>
      <Snackbar
        open={Boolean(message.length)}
        autoHideDuration={3000}
        onClose={() => setMessage("")}
        message={message}
      />
    </Fragment>
  );
}
Ответить с цитированием