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
}
}
1
1 Answer
I’ve worked with WebRTC well over a little while now and am deploying a production level website for many-to-many broadcasts so I can happily help you with this answer but don’t hit me as I’m about to spoil some of your fun.
The Session Description Protocol you generate would had contained the send/recv IPs of both connecting users. Now because none of you are actually port-forwarded to allow this connection or to act as a host, a TURN would be in fact required to mitigate this issue. For security reasons it’s like this and most users will require a TURN if you decide to go this route.
You can skip a TURN server completely but still requiring a server, you’d go the route of sending/receiving RTP and routing it like an MCU/SFU.
These solutions are designed to take in WebRTC produced tracks and output them to many viewers (consumers).
Here’s a SFU I use that works great for many-to-many if you can code it. It’s Node.JS friendly if you don’t know other languages outside JavaScript.
https://mediasoup.org/
2
In the mediasoupe demo why the local view is mirrored and remote view is not? https://i.stack.imgur.com/fmsdH.png
Oh that’s just part of their demo, you can change that with some minor CSS/etc. I believe they’re copying how discord does it this however doesn’t need to be how you do it, I at least don’t it is confusing for people including myself. 🙂
a) Log and check ICE candidates from laptop. See if the ports mentioned are open. Disable antivirus/firewall for testing. b) Do your iphone and laptop have different internet connections? You said it works on localhost. Have you tried over Lan/wifi between the iphone and laptop? if it works in Lan/wifi then check your router for NAT and port forwarding.