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

Что-то не так с видео-вызовом (WebRTC, socket.io)
Добрый день! Мне нужно создать приложение для видео вызова на базе WebRTC. Схема его работы следующая. На главной странице есть кнопка со ссылкой на страницу видео связи. Id страницы — это строка из случайных символов. Далее клиент присоединяется к комнате, отправив запрос на сервер через socket.io. Его id также отображается на экране. Это приложение full-stack на базе React, Node.js, Socket.io, WebRTC, TypeScript. Я все максимально упростил, чтобы сосредоточиться именно на технологии WebRTC.
Чтобы осуществить вызов, нужно ввести id другого клиента и нажать кнопку ниже. Мне нужно, чтобы при установлении соединения у каждого пользователя появлялось видео с устройства собеседника, и наоборот (как например при видео звонке WhatsApp). На каждом устройстве включается разрешение на использование микрофона и камеры.
В качестве сервера для обмена сигналами используется socket.io на базе node.js.
Проблема в том, что при осуществлении вызова, видео иногда отображается у обоих пользователей, иногда только на одном устройстве. Иногда на обоих концах нет видео. В общем, приложение работает нестабильно.
Вот код сервера для обмена сигналами:
import { io } from ".";

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

io.on("connection", (socket) => {
  socket.on("server", (arg: Signal) => {
    console.log(arg)
    if(arg.operation==='join') {
      socket.join(arg.target);
    }
    if(arg.operation==='offer' || arg.operation==='answer' || arg.operation==='ice') {
      console.log(arg.operation);
      io.to(arg.target).emit(arg.target, arg)
    }
  });
});

Главная страница клиента. При клике на кнопку переходим в комнату (роут со случайным id):
import React from "react";
import { XButton2 } from "../components/Buttons";
import { useNavigate } from "react-router-dom";

const makeId = (length: number): string => {
  let result = "";
  const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
  const charactersLength = characters.length;
  for (let i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength));
  }
  return result;
};

export default function Main() {
  const navigate = useNavigate();
  function create() {
    const id = makeId(4);
    navigate(`/room/${id}`);
  }

  return <XButton2 onClick={create}>Create room</XButton2>;
}

Страница комнаты:
import React, { Fragment } from "react";
import { IconButton } from "@mui/material";
import { AiOutlineLeft } from "react-icons/ai";
import { useNavigate, useParams } from "react-router-dom";
import RoomContainer from "../components/RoomContainer";

export default function Room() {
  const navigate = useNavigate();
  const { id } = useParams();

  return (
    <Fragment>
      <IconButton
        sx={{ mb: 3, fontSize: "35px", border: "1px solid #ddd" }}
        onClick={() => navigate("/")}
      >
        <AiOutlineLeft />
      </IconButton>
      {id ? <RoomContainer id={id} /> : null}
    </Fragment>
  );
}

Это компонент, в котором должно выполняться видео соединение:
import React, {
  ChangeEvent,
  Fragment,
  useEffect,
  useRef,
  useState,
} from "react";
import {
  Box,
  Card,
  CardContent,
  CardHeader,
  Snackbar,
  Stack,
  TextField,
  Typography,
} from "@mui/material";
import { io } from "socket.io-client";
import { XButton1 } from "./Buttons";
import { rtcConfig } from "../../rtcConfig";

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

const VITE_SOCK_API = import.meta.env.VITE_SOCK_API;

const socket = io(VITE_SOCK_API);

const peerConnection = new RTCPeerConnection(rtcConfig);

export default function RoomContainer({ id }: { id: string }) {
  const remoteRef = useRef<HTMLVideoElement>(null);
  const [message, setMessage] = useState<string>("");
  const [value, setValue] = useState<string>("");

  async function call() {
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);
    const mySignal: Signal = {
      operation: "offer",
      source: id,
      target: value,
      data: offer,
    };
    socket.emit("server", mySignal);
  }

  useEffect(() => {
    // VIDEO!!!
    navigator.mediaDevices
      .getUserMedia({
        video: true,
        audio: true,
      })
      .then((stream) => {
        // Join
        const mySignal: Signal = { operation: "join", target: id };
        socket.emit("server", mySignal);

        stream.getTracks().forEach((track) => {
          peerConnection.addTrack(track, stream);
        });

        // Incoming sockets
        socket.on(id, async (arg: Signal) => {
          // Incoming offer
          if (arg.operation === "offer" && arg.source) {
            peerConnection.setRemoteDescription(
              new RTCSessionDescription(arg.data)
            );
            const answer = await peerConnection.createAnswer();
            await peerConnection.setLocalDescription(answer);
            const mySignal: Signal = {
              operation: "answer",
              source: id,
              target: arg.source,
              data: answer,
            };
            socket.emit("server", mySignal);
          }
          // Recieve an answer
          if (arg.operation === "answer") {
            const remoteDesc = new RTCSessionDescription(arg.data);
            await peerConnection.setRemoteDescription(remoteDesc);
          }
          // Ice candidate
          if (arg.operation === "ice") {
            try {
              const candidate = new RTCIceCandidate(arg.data); // ??????
              await peerConnection.addIceCandidate(candidate);
            } catch (e) {
              console.error("Error adding received ice candidate", e);
            }
          }
        });
      });
  }, []);

  peerConnection.addEventListener("track", async (event) => {
    const [remoteStream] = event.streams;
    if (remoteRef.current) remoteRef.current.srcObject = remoteStream;
  });

  // Listen for local ICE candidates on the local RTCPeerConnection
  peerConnection.addEventListener("icecandidate", (event) => {
    console.log(event.candidate);
    if (event.candidate) {
      const mySignal: Signal = {
        operation: "ice",
        source: id,
        target: value,
        data: event.candidate,
      };
      socket.emit("server", mySignal);
    }
  });

  // Listen for connectionstatechange on the local RTCPeerConnection
  peerConnection.addEventListener("connectionstatechange", (event) => {
    if (peerConnection.connectionState === "connected") {
      setMessage("Peers connected!");
    }
  });

  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)
              }
            />
            <XButton1
              onClick={async () => {
                // offer
                if (id && value) call();
              }}
            >
              Connect
            </XButton1>
            <Box sx={{ width: "100%", pb: "65%", position: "relative", border: '1px solid #ddd' }}>
              <video
                ref={remoteRef}
                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>
  );
}

Также я заметил, что во время установления соединения, событие по раздаче кандидатов срабатывает очень много раз (я включил прослушивание через console.log).
Полный код также доступен в этом репозитории:

https://github.com/narzantaria/webrtc-app

Подскажите пожалуйста, что нужно исправить или добавить для полноценной работы видео связи?
Спасибо всем за внимание.
Ответить с цитированием