DEV Community

Cover image for Socket.io, WebRTC, Node, Express, MongoDB, and Vue (Final Part)
Kevin Odongo
Kevin Odongo

Posted on

Socket.io, WebRTC, Node, Express, MongoDB, and Vue (Final Part)

Hey Dev's

So today let us complete this tutorial together with some practical work. Those who will be interested to learn more about WebRTC, AWS Websocket, Node, Vue/React Native will enjoy the course when it is out.

This will be quite interesting.

To those who will get lost through the code. This is a simple task just understand the logic, grab a bit of code, and try to do it yourself. I have tried to explain each function.

What we need to achieve in the meeting application is as follows:
This layout looks simple but there is an order to follow.

Alt Text

Backend

Let us create all the files that we will require. This will be our backend structure.

Alt Text

Create your node project by running the following command:

npm init
Enter fullscreen mode Exit fullscreen mode

Install the required packages.

yarn add mongoose express socket.io body-parser dotenv cors jest
Enter fullscreen mode Exit fullscreen mode

Create the following files in your application structure.

/config/db_config.js

This will expose our mongo database URL from the environment file. Ensure you create a .env file and save the mongo database URL.

module.exports = {
    url: process.env.MONGO_DB_URI
  };
Enter fullscreen mode Exit fullscreen mode

/controller/meet.controller.js

This will persist users' session-id to the database.

const db = require("../models");
const Meet = db.meet;

// create a meet
exports.createonemeet = (req, res) => {
  const meet = new Meet({
    name: req.body.name ? req.body.name : "User",
    meetingid: req.body.meetingid,
    sessionid: req.body.sessionid,
  });

  // Save new meet in the database
  meet
    .save(meet)
    .then(data => {
      res.send(data);
    })
    .catch(err => {
      res.status(500).send({
        message:
          err.message || "Some error occurred while creating the meeting."
      });
    });
};

// retrieve all meet from the database for that meeting.
exports.findallmeet = (req, res) => {
  const id = req.params.id;
  var condition = { meetingid: id }

  Meet.find(condition)
    .then(data => {
      res.send(data);
    })
    .catch(err => {
      res.status(500).send({
        message:
          err.message || "Some error occurred while retrieving meets."
      });
    });

};

// retrieve one meet from the database for that meeting.
exports.findonemeet = (req, res) => {
  const id = req.params.id;
  var condition = { sessionid: id }

  Meet.findOne(condition)
    .then(data => {
      res.send(data);
    })
    .catch(err => {
      res.status(500).send({
        message:
          err.message || "Some error occurred while retrieving sessions."
      });
    });

};

// delete a meet with the specified id in the request
exports.deleteonemeet = (req, res) => {
  const id = req.params.id;
  var condition = { sessionid: id }

  Meet.deleteOne(condition)
    .then(data => {
      if (!data) {
        res.status(404).send({
          message: `Cannot delete meet with id=${id}!`
        });
      } else {
        res.send({
          message: "Meet was deleted successfully!"
        });
      }
    })
    .catch(err => {
      res.status(500).send({
        message: "Could not delete meet with id=" + id
      });
    });

};

Enter fullscreen mode Exit fullscreen mode

/controller/session.controller.js

This will handle chat content.

const db = require("../models");
const Session = db.session;

// create a session
exports.createonesession = (req, res) => {
  // check for message or attachments
  if(!req.body.message && !req.body.attachment){
    return res.status(404).send({ message: "No message or attachment!" });
  }
  // session
  const session = new Session({
    message: req.body.message,
    attachment: req.body.attachment,
    meetingid: req.body.meetingid,
    sessionid: req.body.sessionid
  });

  // Save new session in the database
  session
    .save(session)
    .then(data => {
      res.send(data);
    })
    .catch(err => {
      res.status(500).send({
        message:
          err.message || "Some error occurred while creating the new message."
      });
    });
};

// retrieve all session from the database for that meeting.
exports.findallsession = (req, res) => {
  const id = req.params.id;
  var condition = { meetingid: id }

  Session.find(condition)
    .then(data => {
      res.send(data);
    })
    .catch(err => {
      res.status(500).send({
        message:
          err.message || "Some error occurred while retrieving sessions."
      });
    });

};

// retrieve one session from the database for that meeting.
exports.findonesession = (req, res) => {
  const id = req.params.id;
  //var condition = { sessionid: id }

  Session.findById(id)
    .then(data => {
      res.send(data);
    })
    .catch(err => {
      res.status(500).send({
        message:
          err.message || "Some error occurred while retrieving sessions."
      });
    });

};

// delete a session with the specified id in the request
exports.deleteonesession = (req, res) => {
  const id = req.params.id;

  Session.findByIdAndRemove(id)
    .then(data => {
      if (!data) {
        res.status(404).send({
          message: `Cannot delete contents with id=${id}!`
        });
      } else {
        res.send({
          message: "Session was deleted successfully!"
        });
      }
    })
    .catch(err => {
      res.status(500).send({
        message: "Could not delete session with id=" + id
      });
    });

};

// delete all session for the meeting.
exports.deleteallsession = (req, res) => {
  const id = req.params.id;
  var condition = { sessionid: id }

  Session.deleteMany(condition)
  .then(data => {
    res.send({
      message: `${data.deletedCount} Sessions were deleted successfully!`
    });
  })
  .catch(err => {
    res.status(500).send({
      message:
        err.message || "Some error occurred while removing all sessions."
    });
  });
};
Enter fullscreen mode Exit fullscreen mode

/models/meet.model.js

This will be the schema for the meet database.

module.exports = mongoose => {
  const Meet = mongoose.model(
    "Meet",
    mongoose.Schema(
      {
        name: String, // session name
        meetingid: String, // meeting id
        sessionid: String, // socket id
      },
      { timestamps: true }
    )
  );
  return Meet;
};
Enter fullscreen mode Exit fullscreen mode

/model/session.model.js

This will be the schema for the session database.

module.exports = mongoose => {
  const Session = mongoose.model(
    "Session",
    mongoose.Schema(
      {
        message: String,
        attachment: String,
        sessionid: String, // user session id
        meetingid: String // meeting id
      },
      { timestamps: true }
    )
  );
  return Session;
};
Enter fullscreen mode Exit fullscreen mode

/models/index.js

This will expose both the session and meet database.

const dbConfig = require("../config/db.config");

const mongoose = require("mongoose");
mongoose.Promise = global.Promise;

const db = {};
db.mongoose = mongoose;
db.url = dbConfig.url;
// databases
db.meet = require("./meet.model.js")(mongoose);
// db.user = require("./user.model")(mongoose);
db.session = require("./session.model")(mongoose);

module.exports = db;
Enter fullscreen mode Exit fullscreen mode

/routes/meet.js

This folder will hold the routes for the meeting. Ensure you import this file in the index.js file.

const express = require("express")
const router = express.Router()
const meet = require("../controller/meet.controller");

// Create a new blog
router.post("/", meet.createonemeet);

// retrieve all meets for the meeting
router.get("/all/:id", meet.findallmeet);

// retrieve one meets for the meeting
router.get("/:id", meet.findonemeet);

// delete a single meet
router.delete("/:id", meet.deleteonemeet)


module.exports = router
Enter fullscreen mode Exit fullscreen mode

/routes/session.js

This folder will hold the routes for the meeting. Ensure you import this file in the index.js file.

const express = require("express")
const router = express.Router()
const session = require("../controller/session.controller");

// Create a new session
router.post("/", session.createonesession);

// find all session
router.get("/all/:id", session.findallsession);

// retrieve one session for the meeting
router.get("/:id", session.findonesession);

// delete a single session
router.delete("/:id", session.deleteonesession)

// delete all session for spefic session
router.delete("/all/:id", session.deleteallsession);

module.exports = router
Enter fullscreen mode Exit fullscreen mode

index.js

This file will be the entry point for our application. In this file, we will define the socket.io logic. You can either put the logic in the front-end or back-end. For chat content, the request will have to come from the front-end because we want to add attachments capability. Remember attachment will be saved elsewhere while the file name or id will be saved in MongoDB.

Our first connection will come from the socket listening to the joined channel/message. Once a user joins they will get a unique socket id. Let us persist this socket id and the meeting name they have joined. I want you to NOTE In this channel, we are using socket.to while sending back our message. This is because we want everyone to be notified of a new user joining except the sender. The person who joins will not receive this notification.

This process will take place as follows. When Peer A joined they notified everyone but because no one was available in the meeting they did not receive any message. When Peer B joins, Peer A will get the notification of Peer A joining. This will trigger the rest of the exchanges in the meeting.

Below is an example of how you can add the logic to persist session-id and meeting name in the backend. Ensure you send the meeting name from the front-end.

// join a new meeting
  socket.on('joined', async (data) => {
    let meetingid = JSON.parse(data).meetingid
    let username = JSON.parse(data).username
    console.log("joined", meetingid)

    // persist socket id
    const new_meet = {
       name: username,
       meetingid: meetingid,
       sessionid: socket.id
    }
    await Meet.createonemeet(meet)
    if(meetingid !== null){
      socket.join(meetingid);
      // notify everyone of a new user
      socket.to(`${meetingid}`).emit("joined", `${socket.id}`)
    }
  });
Enter fullscreen mode Exit fullscreen mode

The other channels/messages in the socket.io will be offer_message and answer_message. This will be broadcasted to individuals and not the entire meeting so we will be using io.to instead of socket.to.

The last one will be sendmessage this will notify everyone of a new message which will trigger a call to the database to fetch for that specific message. In this channel, we can either use io.in or socket.to to send to everyone including the sender or everyone excluding the sender (if you use this then you need to update the sender message array from the front-end when they send that particular message).

index.js

const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
require('dotenv').config()

const app = express();

// parse application/json
app.use(bodyParser.json())

// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: true }))

// use cors options
app.use(cors())
app.use(require('serve-static')(__dirname + '/../../public'));
// socket io
const httpServer = require("http").createServer(app);
const io = require("socket.io")(httpServer, {
  cors: {
    origin: "*",
    methods: ["GET", "POST"]
  }
});

const Meet = Meet.db
io.on("connection", (socket) => {

  // join a new meeting
  socket.on('joined', async (data) => {
    let meetingid = JSON.parse(data).meetingid
    //let username = JSON.parse(data).username
    //console.log("joined", meetingid)
    //const new_meet = {
       //name: username,
       //meetingid: meetingid,
       //sessionid: socket.id
    //}
    //await Meet.createonemeet(meet)
    if(meetingid !== null){
      socket.join(meetingid);
      // notify everyone of a new user
      socket.to(`${meetingid}`).emit("joined", `${socket.id}`)
    }
  });

  socket.on('offer_message', (data) => {
    let sessionid = JSON.parse(data).offerto
    console.log("[OFFER] Send to session id", sessionid)
    if(data !== null){
      // notify everyone of a new user
      io.to(`${sessionid}`).emit("offer_message", `${data}`)
    }
  });


  socket.on('answer_message', (data) => {
    let sessionid = JSON.parse(data).offerto
    console.log("[ANSWER] Send to session id", sessionid)
    if(data !== null){
      // notify everyone of a new user
      io.to(`${sessionid}`).emit("answer_message", `${data}`)
    }
  });


  // send a message
  socket.on('send', (data) => {
    let meetingid = JSON.parse(data).meetingid
    let sessionid = JSON.parse(data).sessionid
    if(data !== null){
      socket.join(meetingid);
      // notify everyone of a new message
      socket.to(`${meetingid}`).emit("sendmessage", `${sessionid}`)
    }
  });

  // disconnect
  socket.on("disconnect", (data) => {
    if(data !== null){
      // notify everyone of a user has exited
      socket.to(`${data}`).emit("exitmeeting",  'someone has exited')
    }
  });
});



// mongo db database connection
const db = require("./app/models");
db.mongoose
  .connect(db.url, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    useFindAndModify: false,
    useCreateIndex: true
  })
  .then(() => {
    console.log("Connected to the database!");
  })
  .catch(err => {
    console.log("Cannot connect to the database!", err);
    process.exit();
  });


// routes
const meet = require('./app/routes/meet')
const session = require('./app/routes/session')
app.use('/meet', meet)
app.use('/session', session)

// listening port
const PORT = process.env.PORT || 3000;
httpServer.listen(PORT);
// app.listen(PORT, () => {
//     console.log(`Server is running on port ${PORT}.`);
// });
Enter fullscreen mode Exit fullscreen mode

Front-End

In the front-end, I used Vue for my application, and to avoid being bias I will explain how the front-end major functions work so anyone following this tutorial with a different framework can follow along. Install the socket.io client-side package.

yarn add socket.io-client
Enter fullscreen mode Exit fullscreen mode

Home Component

Once a user inputs the name let the following function handle the submit of your form. The function will get your hostname using windows.location.href and add the meeting name (This will create the meeting URL), thereafter re-direct the user to the session component.

submit(){
  let application_url = window.location.href; // get the current 
  href
  let meeting_url = `${application_url}session/${meeting_name}`; 
  this.$router.push("/sessions")
} 
Enter fullscreen mode Exit fullscreen mode

Session component

Once a user clicks to join a meeting. Have a function to validate the meeting name. I used encryption and decryption to handle this. You can use a different approach according to your application needs.

// data in vue
data(){
   item: {
     user_name: "" // meeting URL
   }, 
    messageContent: {
        message: null,
        attachment: null
   }
   socket: {
      signalClient: null
      peerConnection: null
      localStream: null
      remoteSteam: []
   }
}

// join meeting
joinmeeting(){
   this.loading = true;
   // * get the meeting meeting
   let split_meeting_url = this.meeting_code.split("/");
   let meeting_name = split_meeting_url.pop();

   this.socket.signalClient = io(
       "http://localhost:3000/"
   );
   // let us listen to joined message
   this.socket.signalClient.on("connect", async(data) => {
       // This is if you will be persisting user session id from front end
       //let value = {
         // user_name: this.item.user_name, //
         // meeting_url: meeting_name, // meeting id
         // socket_id: this.socket.signalClient.id // socket id
        //};
        //if (value.user_name !== null) {
          //await saveonemeeting(value); // persist session in db
          //this.item.user_name = null;
        //}

        // * Joining meeting this will handle duplication of route when changing from one child component to another.
        this.status = "Joining";
        let path = `/session/${meeting_name}`;
        if (this.$route.path !== path) this.$router.replace(path);

        // * Fetch all that have joined the meeting
        const sessions_response = await fetchallmeetings(`${meeting_name}`);
        this.sessions = [...sessions_response];
        // * Fetch all messages and attachments (sessions)
        const contents_response = await fetchallsessions(`${meeting_name}`);  
        this.messsages = [...contents_response]

        // * this will render the meeting component from the join component. Remember both are children in the session component.
        this.loading = false;
        this.meeting = true;
        this.status = "Processing";

        // * notify joining meeting
        this.socket.signalClient.emit(
          "joined",
          JSON.stringify({
            name: this.item.user_name // ensure its not cleared if you persisted from the front end
            meetingid: `${meeting_name}`,
            sessionid: `${this.socket.signalClient.id}`
          })
        );

        // * initialize the camera
        // * ensure dom is ready in vue we use this.$nextTick()
        this.$nextTick(async () => {
          let localView = document.getElementById("local_view");
          //console.log(localView);

          this.socket.localStream = await navigator.mediaDevices.getUserMedia(
            this.constraints
          );
          localView.srcObject = this.socket.localStream;
          // this will initialize the side section which has all the videos
          this.initializemeeting(
            this.socket.signalClient.id,
            this.socket.localStream
          );
          // !end

          // * Get the video and audio tracks streams
          const audioTracks = localView.srcObject.getAudioTracks();
          const videoTracks = localView.srcObject.getVideoTracks();
          if (audioTracks.length > 0) {
            //console.log(`Using audio device: ${audioTracks[0].label}`);
          }
          if (videoTracks.length > 0) {
            //console.log(`Using video device: ${videoTracks[0].label}`);
          }
        });
        //!end
        this.socket.signalClient.connect();
   }
   // listen to new users joining
   this.socket.signalClient.on("joined", data => {
     ....
  })

}

Enter fullscreen mode Exit fullscreen mode

Joined message channel

NOTE the following at the end. Once we have connected we are listening to joined channel/message.

this.socket.signalClient.on("joined", data => {
     ....
})
Enter fullscreen mode Exit fullscreen mode

Once we have connected and emitted to everyone the socket id we end by listening for joined messages(notification). When a new user (Peer B) joins, (Peer A) will get a notification and this will lead to the next stage of the process.

Peer A will start a RTCpeerConnection, create an Offer and set it to his/her locaDescription, and start gathering Ice Candidates.

There are two ways of handling this. Send the ice Candidates as they are gathered which is called the Trickle or wait for all ice Candidates to be gathered and send an offer containing everything.

The pros of Trickle are it's fast but will require you to be careful with Peer B steps or else you will be getting errors all the time. Without trickle enabled the process has a slight delay but you are guaranteed connection.

Without Trickle

You will NOTE we are listening to icegatheringstatechange and once it is complete we send the whole localDescription. You will also NOTE we are sending back offerfrom and offerto so we can redirect this to the right user (offerto) in the backend to the user who joined.

this.socket.on("joined", data => {
    // * fetch the user who has joined details
     const joined_user = await fetchonemeeting(data);
     this.sessions.push(joined_user);

          // * create rtc session
          const configuration = {
            iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
          };
          this.socket.peerConnection = new RTCPeerConnection(configuration);
          let localView = document.getElementById("local_view");
          //console.log("[user]", localView, localView.srcObject);
          localView.srcObject
            .getTracks()
            .forEach(track =>
           this.socket.peerConnection.addTrack(track, localView.srcObject)
            );
          // ! remote tracks
          this.socket.peerConnection.ontrack = event => {
            // Have a function to initialize the meeting in the side.
            this.initializemeeting(data, event.streams[0]);
          };
          //!end

          // ? ICE GATHERING WITHOUT TRICKLE
          this.socket.peerConnection.addEventListener(
            "icegatheringstatechange",
            event => {
              if (event.target.iceGatheringState === "complete") {
                this.socket.signalClient.emit(
                  "offer_message",
                  JSON.stringify({
                    desc: this.socket.peerConnection.localDescription,
                    offerfrom: `${this.socket.signalClient.id}`,
                    offerto: `${data}` // ? send offer to
                  })
                );
              }
            }
          );
})
Enter fullscreen mode Exit fullscreen mode

With Trickle

With this, we will be sending the offer and iceCandidates as they are generated. Replace all the code from // ? ICE GATHERING WITHOUT TRICKLE with below.

// ? WITH TRICLE ENABLED

          // * create an offer and send
          await this.socket.peerConnection.setLocalDescription(
            await this.socket.peerConnection.createOffer({
              offerToReceiveAudio: true,
              offerToReceiveVideo: true
            })
          );

          this.socket.peerConnection.onicecandidate = ({ candidate }) =>
            this.socket.signalClient.emit(
              "offer_message",
              JSON.stringify({
                desc: {
                  offer: this.socket.peerConnection.localDescription,
                  icecandidate: { candidate }
                },
                offerfrom: `${this.socket.signalClient.id}`,
                offerto: `${data}` // ? send offer to
              })
            );
        }
Enter fullscreen mode Exit fullscreen mode

Offer message

Once Peer B receives an offer they will need to set it to the remoteDescription of their peer Connection.

Without trickle once they set it to remoteDescription they will have the offer and iceCandidates from Peer A. What they will do is just generate an answer, set it to their localDescription, gather iceCandidates and send it to Peer A.

With trickle enabled it is tricky and be careful with this part. The iceCandidates might arrive before the offer has arrived and therefore if you try adding them to your peer Connection before setting the offer to your remoteDescription this connection will fail. Some browsers might not allow trickle

The trick to handle this is to add an event listener to listen on canTrickleIceCandidates. Once this is true then you can addiceCandidates.

// * listen to users offers and create an answer
      this.socket.signalClient.on("offer_message", async data => {
        const response = JSON.parse(data);
        this.$nextTick(async () => {
          // * Get the video and audio tracks streams
          let localView = document.getElementById("local_view");
          this.socket.localStream = await navigator.mediaDevices.getUserMedia(
            this.constraints
          );
          localView.srcObject = this.socket.localStream;
          const audioTracks = localView.srcObject.getAudioTracks();
          const videoTracks = localView.srcObject.getVideoTracks();
          const peerTracks = localView.srcObject.getTracks();
          if (audioTracks.length > 0) {
            //console.log(`Using audio device: ${audioTracks[0].label}`);
          }
          if (videoTracks.length > 0) {
            //console.log(`Using video device: ${videoTracks[0].label}`);
          }
          //!end

          // * create rtc connection
          const configuration = {
            iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
          };
          this.socket.peerConnection = new RTCPeerConnection(configuration);
          // ! remote tracks
          this.socket.peerConnection.ontrack = event => {
            this.initializemeeting(response.offerfrom, event.streams[0]);
          };
          //!end

          if (response.desc) {
            //console.log("[user] Offer", response);
            // * set offer to remote

            if (response.desc.offer) {
              await this.socket.peerConnection
                .setRemoteDescription(response.desc.offer)
                .catch(error => {
                  if (error) return;
                });
            }

            peerTracks.forEach(track =>
              this.socket.peerConnection.addTrack(track, localView.srcObject)
            );
            // * create an answer set to local description and send
            await this.socket.peerConnection.setLocalDescription(
              await this.socket.peerConnection.createAnswer({
                offerToReceiveAudio: true,
                offerToReceiveVideo: true
              })
            );

            // * send a answer and candidate
            this.socket.peerConnection.onicecandidate = ({ candidate }) =>
              this.socket.signalClient.emit(
                "answer_message",
                JSON.stringify({
                  desc: {
                    answer: this.socket.peerConnection.localDescription,
                    icecandidate: { candidate }
                  },
                  offerfrom: `${this.socket.signalingClient.id}`,
                  offerto: `${response.offerfrom}` // ? send answer to
                })
              );

            // * add ice candidates
            if (this.socket.peerConnection.canTrickleIceCandidates === true) {
              //console.log("[user] Candidate", response.desc.icecandidate);
              await this.socket.peerConnection
                .addIceCandidate(response.desc.icecandidate)
                .catch(error => {
                  if (error) return;
                });
            }

            // ? ICE GATHERING WITHOUT TRICKLE
            //   this.socket.peerConnection.addEventListener(
            //     "icegatheringstatechange",
            //     event => {
            //       if (event.target.iceGatheringState === "complete") {
            //         this.socket.signalClient.emit(
            //           "answer_message",
            //           JSON.stringify({
            //             desc: this.socket.peerConnection.localDescription,
            //             offerfrom: `${this.socket.signalingClient.id}`,
            //             offerto: `${response.offerfrom}` // ? send answer to
            //           })
            //         );
            //       }
            //     }
            //   );
            //   console.log("[user] peer connection", this.socket.peerConnection);
          }
        });
        this.socket.signalClient.connect();
      });

Enter fullscreen mode Exit fullscreen mode

Answer message

Once peer A receives an answer they will set it to their remoteDescription and add the ice Candidates from Peer B. At this stage, the connection will be complete. Both parties will be receiving video and audio from each other.

 // * listen to answers and set to remote description
      this.socket.signalClient.on("answer_message", async data => {
        const response = JSON.parse(data);

        if (response.desc) {
          // * set remote description and ice
          //console.log("[user] Answer", response);
          if (response.desc.answer) {
            await this.socket.peerConnection
              .setRemoteDescription(response.desc.answer)
              .catch(error => {
                if (error) return;
              });
          }

          if (this.socket.peerConnection.canTrickleIceCandidates === true) {
            //console.log("[user] Candidate", response.desc.icecandidate);
            await this.socket.peerConnection
              .addIceCandidate(response.desc.icecandidate)
              .catch(error => {
                if (error) return;
              });
          }
        }

        this.socket.signalClient.connect();
      });
Enter fullscreen mode Exit fullscreen mode

sendmessage.

For attachments, you can use AWS Simple Storage or any product you can use. Just ensure you save the attachment filename in your database so you can use it to retrieve the attachment from wherever you have decided to store it.

// * send message
    async sendmessage() {
      // * retrive session details
      let split_meeting_url = window.location.href.split("/");
      let value = {
        message: this.messageContent.message,
        sessionid: this.user.signalClient.id, // user session id
        meetingid: split_meeting_url.pop() // meeting id
      };
      const session_response = await saveonesession(value);
      this.messages.push(session_response);
      this.messageContent = {
        message: null,
        attachment: null
      };
      // * send a message
      this.socket.signalClient.emit(
        "send",
        JSON.stringify({
          sessionid: session_response._id,
          meetingid: value.meetingid // meeting id
        })
      );

      this.socket.signalClient.connect();
    },
Enter fullscreen mode Exit fullscreen mode

To exit the meeting you need to disconnect the video tracks. You can either delete everything from the front end or backend.

// * exit meetings
    async exitsession() {
      // * disconnect media
      await this.disconnectmedia();
      // * delete all contents with session id
      if (this.user.signalingClient) {
        await deleteonemeeting(this.socket.signalClient.id);
        await deleteallsession(this.socket.signalClient.id);
        // * disconnect from meeting
        this.socket.signalClient.on("disconnect");

      }
      let path = "/";
      if (this.$route.path !== path) this.$router.push(path);
    },
    // * disconnect media
    disconnectmedia() {
      let localView = document.getElementById("local_view");
      if (localView !== null) {
        if (localView.srcObject) {
          localView.srcObject.getTracks().forEach(track => track.stop());
        }
      }
      let meeting_views = document.querySelectorAll(".meeting-streams");
      meeting_views.forEach(e => {
        if (e.srcObject) {
          e.srcObject.getTracks().forEach(track => track.stop());
        }
      });
      this.sessions = [];
    },
Enter fullscreen mode Exit fullscreen mode

That's it you will have both video and chat capability. For many users, you will need to have a good backend infrastructure that can scale.

Here is documentation about WebRTC https://webrtc.org/.
Here is documentation about socket.io https://socket.io/docs/v3.

With socket.io the trick is you can listen to any changes and let that trigger a function in the front end or backend. This can be a great way of achieving real-time data streaming.

WebRTC is quite powerful you can achieve a lot with it. Dig through the documentation.

If you are working on a project and need assistance reach out. (Vue, React, and React Native)

Have a good weekend.

Thank you.

Top comments (5)

Collapse
 
masharsamue profile image
Samuel Mashar

nomare so unamind kutuekea link ya netlify tutest hii stuff

Collapse
 
kevin_odongo35 profile image
Kevin Odongo

Go to part one of the article you will get the link

Collapse
 
masharsamue profile image
Samuel Mashar

Am speaking about the deployed link not the codebase, but ni sawa tu. Great stuff.

Thread Thread
 
kevin_odongo35 profile image
Kevin Odongo

main.d37i78548hx7uj.amplifyapp.com/

This is link was in the first article. Play around with it to understand how it works

Thread Thread
 
masharsamue profile image
Samuel Mashar

I understand it i just wanted to see it in action