Search code examples
javascriptreactjssocket.iowebrtcpeerjs

React: DOMException: Failed to execute 'setLocalDescription' on 'RTCPeerConnection': Failed to set local answer sdp: Called in wrong state: stable


I am making a live stream application using peerjs in React with vite.

I know there are already many questions like this but this is specific to ReactJs. I could not find any solution related to React.

I get this error:

React: DOMException: Failed to execute 'setLocalDescription' on 'RTCPeerConnection': Failed to set local answer sdp: Called in wrong state: stable

Edit: I found the answer and in short this is a problem that uses two other problems' solutions to fix it.

This is my webRTC.ts file.

import Peer, { DataConnection, MediaConnection } from "peerjs";
import socket from "./socket";
import { JOIN_LECTURE } from "./socketaction";

export const getUserStream: () => Promise<MediaStream> =
  (): Promise<MediaStream> => {
    return navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true,
    });
  };
export const getScreenStream: () => Promise<MediaStream> =
  (): Promise<MediaStream> => {
    return navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
  };

export const initPeer: (peer: Peer, name: string) => void = (
  peer: Peer,
  name: string
) => {
  // On Error
  peer.on("error", (err: Error): void => {
    console.error(err);
  });
  // On Open
  peer.on("open", (id: string): void => {
    console.log("[p]open", name);
    console.log(id);
  });
  // On Connection
  peer.addListener("connection", (conn: DataConnection): void => {
    conn.on("data", (data: unknown): void => {
      console.log(data, "data");
    });
  });
};

export const answerCall = (
  peer: Peer,
  lectureId: string,
  cb: (stream: MediaStream, type: "user" | "screen") => void
): void => {
  peer.addListener("open", () => {
    socket.emit(JOIN_LECTURE, {
      peerId: peer.id,
      lectureId: lectureId,
    });
    // socket.emit(GET_STREAM);
  });
  peer.on("call", (call: MediaConnection): void => {
    call.answer();
    call.on("stream", (stream: MediaStream): void => {
      cb(stream, call.metadata.streamType);
    });
    call.on("error", console.error);
    call.on("close", (): void => {
      console.log("call closed");
    });
  });
};

export const shareUser = (
  peer: Peer,
  stream: MediaStream,
  studentId: string
) => {
  if (peer && studentId) {
    console.log(studentId);
    const conn = peer.connect(studentId);
    conn.on("open", () => {
      const call: MediaConnection = peer.call(studentId, stream, {
        metadata: {
          streamType: "user",
        },
      });
      call.on("error", console.error);
      call.on("close", (): void => {
        console.log("call closed");
      });
    });
    conn.on("data", console.log);
    conn.on("error", console.error);
    conn.on("iceStateChanged", () => console.log("IceStageChanged"));
  }
};

This is my main.tsx file

import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
import { ThemeProvider } from "@mui/system";
import MuiTheme from "./mui-theme";
import { Provider } from "react-redux";
import { store } from "./redux/store";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import { CookiesProvider } from "react-cookie";
import { StrictMode } from "react";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <StrictMode>
    <Provider store={store}>
      <CookiesProvider>
        <ThemeProvider theme={MuiTheme}>
          <App />
        </ThemeProvider>
        <ToastContainer theme="dark" position="bottom-right" />
      </CookiesProvider>
    </Provider>
  </StrictMode>
);

This is my liveLecture.tsx file.

import {
  CallEndRounded,
  ChatRounded,
  DoNotTouchRounded,
  FullscreenRounded,
  MicOffRounded,
  MicRounded,
  PanToolRounded,
  PausePresentationRounded,
  PresentToAllRounded,
  SendRounded,
  SpeakerNotesOffRounded,
  VideocamOffRounded,
  VideocamRounded,
  ViewComfyOutlined,
  ViewComfyRounded,
} from "@mui/icons-material";
import { Avatar, IconButton, Input, InputAdornment } from "@mui/material";
import { AnyAction, Dispatch } from "@reduxjs/toolkit";
import Peer, { DataConnection, MediaConnection } from "peerjs";
import { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import Logo from "../components/logo";
import VideoPlayer from "../components/videoplayer";
import { User } from "../redux/slices/user";
import { RootState } from "../redux/store";
import socket from "../utils/socket";
import { GET_STREAM, IS_ADMIN, JOIN_LECTURE } from "../utils/socketaction";
import {
  answerCall,
  getScreenStream,
  getUserStream,
  initPeer,
  shareUser,
} from "../utils/webRTC";

interface Paused {
  video: boolean;
  audio: boolean;
}

function LiveLecture() {
  const { lectureId } = useParams();

  const user: User = useSelector((state: RootState): User => state.user);

  const [userStream, setUserStream] = useState<MediaStream | undefined>();
  const [screenStream, setScreenStream] = useState<MediaStream | undefined>();
  const [isHandRaised, setIsHandRaised] = useState<boolean>(false);

  const [isChatOpen, setIsChatOpen] = useState<boolean>(true);
  const [isPresentationView, setIsPresentationView] = useState<boolean>(true);

  const [paused, setPaused] = useState<Paused>({
    audio: true,
    video: true,
  });

  const [isAdmin, setIsAdmin] = useState<boolean>(false);

  const [userPeer, setUserPeer] = useState<Peer>(new Peer());
  const [screenPeer, setScreenPeer] = useState<Peer>(new Peer());

  const [isFullScreen, setIsFullScreen] = useState(false);

  const dispatch: Dispatch<AnyAction> = useDispatch();

  const shareUserStream = (studentId: string) => {
    if (userStream) {
      shareUser(userPeer, userStream, studentId);
    } else {
      getUserStream().then((stream: MediaStream): void => {
        setUserStream(stream);
        console.log(userPeer, studentId);
        shareUser(userPeer, stream, studentId);
      });
    }
  };

  useEffect((): (() => void) => {
    if (userPeer && lectureId) {
      initPeer(userPeer, "user");
      answerCall(
        userPeer,
        lectureId,
        (stream: MediaStream, type: "user" | "screen"): void => {
          console.log("second");
          if (type == "user") {
            setUserStream(stream);
          } else {
            setScreenStream(stream);
          }
        }
      );
    }
    return (): void => {};
  }, [userPeer]);

  useEffect((): (() => void) => {
    if (screenPeer) {
      initPeer(screenPeer, "screen");
    }
    return (): void => {};
  }, [screenPeer]);

  useEffect((): (() => void) => {
    socket.on(IS_ADMIN, (admin: boolean) => {
      setIsAdmin(admin);
      if (admin) {
        socket.on(GET_STREAM, (studentPeerId) => {
          // call the user
          studentPeerId && shareUserStream(studentPeerId);
        });
      }
      console.log(admin);
    });
    return (): void => {};
  }, []);

  return (
    <div className="flex overflow-hidden flex-col w-full h-screen">
      <div className="w-full flex px-10 bg-gray-900 shadow-lg py-3">
        <div className=" flex sm:gap-6 gap-4 divide-x-2 justify-center items-center text-2xl font-semibold text-white">
          <div className="hidden sm:block">
            <Logo />
          </div>
          <div className="md:pl-6 pl-4">Batch Name</div>
          <div className="select-none ring-2 ring-red-500 bg-white text-red-500 font-bold uppercase px-2 rounded-lg">
            Live
          </div>
        </div>
      </div>
      <div className="flex py-4 lg:justify-around lg:flex-row bg-secondary-100 grow h-[calc(100vh-16rem)] flex-col">
        <div className="flex grow gap-3 flex-col justify-between items-center">
          <div
            className={`flex justify-center items-center grow px-4 lg:w-full`}
          >
            <div
              className={`grid ${
                !isChatOpen && "px-10"
              } w-full gap-4 grid-cols-4 grid-row-4`}
            >
              <div
                className={`${
                  isChatOpen ? "col-span-1" : "col-span-3 sm:col-span-1"
                }`}
              >
                {/* secondary player */}
                <VideoPlayer
                  stream={isPresentationView ? userStream : screenStream}
                />
              </div>
              <div
                className={`col-span-3 px-2 grow flex justify-center items-center`}
              >
                {/* primary player */}
                <VideoPlayer
                  isFullScreen={isFullScreen}
                  stream={isPresentationView ? screenStream : userStream}
                />
              </div>
            </div>
          </div>
          <div className="justify-center items-center">
            <div className="py-2 px-6 rounded-full bg-gray-900 text-gray-500 flex gap-2 sm:gap-6 justify-center items-center">
              {isAdmin ? (
                <>
                  <IconButton
                    onClick={(): void => {
                      setPaused(
                        (pp: Paused): Paused => ({
                          ...pp,
                          audio: !pp.audio,
                        })
                      );
                    }}
                    color="inherit"
                  >
                    {paused.audio ? (
                      <MicOffRounded color="inherit" />
                    ) : (
                      <MicRounded color="inherit" />
                    )}
                  </IconButton>
                  <IconButton
                    onClick={(): void => {
                      setPaused(
                        (pp: Paused): Paused => ({
                          ...pp,
                          video: !pp.video,
                        })
                      );
                    }}
                    color="inherit"
                  >
                    {paused.video ? (
                      <VideocamOffRounded color="inherit" />
                    ) : (
                      <VideocamRounded color="inherit" />
                    )}
                  </IconButton>
                  <IconButton
                    onClick={() => {
                      if (!screenStream) {
                        const f: string | null = prompt("fId:");
                        if (screenPeer && f) {
                          getScreenStream().then((stream: MediaStream) => {
                            setScreenStream(stream);
                            const conn: DataConnection = screenPeer.connect(f);
                            conn.on("open", (): void => {
                              const call: MediaConnection = screenPeer.call(
                                f,
                                stream,
                                {
                                  metadata: {
                                    streamType: "screen",
                                  },
                                }
                              );
                              call.on("error", console.error);
                              call.on("close", (): void => {
                                console.log("call closed");
                              });
                            });
                          });
                        }
                      } else {
                        screenStream
                          .getTracks()
                          .forEach((track: MediaStreamTrack): void =>
                            track.stop()
                          );
                        setScreenStream(undefined);
                      }
                    }}
                    color="inherit"
                  >
                    {screenStream ? (
                      <PausePresentationRounded color="inherit" />
                    ) : (
                      <PresentToAllRounded color="inherit" />
                    )}
                  </IconButton>
                  <IconButton
                    onClick={() => {
                      setIsPresentationView((pipv) => !pipv);
                    }}
                    color="inherit"
                  >
                    {isPresentationView ? (
                      <ViewComfyOutlined color="inherit" />
                    ) : (
                      <ViewComfyRounded color="inherit" />
                    )}
                  </IconButton>
                </>
              ) : (
                <IconButton color="inherit">
                  {isHandRaised ? (
                    <PanToolRounded color="inherit" />
                  ) : (
                    <DoNotTouchRounded color="inherit" />
                  )}
                </IconButton>
              )}
              <IconButton
                color="inherit"
                onClick={(): void => {
                  setIsChatOpen((pico: boolean): boolean => !pico);
                }}
              >
                {isChatOpen ? (
                  <SpeakerNotesOffRounded color="inherit" />
                ) : (
                  <ChatRounded color="inherit" />
                )}
              </IconButton>
              <IconButton
                onClick={() => {
                  setIsFullScreen((pifs: boolean): boolean => !pifs);
                  setIsAdmin(true);
                }}
                color="inherit"
              >
                <FullscreenRounded color="inherit" />
              </IconButton>
              <IconButton
                onClick={(): void => {
                  // const f: string | null = window.prompt("fId");
                  // getUserStream().then((stream: MediaStream): void => {
                  //   setUserStream(stream);
                  //   if (userPeer && f) {
                  //     const conn: DataConnection = userPeer.connect(f);
                  //     conn.on("open", (): void => {
                  //       const call: MediaConnection = userPeer.call(f, stream, {
                  //         metadata: {
                  //           streamType: "user",
                  //         },
                  //       });
                  //       call.on("error", console.error);
                  //       call.on("close", (): void => {
                  //         console.log("call closed");
                  //       });
                  //     });
                  //   }
                  // });
                  const f: string | null = window.prompt("fId");
                  f && shareUserStream(f);
                }}
                sx={{
                  bgcolor: "#550000",
                }}
                color="error"
              >
                <div className="flex justify-center items-center grow">
                  <CallEndRounded />
                </div>
              </IconButton>
            </div>
          </div>
        </div>
        <div className="px-3 py-4">
          <div
            className={`sm:h-1/4 ${
              !isChatOpen && "hidden"
            } h-2/5 sm:h-1/4 lg:h-[calc(100%-5rem)] w-full lg:min-w-[20rem]`}
          >
            <div className="py-2 px-4 bg-primary-400 text-white shadow-lg rounded-t-lg uppercase font-semibold select-none">
              live chats
            </div>
            <div className="flex px-4 h-full overflow-auto flex-col bg-primary-400">
              <div className="flex flex-col gap-2 py-2 py-3overflow-auto">
                <div className="flex bg-white rounded-md gap-2 px-4 py-2 items-center">
                  <div>
                    <Avatar />
                  </div>
                  <div className="flex flex-col gap-1">
                    <div className="font-semibold text-sm">Name Name</div>
                    <div className="text-sm">
                      This is some chat message to test the ui responsiveness
                    </div>
                  </div>
                </div>
                <div className="flex bg-white rounded-md gap-2 px-4 py-2 items-center">
                  <div>
                    <Avatar />
                  </div>
                  <div className="flex flex-col gap-1">
                    <div className="font-semibold text-sm">Name Name</div>
                    <div className="text-sm">
                      This is some chat message to test the ui responsiveness
                    </div>
                  </div>
                </div>
                <div className="flex bg-white rounded-md gap-2 px-4 py-2 items-center">
                  <div>
                    <Avatar />
                  </div>
                  <div className="flex flex-col gap-1">
                    <div className="font-semibold text-sm">Name Name</div>
                    <div className="text-sm">
                      This is some chat message to test the ui responsiveness
                    </div>
                  </div>
                </div>
                <div className="flex bg-white rounded-md gap-2 px-4 py-2 items-center">
                  <div>
                    <Avatar />
                  </div>
                  <div className="flex flex-col gap-1">
                    <div className="font-semibold text-sm">Name Name</div>
                    <div className="text-sm">
                      This is some chat message to test the ui responsiveness
                    </div>
                  </div>
                </div>
                <div className="flex bg-white rounded-md gap-2 px-4 py-2 items-center">
                  <div>
                    <Avatar />
                  </div>
                  <div className="flex flex-col gap-1">
                    <div className="font-semibold text-sm">Name Name</div>
                    <div className="text-sm">
                      This is some chat message to test the ui responsiveness
                    </div>
                  </div>
                </div>
                <div className="flex bg-white rounded-md gap-2 px-4 py-2 items-center">
                  <div>
                    <Avatar />
                  </div>
                  <div className="flex flex-col gap-1">
                    <div className="font-semibold text-sm">Name Name</div>
                    <div className="text-sm">This is some chat message</div>
                  </div>
                </div>
                <div className="flex bg-white rounded-md gap-2 px-4 py-2 items-center">
                  <div>
                    <Avatar />
                  </div>
                  <div className="flex flex-col gap-1">
                    <div className="font-semibold text-sm">Name Name</div>
                    <div className="text-sm">
                      This is some chat message to test the ui responsiveness
                    </div>
                  </div>
                </div>
                <div className="flex bg-white rounded-md gap-2 px-4 py-2 items-center">
                  <div>
                    <Avatar />
                  </div>
                  <div className="flex flex-col gap-1">
                    <div className="font-semibold text-sm">Name Name</div>
                    <div className="text-sm">
                      This is some chat message to test the ui responsiveness
                    </div>
                  </div>
                </div>
                <div className="flex bg-white rounded-md gap-2 px-4 py-2 items-center">
                  <div>
                    <Avatar />
                  </div>
                  <div className="flex flex-col gap-1">
                    <div className="font-semibold text-sm">Name Name</div>
                    <div className="text-sm">
                      This is some chat message to test the ui responsiveness
                    </div>
                  </div>
                </div>
              </div>
            </div>
            <div className="py-2 px-4 bg-primary-400 rounded-b-md">
              <Input
                // value={message}
                // onChange={(e) => {
                //   setMessage(e.target.value);
                // }}
                placeholder="message"
                inputProps={{
                  className: "no-scrollbar",
                }}
                fullWidth={true}
                className="bg-primary-400"
                multiline
                maxRows={3}
                endAdornment={
                  <InputAdornment position="end">
                    <IconButton>
                      <SendRounded className="cursor-pointer" />
                    </IconButton>
                  </InputAdornment>
                }
              />
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

export default LiveLecture;

and finally this is my server code for socket.io connection

socket.on(
    JOIN_LECTURE,
    ({ lectureId, peerId }: { lectureId: string; peerId: string }): void => {
      // verify user identity

      socket.join(lectureId);
      socket.to(lectureId).emit(GET_STREAM, peerId);
      console.log(colors.blueBright(`[${socket.id}] joined ${lectureId}`));

      // check if the connected user is admin;
      // temporarily setting the first user as admin; pink;
      const clientsInRoom: Set<string> | undefined =
        io.sockets.adapter.rooms.get(lectureId);
      const viewerCount: number = (clientsInRoom ? clientsInRoom.size : 0) - 1;
      const isAdmin: boolean = !viewerCount;
      socket.emit(IS_ADMIN, isAdmin);
      // console.log(io.sockets.adapter.rooms);
      socket.addListener(disconnect, () => {
        console.log("disconnect");
        // temporarily transferring admin rights to the next socket; pink;
        if (isAdmin) {
          const nextAdmin = clientsInRoom?.entries().next();
          if (nextAdmin) {
            console.log(nextAdmin, "nextAdmin");
            io.sockets.to(nextAdmin.value).emit(IS_ADMIN, true);
          }
        }
      });
    }
  );```

Solution

  • I fixed the error after some research and answer my own question.

    In general this error comes from answering the same call multiple times(or so I think from this question's answers). you can see that if you are not facing this problem in ReactJs.

    I found out that React calls useEffect twice in order to detect any problems with your code and warn you about them.(find more about in this question)

    So, I simply removed the StrictMode wrapper from my main.tsx like:

    ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
        <Provider store={store}>
          <CookiesProvider>
            <ThemeProvider theme={MuiTheme}>
              <App />
            </ThemeProvider>
            <ToastContainer theme="dark" position="bottom-right" />
          </CookiesProvider>
        </Provider>
    );
    

    and now my app works like it should have.

    NOTE: Please see the cons of removing StrictMode in react.