Reputation: 83
So Unity seems to have wrapped WebRTC in a neat package. This looks like good news, since they deprecated UNET without placing a counterbalance first. Whatever.
I now just so happen to have to implement multiplayer for some games, and since my company doesn't want to invest without having a first impression of how it will be received by gamers, I have to make do without a server to handle connections. So I stumbled on WebRTC, of which DataChannels seem to be perfect for my use case, since I will have to transmit a few bytes representing the game state (which is in lockstep, so no problem there).
However, for the life of me I can't understand how this thing works.
It looks like it exchanges addresses and other data via a google STUN server, does some offer\answer shenanigans, and thus the data channel is established. However I can't understand how it knows that 2 devices are the ones that need to be connected, and I can't understand why my code doesn't work. I made a class that connects local and remote peers, so they should be able to exhange data, right?
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.WebRTC;
namespace REPLAYSRL.Forza4 {
public class P2PManager : MonoBehaviour {
//---------------------------------------------- VARIABILI PRIVATE
private RTCPeerConnection localPeer, remotePeer; //Le istanze delle due rispettive sessioni peer 2 peer
private RTCDataChannel localDataChannel, remoteDataChannel; //I data channel per il peer locale e quello remoto per una comunicazione fullduplex
private RTCOfferOptions OfferOptions = new RTCOfferOptions {
iceRestart = false,
offerToReceiveAudio = true,
offerToReceiveVideo = false
};
private RTCAnswerOptions AnswerOptions = new RTCAnswerOptions {
iceRestart = false,
};
//---------------------------------------------- VARIABILI PUBBLICHE
/// <summary>
/// Se è stata stabilita una connessione P2P.
/// </summary>
public bool Is_Connected { get; private set; } = false;
/// <summary>
/// Istanza singleton della classe
/// </summary>
public static P2PManager Instance;
//---------------------------------------------- FUNZIONI PRIVATE
private void OnIceConnectionChange(RTCPeerConnection peer, RTCIceConnectionState status) {
Debug.Log(((peer == localPeer) ? "localPeer" : "remotePeer") + " Status: " + status);
}
private void OnIceCandidate(RTCPeerConnection peer, RTCIceCandidate candidate) {
((peer == localPeer) ? remotePeer : localPeer).AddIceCandidate(ref candidate);
Debug.Log("Aggiunto candidato a " + ((peer == localPeer) ? "LocalPeer" : "RemotePeer"));
}
private RTCPeerConnection GetPairedPeer(RTCPeerConnection peer) {
return (peer == localPeer) ? remotePeer : localPeer;
}
private string GetName(RTCPeerConnection peer) {
return (peer == localPeer) ? "localPeer" : "remotePeer";
}
private IEnumerator P2PConnection(DataReceivedBehaviour drb) {
RTCConfiguration peerCfg = default;
peerCfg.iceServers = new RTCIceServer[]
{
new RTCIceServer { urls = new string[] { "stun:stun.l.google.com:19302" } }
};
localPeer = new RTCPeerConnection(ref peerCfg);
localPeer.OnIceCandidate = (candidate => { OnIceCandidate(localPeer, candidate); });
localPeer.OnIceConnectionChange = (state => { OnIceConnectionChange(localPeer, state); });
/*
localPeer.OnDataChannel = (channel => {
//OnChannelCreate
Debug.Log("Canale locale creato.");
localDataChannel = channel;
localDataChannel.OnMessage = (bytes => {
drb(bytes);
});
});
*/
remotePeer = new RTCPeerConnection(ref peerCfg);
remotePeer.OnIceCandidate = (candidate => { OnIceCandidate(localPeer, candidate); });
remotePeer.OnIceConnectionChange = (state => { OnIceConnectionChange(remotePeer, state); });
remotePeer.OnDataChannel = (channel => {
//OnChannelCreate
Debug.Log("Canale remoto creato");
remoteDataChannel = channel;
remoteDataChannel.OnMessage = (bytes => {
drb(bytes);
});
});
RTCDataChannelInit dataChannelCfg = new RTCDataChannelInit(true);
dataChannelCfg.id = 0;
dataChannelCfg.reliable = true;
localDataChannel = localPeer.CreateDataChannel("data", ref dataChannelCfg);
localDataChannel.OnOpen = (() => {
//Inizializzazioni
Is_Connected = true;
});
localDataChannel.OnClose = (() => {
//Distruzioni
Is_Connected = false;
});
//Connessione
var localOfferResult = localPeer.CreateOffer(ref OfferOptions);
yield return localOfferResult;
if (!localOfferResult.IsError) {
var desc = localOfferResult.Desc;
var localDescriptionResult = localPeer.SetLocalDescription(ref desc);
yield return localDescriptionResult;
var op2 = remotePeer.SetRemoteDescription(ref desc);
yield return op2;
var op3 = remotePeer.CreateAnswer(ref AnswerOptions);
yield return op3;
if (!op3.IsError) {
desc = op3.Desc;
var op4 = remotePeer.SetLocalDescription(ref desc);
yield return op4;
var op5 = localPeer.SetRemoteDescription(ref desc);
yield return op5;
}
}
}
//---------------------------------------------- FUNZIONI PUBBLICHE
public delegate void DataReceivedBehaviour(byte[] bytes);
public void Initialize(DataReceivedBehaviour dataReceivedBehaviour) {
WebRTC.Initialize();
StartCoroutine(P2PConnection(dataReceivedBehaviour));
}
public void SendToLocal(byte[] Value) {
localDataChannel.Send(Value);
}
public void SendToLocal(string Value) {
localDataChannel.Send(Value);
}
public void SendToRemote(byte[] Value) {
remoteDataChannel.Send(Value);
}
public void SendToRemote(string Value) {
remoteDataChannel.Send(Value);
}
public void Dispose() {
if (localPeer != null)
localPeer.Close();
if (remotePeer != null)
remotePeer.Close();
WebRTC.Dispose();
}
//---------------------------------------------- FUNZIONI DI UNITY
private void Awake() {
if (Instance == null) {
Instance = this;
} else if (Instance != this) {
Destroy(this);
}
}
}
}
Apologies for the comments in italian.
I don't know, maybe I'm getting something wrong. If you happen to have any advice on other, better alternatives, I'm all ears.
Upvotes: 2
Views: 1997
Reputation: 30699
Your logic looks largely correct to me. I don't know if it will fix your issue but to make things clearer I would adjust your SDP exchange so the description objects aren't overwritten.
//Connessione
var localOfferResult = localPeer.CreateOffer(ref OfferOptions);
yield return localOfferResult;
if (!localOfferResult.IsError) {
var offer = localOfferResult.Desc;
var localDescriptionResult = localPeer.SetLocalDescription(ref offer);
yield return localDescriptionResult;
var op2 = remotePeer.SetRemoteDescription(ref desc);
yield return op2;
var op3 = remotePeer.CreateAnswer(ref AnswerOptions);
yield return op3;
if (!op3.IsError) {
var answer = op3.Desc;
var op4 = remotePeer.SetLocalDescription(ref answer);
yield return op4;
var op5 = localPeer.SetRemoteDescription(ref answer);
yield return op5;
}
}
Other than that are you getting any log messages? Do the ICE connection states for either peer change?
Also be aware that when you want to take the next step and have the local and remote peers on different machines you will most likely need some kind of signalling server involved to allow the SDP offer/answer and ICE candidates to be exchanged.
Upvotes: 2