Why is my WebRTC peer-to-peer application failing to work properly?

it’s been quite a long time since I’ve posted here. Just wanted to bounce this off of you as it has been making my brain hurt. So, I have been developing a real time video chat app with WebRTC. Now, I know that the obligatory “it’s somewhere in the network stack (NAT)” answer always applies.

As is always the case it seems with WebRTC, it works perfectly in my browser and on my laptop between tabs or between Safari/Chrome. However, over the internet on HTTPS on a site I’ve created, it is shotty at best. It can accept and display the media stream from my iPhone but it cannot receive the media stream from my laptop. It just shows a black square on the iPhone for the remote video.

Any pointers would be most appreciate though as I’ve been going crazy. I know that TURN servers are an inevitable aspect of WebRTC but I’m trying to avoid employing that.

So, here is my Session class which handles essentially all the WebRTC related client side session logic:

(The publish method is just an inherited member that emulates EventTarget/EventEmitter functionality and the p2p config is just for Google’s public STUN servers)

class Session extends Notifier {
    constructor(app) {
        super()

        this.app = app

        this.client = this.app.client

        this.clientSocket = this.client.socket

        this.p2p = new RTCPeerConnection(this.app.config.p2pConfig)

        this.closed = false

        this.initialize()
    }

    log(message) {
        if (this.closed) return

        console.log(`[${Date.now()}] {Session} ${message}`)
    }

    logEvent(event, message) {
        let output = event
        if (message) output += `: ${message}`
        this.log(output)
    }

    signal(family, data) {
        if (this.closed) return
        
        if (! data) return

        let msg = {}
        msg[family] = data
        
        this.clientSocket.emit("signal", msg)
    }

    initialize() {
        this.p2p.addEventListener("track", async event => {
            if (this.closed) return
            try {
                const [remoteStream] = event.streams
                this.app.mediaManager.remoteVideoElement.srcObject = remoteStream
            } catch (e) {
                this.logEvent("Failed adding track", `${e}`)
                this.close()
            }
        })

        this.p2p.addEventListener("icecandidate", event => {
            if (this.closed) return 
            if (! event.candidate) return
            this.signal("candidate", event.candidate)
            this.logEvent("Candidate", "Sent")
        })

        this.p2p.addEventListener("connectionstatechange", event => {
            if (this.closed) return
            switch (this.p2p.connectionState) {
                case "connected":
                    this.publish("opened")
                    this.logEvent("Opened")
                    break
            
                // A fail safe to ensure that faulty connections 
                // are terminated abruptly
                case "disconnected":
                case "closed":
                case "failed":
                    this.close()
                    break

                default:
                    break
            }
        })

        this.clientSocket.on("initiate", async () => {
            if (this.closed) return
            try {
                const offer = await this.p2p.createOffer()
                await this.p2p.setLocalDescription(offer)
                this.signal("offer", offer)
                this.logEvent("Offer", "Sent")
            } catch (e) {
                this.logEvent("Uninitiated", `${e}`)
                this.close()
            }

        })

        this.clientSocket.on("signal", async data => {
            if (this.closed) return
            try {
                if (data.offer) {
                    this.p2p.setRemoteDescription(new RTCSessionDescription(data.offer))
                    this.logEvent("Offer", "Received")
                    const answer = await this.p2p.createAnswer()
                    await this.p2p.setLocalDescription(answer)
                    this.signal("answer", answer)
                    this.logEvent("Answer", "Sent")
                }

                if (data.answer) {
                    const remoteDescription = new RTCSessionDescription(data.answer)
                    await this.p2p.setRemoteDescription(remoteDescription)
                    this.logEvent("Answer", "Received")
                }

                if (data.candidate) {
                    try {
                        await this.p2p.addIceCandidate(data.candidate)
                        this.logEvent("Candidate", "Added")
                    } catch (e) {
                        this.logEvent("Candidate", `Failed => ${e}`)
                    }
                }
            } catch (e) {
                this.logEvent("Signal Failed", `${e}`)
                this.close()
            }
        })

        this.app.mediaManager.localStream.getTracks().forEach(track => {
            this.p2p.addTrack(track, this.app.mediaManager.localStream)
        })
    }

    close() {
        if (this.closed) return

        this.p2p.close()
        this.app.client.unmatch()
        this.logEvent("Closed")
        
        this.closed = true
    }
}