Thursday, 21 January 2021

Managing Chat History with Socket.io/React/Node

I am working on a real time chat feature in my application using Socket.io. I have the sockets set up, and users are able to send messages to each other.

I defined the following helper functions to assist in setting up a socket, and retrieving the chat history from the server:

export const initiateChatSocket = (room, username) => {
  socket = io("http://localhost:5000/chat", {
    transports: ["websocket"],
  });
  console.log(`Connecting socket...`);
  if (socket && room) socket.emit("join", room, username);
};

export const loadInitialChat = (cb) => {
  if (!socket) return true;

  socket.on("joinResponse", (msg) => cb(null, msg));
};

export const subscribeToChat = (cb) => {
  if (!socket) return true;
  socket.on("chat", (msg) => {
    console.log("Websocket event received!");
    return cb(null, msg);
  });
};

export const sendMessage = (room, message) => {
  if (socket) socket.emit("chat", { message, room });
};

Adapted from this guide.

Here is the backend code:

chatNsp.on("connection", (socket) => {
  let socketRoom;
  socket.on("join", (roomID, userID) => {
    socket.join(roomID);
    socketRoom = roomID;
    socket.emit("joinResponse", socketHistory[socketRoom]);
  });

  socket.on("chat", (data) => {
    socket.broadcast.to(socketRoom).emit("chat", data.message);
    socketHistory[socketRoom] = socketHistory[socketRoom]
      ? [data.message, ...socketHistory[socketRoom]]
      : [data.message];
  });
});

When a user connects, they can successfully receive the chat history for the room. That is being stored in the socketHistory object:

socketHistory {
  '0b1b05e2-1c50-42b3-baab-59b5548c6930': [ 'Test', 'Baz', 'Foo', 'Bar', 'Foo' ]
}

There is a disconnect between the two components that use the chat component, LiveStream and ViewStream. The chat in the LiveStream component gets overwritten with each new message. However the ViewStream component maintains the full history as expected. The full history is still available on the server. In my chat component, I am adding the message to my chat array in state before sending the message, but I can't seem to track down the problem.

const Chat = ({ username, roomId, socket }) => {
  const [message, setMessage] = useState("");
  const [chat, setChat] = useState([]);
  
  useEffect(() => {
    loadInitialChat((err, data) => {
      if (err) return;

      if (data !== null) {
        setChat(data);
      }
    });
  }, [roomId]);

  useEffect(() => {
    subscribeToChat((err, data) => {
      if (err) return;
      
      setChat([...chat, data]);
    });
  }, [chat]);

  const handleChange = (event) => {
    setMessage(event.target.value);
  };

  const handleMessage = (event) => {
    sendMessage(roomId, message);
  };

  return (
    <div>
      <TextField
        placeholder="Send a Message"
        value={message}
        onChange={handleChange}
      ></TextField>
      <Button
        onClick={() => {
          setChat([...chat, message]);
          sendMessage(roomId, message);
          setMessage("");
        }}
      >
        Send
      </Button>
      {chat ? chat.map((m, i) => <p key={i}>{m}</p>) : ""}
    </div>
  );
};

Here is my LiveStream component:


...removed for brevity

const LiveStream = ({ username, roomId, peer, peerId }) => {
  // Set the user's video stream in state once given permission on component load
  const [videoStream, _setVideoStream] = useState(null);
  const [socketRegistered, setSocketRegistered] = useState(false);
  const videoStreamRef = useRef(videoStream);
  const setVideoStream = (data) => {
    videoStreamRef.current = data;
    _setVideoStream(data);
  };
  const chatSocket = initiateChatSocket(roomId, username);

  useEffect(() => {
    videoGrid = document.getElementById("video-grid");
    viewerVideoGrid = document.getElementById("viewer-grid");
    const myVideo = document.createElement("video");
    const configOptions = { video: true, audio: false };
    myVideo.muted = true;
    async function enableStream() {
      try {
        const stream = await navigator.mediaDevices.getUserMedia(configOptions);
        setVideoStream(stream);
        setupMediaRecorder(stream);
        addVideoStream(videoSocket, chatSocket, myVideo, stream);
      } catch (err) {
        console.error(err);
      }
    }

    if (!videoStream) {
      enableStream();
    } else {
      return function cleanup() {
        videoStream.getTracks().forEach((track) => {
          track.stop();
        });
        disconnectSocket();
      };
    }
  }, [videoStream]);

  useEffect(() => {
    videoSocket = initiateVideoSocket(roomId, username, peerId);

    console.log(videoSocket);
    // When a viewer connects, this event is emitted and the streamer will connect to the viewer.
    videoSocket.on("viewer-connected", (id, viewer, viewerPeerId) => {
      console.log("inside videoSocket listener", videoStream);
      connectToNewViewer(viewerPeerId, videoStream);
    });
  }, [videoStream]);

   ...removed for brevity

  // Function to connect to new viewer using PeerJS. Inputs are the peerID of the viewer, and the streamer's video stream to send
  // to the viewer.
  function connectToNewViewer(viewerId, stream) {
    const call = peer.call(viewerId, stream);
    const video = document.createElement("video");
    call.on("stream", (userVideoStream) => {
      addViewerStream(video, userVideoStream);
    });
    call.on("close", () => {
      video.remove();
    });

    peers[viewerId] = call;
  }

  // Function to add the streamer's video to the LiveStream component on load. Called after permission is given to access the
  // camera and mic.
  function addVideoStream(vs, cs, video, stream) {
    video.srcObject = stream;
    video.addEventListener("loadedmetadata", () => {
      video.play();
    });
    videoGrid.append(video);
  }

  function addViewerStream(vs, cs, video, stream) {
    video.srcObject = stream;
    video.addEventListener("loadedmetadata", () => {
      video.play();
    });
    viewerVideoGrid.append(video);
  }


  return (
    <>
      <Grid container direction="column" justify="space-between" spacing={10}>
        <Grid container>
          <Grid item xs={8} sm={6}>
            <div id="video-grid"></div>
            <StreamControls
              mediaRecorder={mediaRecorder}
              stoppedVideo={stoppedVideo}
            />
          </Grid>
          <Grid item sm={4}>
            <Chat username={username} roomId={roomId} socket={chatSocket} />
          </Grid>
        </Grid>
        <Grid container>
          <div id="viewer-grid"></div>
        </Grid>
      </Grid>
    </>
  );
};
...removed for brevity
);

And here is ViewStream:


...removed for brevity

const ViewStream = ({ peer, streamerPeerId, ...props }) => {
  const history = useHistory();
  const location = useLocation();
  const room = props.match.params.id;
  const viewerPeer = peer;
  const viewerVideoSocket = initiateVideoSocket(
    room,
    props.username,
    props.viewerPeerId
  );

  const viewerChatSocket = initiateChatSocket(room, props.username);
  let streamerVideoDiv;

  useEffect(() => {
    viewerVideoSocket.emit("viewer-connected", room, props.username, peer.id);

    viewerPeer.on("call", (call) => {
      call.answer();
      call.on("stream", (stream) => {
        streamerVideoDiv = document.getElementById("streamer-video");
        const content = document.createElement("video");
        content.srcObject = stream;
        content.addEventListener("loadedmetadata", () => {
          content.play();
        });
        streamerVideoDiv.append(content);
      });
    });

    return () => {
      disconnectSocket();
    };
  }, []);

  return (
    <>
      <Grid container direction="column" justify="space-between" spacing={10}>
        <Grid item xs={8} sm={6}>
          <div id="streamer-video"></div>
        </Grid>
        <Grid item sm={4}>
          <Chat
            username={props.username}
            roomId={props.match.params.id}
            socket={viewerChatSocket}
          />
        </Grid>
      </Grid>
    </>
  );
});


from Managing Chat History with Socket.io/React/Node

No comments:

Post a Comment