Skip to content

Commit 2a57c64

Browse files
committed
Add webrtc signaling example
1 parent 0bbe5ef commit 2a57c64

File tree

14 files changed

+1325
-0
lines changed

14 files changed

+1325
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
webrtc
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# A WebSocket signaling server/client for WebRTC.
2+
3+
This demo is devided in 4 parts:
4+
5+
- The `server` folder contains the signaling server implementation written in GDScript (so it can be run by a game server running Godot)
6+
- The `server_node` folder contains the signaling server implementation written in Node.js (if you don't plan to run a game server but only match-making).
7+
- The `client` part contains the client implementation in GDScript.
8+
- Itself divided into raw protocol and `WebRTCMultiplayer` handling.
9+
- The `demo` contains a small app that uses it.
10+
11+
**NOTE**: You must extract the [latest version](https://github.com/godotengine/webrtc-native/releases) of the WebRTC GDNative plugin in the project folder to run from desktop.
12+
13+
## Protocol
14+
15+
The protocol is text based, and composed by a command and possibly multiple payload arguments, each separated by a new line.
16+
17+
Messages without payload must still end with a newline and are the following:
18+
- `J: ` (or `J: <ROOM>`), must be sent by client immediately after connection to get a lobby assigned or join a known one.
19+
This messages is also sent by server back to the client to notify assigned lobby, or simply a successful join.
20+
- `I: <ID>`, sent by server to identify the client when it joins a room.
21+
- `N: <ID>`, sent by server to notify new peers in the same lobby.
22+
- `D: <ID>`, sent by server to notify when a peer in the same lobby disconnects.
23+
- `S: `, sent by client to seal the lobby (only the client that created it is allowed to seal a lobby).
24+
25+
When a lobby is sealed, no new client will be able to join, and the lobby will be destroyed (and clients disconnected) after 10 seconds.
26+
27+
Messages with payload (used to transfer WebRTC parameters) are:
28+
- `O: <ID>`, used to send an offer.
29+
- `A: <ID>`, used to send an answer.
30+
- `C: <ID>`, used to send a candidate.
31+
32+
When sending the parameter, a client will set `<ID>` as the destination peer, the server will replace it with the id of the sending peer, and rely it to the proper destination.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
extends "ws_webrtc_client.gd"
2+
3+
var rtc_mp : WebRTCMultiplayer = WebRTCMultiplayer.new()
4+
var sealed = false
5+
6+
func _init():
7+
connect("connected", self, "connected")
8+
connect("disconnected", self, "disconnected")
9+
10+
connect("offer_received", self, "offer_received")
11+
connect("answer_received", self, "answer_received")
12+
connect("candidate_received", self, "candidate_received")
13+
14+
connect("lobby_joined", self, "lobby_joined")
15+
connect("lobby_sealed", self, "lobby_sealed")
16+
connect("peer_connected", self, "peer_connected")
17+
connect("peer_disconnected", self, "peer_disconnected")
18+
19+
func start(url, lobby = ""):
20+
stop()
21+
sealed = false
22+
self.lobby = lobby
23+
connect_to_url(url)
24+
25+
func stop():
26+
rtc_mp.close()
27+
close()
28+
29+
func _create_peer(id : int):
30+
var peer : WebRTCPeerConnection = WebRTCPeerConnection.new()
31+
peer.initialize({
32+
"iceServers": [ { "urls": ["stun:stun.l.google.com:19302"] } ]
33+
})
34+
peer.connect("session_description_created", self, "_offer_created", [id])
35+
peer.connect("ice_candidate_created", self, "_new_ice_candidate", [id])
36+
rtc_mp.add_peer(peer, id)
37+
if id > rtc_mp.get_unique_id():
38+
peer.create_offer()
39+
return peer
40+
41+
func _new_ice_candidate(mid_name : String, index_name : int, sdp_name : String, id : int):
42+
send_candidate(id, mid_name, index_name, sdp_name)
43+
44+
func _offer_created(type : String, data : String, id : int):
45+
if not rtc_mp.has_peer(id):
46+
return
47+
print("created", type)
48+
rtc_mp.get_peer(id).connection.set_local_description(type, data)
49+
if type == "offer": send_offer(id, data)
50+
else: send_answer(id, data)
51+
52+
func connected(id : int):
53+
print("Connected %d" % id)
54+
rtc_mp.initialize(id, true)
55+
56+
func lobby_joined(lobby : String):
57+
self.lobby = lobby
58+
59+
func lobby_sealed():
60+
sealed = true
61+
62+
func disconnected():
63+
print("Disconnected: %d: %s" % [code, reason])
64+
if not sealed:
65+
stop() # Unexpected disconnect
66+
67+
func peer_connected(id : int):
68+
print("Peer connected %d" % id)
69+
_create_peer(id)
70+
71+
func peer_disconnected(id : int):
72+
if rtc_mp.has_peer(id): rtc_mp.remove_peer(id)
73+
74+
func offer_received(id : int, offer : String):
75+
print("Got offer: %d" % id)
76+
if rtc_mp.has_peer(id):
77+
rtc_mp.get_peer(id).connection.set_remote_description("offer", offer)
78+
79+
func answer_received(id : int, answer : String):
80+
print("Got answer: %d" % id)
81+
if rtc_mp.has_peer(id):
82+
rtc_mp.get_peer(id).connection.set_remote_description("answer", answer)
83+
84+
func candidate_received(id : int, mid : String, index : int, sdp : String):
85+
if rtc_mp.has_peer(id):
86+
rtc_mp.get_peer(id).connection.add_ice_candidate(mid, index, sdp)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
extends Node
2+
3+
export var autojoin = true
4+
export var lobby = "" # Will create a new lobby if empty
5+
6+
var client : WebSocketClient = WebSocketClient.new()
7+
var code = 1000
8+
var reason = "Unknown"
9+
10+
signal lobby_joined(lobby)
11+
signal connected(id)
12+
signal disconnected()
13+
signal peer_connected(id)
14+
signal peer_disconnected(id)
15+
signal offer_received(id, offer)
16+
signal answer_received(id, answer)
17+
signal candidate_received(id, mid, index, sdp)
18+
signal lobby_sealed()
19+
20+
func _init():
21+
client.connect("data_received", self, "_parse_msg")
22+
client.connect("connection_established", self, "_connected")
23+
client.connect("connection_closed", self, "_closed")
24+
client.connect("connection_error", self, "_closed")
25+
client.connect("server_close_request", self, "_close_request")
26+
27+
func connect_to_url(url : String):
28+
close()
29+
code = 1000
30+
reason = "Unknown"
31+
client.connect_to_url(url)
32+
33+
func close():
34+
client.disconnect_from_host()
35+
36+
func _closed(was_clean : bool = false):
37+
emit_signal("disconnected")
38+
39+
func _close_request(code : int, reason : String):
40+
self.code = code
41+
self.reason = reason
42+
43+
func _connected(protocol = ""):
44+
client.get_peer(1).set_write_mode(WebSocketPeer.WRITE_MODE_TEXT)
45+
if autojoin:
46+
join_lobby(lobby)
47+
48+
func _parse_msg():
49+
var pkt_str : String = client.get_peer(1).get_packet().get_string_from_utf8()
50+
51+
var req : PoolStringArray = pkt_str.split('\n', true, 1)
52+
if req.size() != 2: # Invalid request size
53+
return
54+
55+
var type : String = req[0]
56+
if type.length() < 3: # Invalid type size
57+
return
58+
59+
if type.begins_with("J: "):
60+
emit_signal("lobby_joined", type.substr(3, type.length() - 3))
61+
return
62+
elif type.begins_with("S: "):
63+
emit_signal("lobby_sealed")
64+
return
65+
66+
var src_str : String = type.substr(3, type.length() - 3)
67+
if not src_str.is_valid_integer(): # Source id is not an integer
68+
return
69+
70+
var src_id : int = int(src_str)
71+
72+
if type.begins_with("I: "):
73+
emit_signal("connected", src_id)
74+
elif type.begins_with("N: "):
75+
# Client connected
76+
emit_signal("peer_connected", src_id)
77+
elif type.begins_with("D: "):
78+
# Client connected
79+
emit_signal("peer_disconnected", src_id)
80+
elif type.begins_with("O: "):
81+
# Offer received
82+
emit_signal("offer_received", src_id, req[1])
83+
elif type.begins_with("A: "):
84+
# Answer received
85+
emit_signal("answer_received", src_id, req[1])
86+
elif type.begins_with("C: "):
87+
# Candidate received
88+
var candidate : PoolStringArray = req[1].split('\n', false)
89+
if candidate.size() != 3:
90+
return
91+
if not candidate[1].is_valid_integer():
92+
return
93+
emit_signal("candidate_received", src_id, candidate[0], int(candidate[1]), candidate[2])
94+
95+
func join_lobby(lobby : String):
96+
return client.get_peer(1).put_packet(("J: %s\n" % lobby).to_utf8())
97+
98+
func seal_lobby():
99+
return client.get_peer(1).put_packet("S: \n".to_utf8())
100+
101+
func send_candidate(id : int, mid : String, index : int, sdp : String) -> int:
102+
return _send_msg("C", id, "\n%s\n%d\n%s" % [mid, index, sdp])
103+
104+
func send_offer(id : int, offer : String) -> int:
105+
return _send_msg("O", id, offer)
106+
107+
func send_answer(id : int, answer : String) -> int:
108+
return _send_msg("A", id, answer)
109+
110+
func _send_msg(type : String, id : int, data : String) -> int:
111+
return client.get_peer(1).put_packet(("%s: %d\n%s" % [type, id, data]).to_utf8())
112+
113+
func _process(delta):
114+
var status : int = client.get_connection_status()
115+
if status == WebSocketClient.CONNECTION_CONNECTING or status == WebSocketClient.CONNECTION_CONNECTED:
116+
client.poll()
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
extends Control
2+
3+
onready var client = $Client
4+
5+
func _ready():
6+
client.connect("lobby_joined", self, "_lobby_joined")
7+
client.connect("lobby_sealed", self, "_lobby_sealed")
8+
client.connect("connected", self, "_connected")
9+
client.connect("disconnected", self, "_disconnected")
10+
client.rtc_mp.connect("peer_connected", self, "_mp_peer_connected")
11+
client.rtc_mp.connect("peer_disconnected", self, "_mp_peer_disconnected")
12+
client.rtc_mp.connect("server_disconnected", self, "_mp_server_disconnect")
13+
client.rtc_mp.connect("connection_succeeded", self, "_mp_connected")
14+
15+
func _process(delta):
16+
client.rtc_mp.poll()
17+
while client.rtc_mp.get_available_packet_count() > 0:
18+
_log(client.rtc_mp.get_packet().get_string_from_utf8())
19+
20+
func _connected(id):
21+
_log("Signaling server connected with ID: %d" % id)
22+
23+
func _disconnected():
24+
_log("Signaling server disconnected: %d - %s" % [client.code, client.reason])
25+
26+
func _lobby_joined(lobby):
27+
_log("Joined lobby %s" % lobby)
28+
29+
func _lobby_sealed():
30+
_log("Lobby has been sealed")
31+
32+
func _mp_connected():
33+
_log("Multiplayer is connected (I am %d)" % client.rtc_mp.get_unique_id())
34+
35+
func _mp_server_disconnect():
36+
_log("Multiplayer is disconnected (I am %d)" % client.rtc_mp.get_unique_id())
37+
38+
func _mp_peer_connected(id : int):
39+
_log("Multiplayer peer %d connected" % id)
40+
41+
func _mp_peer_disconnected(id : int):
42+
_log("Multiplayer peer %d disconnected" % id)
43+
44+
func _log(msg):
45+
print(msg)
46+
$vbox/TextEdit.text += str(msg) + "\n"
47+
48+
func ping():
49+
_log(client.rtc_mp.put_packet("ping".to_utf8()))
50+
51+
func _on_Peers_pressed():
52+
var d = client.rtc_mp.get_peers()
53+
_log(d)
54+
for k in d:
55+
_log(client.rtc_mp.get_peer(k))
56+
57+
func start():
58+
client.start($vbox/connect/host.text, $vbox/connect/RoomSecret.text)
59+
60+
func _on_Seal_pressed():
61+
client.seal_lobby()
62+
63+
func stop():
64+
client.stop()

0 commit comments

Comments
 (0)