WebRTC Data Channel Connection Fails as soon as i add media tracks to the connection

  javascript, node.js, reactjs, webrtc

I have been trying to create a video group call application. It follows a mesh topology and the steps I follow are these:

  1. When a client connects. A room is created if it is the first client or it broadcasts its socket id to all other clients in that room
  2. When a client receives the socket id it sends it an offer and the cycle starts. A similar flow is followed for the other clients.
    The problem is when I simply create data channels, it works fine but as soon as I start adding media Tracks, things break and even the data channel doesn’t get connected. I’ll attach the code below
import { io } from 'socket.io-client'
class RTConnection {
    constructor() {
        this.config = {
            iceServers: [
                {
                    urls: ["stun:stun.l.google.com:19302"]
                }
            ]
        }
        this.connections = {}
        this.videoElements = {}
        this.ICEqueue = {}
    }

    init = async (roomId, videoElemRef) => {
        this.roomId = roomId
        this.videoElemRef = videoElemRef.current
        let video = document.createElement('VIDEO')
        this.myVideo = video
        video.onloadedmetadata = e => video.play()
        this.videoElemRef.appendChild(video)
        const stream = await navigator.mediaDevices.getUserMedia({video: true, audio: false})
        video.srcObject = stream
        this.socket = io()
        
        this.socket.emit("join-room", roomId)
        
        this.socket.on('new-member', id => {
            this.createOffer(id)
        })

        this.socket.on("offer", (id, offer) => {
            this.acceptOffer(id, offer)
        })

        this.socket.on("new-ice-candidate", (id, candidate) => {
            this.handleIceCandidate(id, candidate)
        })

        this.socket.on("answer", (id, answer) => {
            this.acceptAnswer(id, answer)
        })
    }

    acceptAnswer = async (id, answer) => {
        const peerConnection = this.connections[id]
        await peerConnection.setRemoteDescription(answer)
        console.log("Answer Accepted")
    }

    handleIceCandidate = async (id, candidate) => {
        if(!candidate) {
            return
        }

        const peerConnection = this.connections[id]
        if(peerConnection.remoteDescription) {
            console.log("Setting Ice")
            console.log(candidate)
            await peerConnection.addIceCandidate(new RTCIceCandidate(candidate))
        } else {
            if(id in this.ICEqueue) {
                console.log("Storing Ice Candidates")
                console.log(id)
                this.ICEqueue[id].push(candidate)
            } else {
                this.ICEqueue[id] = []
            }
        }
    }

    hydrateIceCandidates = async (id) => {
        const peerConnection = this.connections[id]
        console.log("Hydrating Ice")
        for(let i in this.ICEqueue[id]) {
            const candidate = this.ICEqueue[id][i]
            await peerConnection.addIceCandidate(candidate)
        }
    }

    acceptOffer = async (id, offer) => {
        const peerConnection = new RTCPeerConnection()
        peerConnection.ondatachannel = e => {
            console.log("Data Channel Received")
            peerConnection.dc = e.channel
            peerConnection.dc.onopen = e => console.log("Connected!!!!")
        }
        this.connections[id] = peerConnection
        peerConnection.onicecandidate = e => {
            this.socket.emit("new-ice-candidate", e.candidate)
        }
        peerConnection.ontrack = (e) => {
            console.log("Received a track");
            console.log(e.streams)
            const video = document.createElement('VIDEO')
            this.videoElemRef.appendChild(video)
            video.onloadeddata = ev => {console.log("Meta Data Loaded") ; video.play()}
            video.srcObject = e.streams[0]
        }
        const stream = this.myVideo.srcObject
        console.log("Private Stream")
        console.log(stream)
        //await stream.getTracks().forEach(async track => peerConnection.addTrack(track, stream))
        await peerConnection.setRemoteDescription(offer)
        console.log("Remote Description Set")
        const ans = await peerConnection.createAnswer()
        console.log("Answer Created")
        await peerConnection.setLocalDescription(ans)
        console.log("Local Description Set")
        await this.hydrateIceCandidates(id)
        this.socket.emit("answer", id, ans)
    }

    createOffer = async (id) => {
        const peerConnection = new RTCPeerConnection()
        const stream = this.myVideo.srcObject
        //stream.getTracks().forEach(track => peerConnection.addTrack(track, stream))
        peerConnection.dc = peerConnection.createDataChannel("channel")
        peerConnection.dc.onopen = e => {

            console.log("Connected!!!")
        }
        this.connections[id] = peerConnection
        peerConnection.ontrack = (e) => {
            console.log("Received a track");
            console.log(e.streams)
            const video = document.createElement('VIDEO')
            console.log("Video Element")
            console.log(video)
            this.videoElemRef.appendChild(video)
            video.onloadedmetadata = ev => video.play()
            video.srcObject = e.streams[0]
        }
        peerConnection.onicecandidate = (e) => {
            this.socket.emit("new-ice-candidate", e.candidate)
        }
        const offer = await peerConnection.createOffer()
        await peerConnection.setLocalDescription(offer)
        console.log(peerConnection.localDescription)
        this.socket.emit("offer", id, offer)
    }

    stopStream = (stream, elem) => {
        stream.getTracks().forEach(track => {
            track.stop()
        })
        this.videoElemRef.current.removeChild(elem)
    }

    cleanMedia = () => {
        this.stopStream(this.videoEl.srcObject, this.videoEl)
        for(let id in this.videoElements) {
            this.stopStream(this.videoElements[id].srcObject, this.videoElements[id])
            delete this.videoElements[id]
        }
        delete this.videoElements
    }

    leaveCall = () => {
        for(let id in this.connections) {
            this.connections[id].close()
            delete this.connections[id]
        }
        delete this.connections
        this.cleanMedia()
        this.socket.disconnect()
    }
}

export default RTConnection

Server-Side Code

const express = require('express')
const fs = require('fs')
const app = express()
const path = require('path')
const v4 = require('uuid').v4

const options = {
    key: fs.readFileSync("key.pem"),
    cert: fs.readFileSync("cert.pem")
}

const server = require('http').createServer(app)
const io = require('socket.io')(server)
const port = process.env.port || '5000'

app.set('view engine', 'ejs')

app.use("/",express.static(path.resolve(__dirname + '/public')))

app.get('/',(req, res) => {
    res.render('home')   
})

app.get('/room', (req,res) => {
    const roomId = v4()
    res.redirect(`/room/${roomId}`)
})

app.get('/room/:roomId',(req, res) => {
    res.render('room', {roomId: req.params.roomId})
})

server.listen(port, () => {
    console.log(`Starting server on ${port}`)
})

io.on("connection", socket => {
    socket.on('join-room', (roomId) => {
        socket.join(roomId)
        socket.to(roomId).emit("new-member", socket.id)
        
        socket.on("new-ice-candidate", (candidate) => {
            socket.to(roomId).emit("new-ice-candidate", socket.id, candidate)
        })

        socket.on("offer", (id, offer) => {
            socket.to(id).emit("offer", socket.id, offer)          
        })

        socket.on("answer", (id, answer) => {
            socket.to(id).emit("answer", socket.id, answer)
        })

        socket.on("disconnecting", () => {
            for(let room of socket.rooms) {
                socket.to(room).emit("leaveCall", socket.id)
            }
        })
    })  
})

Source: Ask Javascript Questions

LEAVE A COMMENT