From dbff58ebdd5e5e406da594a776e8724c574a310b Mon Sep 17 00:00:00 2001 From: Fabio Alessandrelli Date: Thu, 8 Sep 2022 04:39:32 +0200 Subject: [PATCH] [WebRTC] Expose more of the WebRTC API. Add get_gathering_state() returning the iceGatheringState of the connection. Add get_signaling_state() returning the signalingState of the connection. Improve JS library. --- .../doc_classes/WebRTCPeerConnection.xml | 39 ++++ .../WebRTCPeerConnectionExtension.xml | 10 + modules/webrtc/library_godot_webrtc.js | 200 +++++++++++------- modules/webrtc/webrtc_peer_connection.cpp | 13 ++ modules/webrtc/webrtc_peer_connection.h | 19 ++ .../webrtc_peer_connection_extension.cpp | 2 + .../webrtc/webrtc_peer_connection_extension.h | 2 + modules/webrtc/webrtc_peer_connection_js.cpp | 23 +- modules/webrtc/webrtc_peer_connection_js.h | 14 +- 9 files changed, 237 insertions(+), 85 deletions(-) diff --git a/modules/webrtc/doc_classes/WebRTCPeerConnection.xml b/modules/webrtc/doc_classes/WebRTCPeerConnection.xml index e99aeb4f510..4ecc71ddbb1 100644 --- a/modules/webrtc/doc_classes/WebRTCPeerConnection.xml +++ b/modules/webrtc/doc_classes/WebRTCPeerConnection.xml @@ -67,6 +67,18 @@ Returns the connection state. See [enum ConnectionState]. + + + + Returns the ICE [enum GatheringState] of the connection. This lets you detect, for example, when collection of ICE candidates has finished. + + + + + + Returns the [enum SignalingState] on the local end of the connection while connecting or reconnecting to another peer. + + @@ -165,5 +177,32 @@ The peer connection is closed (after calling [method close] for example). + + The peer connection was just created and hasn't done any networking yet. + + + The ICE agent is in the process of gathering candidates for the connection. + + + The ICE agent has finished gathering candidates. If something happens that requires collecting new candidates, such as a new interface being added or the addition of a new ICE server, the state will revert to gathering to gather those candidates. + + + There is no ongoing exchange of offer and answer underway. This may mean that the [WebRTCPeerConnection] is new ([constant STATE_NEW]) or that negotiation is complete and a connection has been established ([constant STATE_CONNECTED]). + + + The local peer has called [method set_local_description], passing in SDP representing an offer (usually created by calling [method create_offer]), and the offer has been applied successfully. + + + The remote peer has created an offer and used the signaling server to deliver it to the local peer, which has set the offer as the remote description by calling [method set_remote_description]. + + + The offer sent by the remote peer has been applied and an answer has been created and applied by calling [method set_local_description]. This provisional answer describes the supported media formats and so forth, but may not have a complete set of ICE candidates included. Further candidates will be delivered separately later. + + + A provisional answer has been received and successfully applied in response to an offer previously sent and established by calling [method set_local_description]. + + + The [WebRTCPeerConnection] has been closed. + diff --git a/modules/webrtc/doc_classes/WebRTCPeerConnectionExtension.xml b/modules/webrtc/doc_classes/WebRTCPeerConnectionExtension.xml index 3c4bf18a767..474d2f6a896 100644 --- a/modules/webrtc/doc_classes/WebRTCPeerConnectionExtension.xml +++ b/modules/webrtc/doc_classes/WebRTCPeerConnectionExtension.xml @@ -37,6 +37,16 @@ + + + + + + + + + + diff --git a/modules/webrtc/library_godot_webrtc.js b/modules/webrtc/library_godot_webrtc.js index e57e4299e0e..e6604eecd7e 100644 --- a/modules/webrtc/library_godot_webrtc.js +++ b/modules/webrtc/library_godot_webrtc.js @@ -220,64 +220,123 @@ mergeInto(LibraryManager.library, GodotRTCDataChannel); const GodotRTCPeerConnection = { $GodotRTCPeerConnection__deps: ['$IDHandler', '$GodotRuntime', '$GodotRTCDataChannel'], $GodotRTCPeerConnection: { - onstatechange: function (p_id, p_conn, callback, event) { - const ref = IDHandler.get(p_id); - if (!ref) { - return; - } - let state; - switch (p_conn.iceConnectionState) { - case 'new': - state = 0; - break; - case 'checking': - state = 1; - break; - case 'connected': - case 'completed': - state = 2; - break; - case 'disconnected': - state = 3; - break; - case 'failed': - state = 4; - break; - case 'closed': - default: - state = 5; - break; - } - callback(state); + // Enums + ConnectionState: { + 'new': 0, + 'connecting': 1, + 'connected': 2, + 'disconnected': 3, + 'failed': 4, + 'closed': 5, }, - onicecandidate: function (p_id, callback, event) { - const ref = IDHandler.get(p_id); - if (!ref || !event.candidate) { - return; - } - - const c = event.candidate; - const candidate_str = GodotRuntime.allocString(c.candidate); - const mid_str = GodotRuntime.allocString(c.sdpMid); - callback(mid_str, c.sdpMLineIndex, candidate_str); - GodotRuntime.free(candidate_str); - GodotRuntime.free(mid_str); + ConnectionStateCompat: { + // Using values from IceConnectionState for browsers that do not support ConnectionState (notably Firefox). + 'new': 0, + 'checking': 1, + 'connected': 2, + 'completed': 2, + 'disconnected': 3, + 'failed': 4, + 'closed': 5, }, - ondatachannel: function (p_id, callback, event) { - const ref = IDHandler.get(p_id); - if (!ref) { - return; + IceGatheringState: { + 'new': 0, + 'gathering': 1, + 'complete': 2, + }, + + SignalingState: { + 'stable': 0, + 'have-local-offer': 1, + 'have-remote-offer': 2, + 'have-local-pranswer': 3, + 'have-remote-pranswer': 4, + 'closed': 5, + }, + + // Callbacks + create: function (config, onConnectionChange, onSignalingChange, onIceGatheringChange, onIceCandidate, onDataChannel) { + let conn = null; + try { + conn = new RTCPeerConnection(config); + } catch (e) { + GodotRuntime.error(e); + return 0; } - const cid = IDHandler.add(event.channel); - callback(cid); + const id = IDHandler.add(conn); + + if ('connectionState' in conn && conn['connectionState'] !== undefined) { + // Use "connectionState" if supported + conn.onconnectionstatechange = function (event) { + if (!IDHandler.get(id)) { + return; + } + onConnectionChange(GodotRTCPeerConnection.ConnectionState[conn.connectionState] || 0); + }; + } else { + // Fall back to using "iceConnectionState" when "connectionState" is not supported (notably Firefox). + conn.oniceconnectionstatechange = function (event) { + if (!IDHandler.get(id)) { + return; + } + onConnectionChange(GodotRTCPeerConnection.ConnectionStateCompat[conn.iceConnectionState] || 0); + }; + } + conn.onicegatheringstatechange = function (event) { + if (!IDHandler.get(id)) { + return; + } + onIceGatheringChange(GodotRTCPeerConnection.IceGatheringState[conn.iceGatheringState] || 0); + }; + conn.onsignalingstatechange = function (event) { + if (!IDHandler.get(id)) { + return; + } + onSignalingChange(GodotRTCPeerConnection.SignalingState[conn.signalingState] || 0); + }; + conn.onicecandidate = function (event) { + if (!IDHandler.get(id)) { + return; + } + const c = event.candidate; + if (!c || !c.candidate) { + return; + } + const candidate_str = GodotRuntime.allocString(c.candidate); + const mid_str = GodotRuntime.allocString(c.sdpMid); + onIceCandidate(mid_str, c.sdpMLineIndex, candidate_str); + GodotRuntime.free(candidate_str); + GodotRuntime.free(mid_str); + }; + conn.ondatachannel = function (event) { + if (!IDHandler.get(id)) { + return; + } + const cid = IDHandler.add(event.channel); + onDataChannel(cid); + }; + return id; + }, + + destroy: function (p_id) { + const conn = IDHandler.get(p_id); + if (!conn) { + return; + } + conn.onconnectionstatechange = null; + conn.oniceconnectionstatechange = null; + conn.onicegatheringstatechange = null; + conn.onsignalingstatechange = null; + conn.onicecandidate = null; + conn.ondatachannel = null; + IDHandler.remove(p_id); }, onsession: function (p_id, callback, session) { - const ref = IDHandler.get(p_id); - if (!ref) { + if (!IDHandler.get(p_id)) { return; } const type_str = GodotRuntime.allocString(session.type); @@ -297,27 +356,19 @@ const GodotRTCPeerConnection = { }, }, - godot_js_rtc_pc_create__sig: 'iiiiii', - godot_js_rtc_pc_create: function (p_config, p_ref, p_on_state_change, p_on_candidate, p_on_datachannel) { - const onstatechange = GodotRuntime.get_func(p_on_state_change).bind(null, p_ref); - const oncandidate = GodotRuntime.get_func(p_on_candidate).bind(null, p_ref); - const ondatachannel = GodotRuntime.get_func(p_on_datachannel).bind(null, p_ref); - - const config = JSON.parse(GodotRuntime.parseString(p_config)); - let conn = null; - try { - conn = new RTCPeerConnection(config); - } catch (e) { - GodotRuntime.error(e); - return 0; - } - - const base = GodotRTCPeerConnection; - const id = IDHandler.add(conn); - conn.oniceconnectionstatechange = base.onstatechange.bind(null, id, conn, onstatechange); - conn.onicecandidate = base.onicecandidate.bind(null, id, oncandidate); - conn.ondatachannel = base.ondatachannel.bind(null, id, ondatachannel); - return id; + godot_js_rtc_pc_create__sig: 'iiiiiiii', + godot_js_rtc_pc_create: function (p_config, p_ref, p_on_connection_state_change, p_on_ice_gathering_state_change, p_on_signaling_state_change, p_on_ice_candidate, p_on_datachannel) { + const wrap = function (p_func) { + return GodotRuntime.get_func(p_func).bind(null, p_ref); + }; + return GodotRTCPeerConnection.create( + JSON.parse(GodotRuntime.parseString(p_config)), + wrap(p_on_connection_state_change), + wrap(p_on_signaling_state_change), + wrap(p_on_ice_gathering_state_change), + wrap(p_on_ice_candidate), + wrap(p_on_datachannel) + ); }, godot_js_rtc_pc_close__sig: 'vi', @@ -331,14 +382,7 @@ const GodotRTCPeerConnection = { godot_js_rtc_pc_destroy__sig: 'vi', godot_js_rtc_pc_destroy: function (p_id) { - const ref = IDHandler.get(p_id); - if (!ref) { - return; - } - ref.oniceconnectionstatechange = null; - ref.onicecandidate = null; - ref.ondatachannel = null; - IDHandler.remove(p_id); + GodotRTCPeerConnection.destroy(p_id); }, godot_js_rtc_pc_offer_create__sig: 'viiii', diff --git a/modules/webrtc/webrtc_peer_connection.cpp b/modules/webrtc/webrtc_peer_connection.cpp index d885b9262be..5aa891d35c3 100644 --- a/modules/webrtc/webrtc_peer_connection.cpp +++ b/modules/webrtc/webrtc_peer_connection.cpp @@ -69,6 +69,8 @@ void WebRTCPeerConnection::_bind_methods() { ClassDB::bind_method(D_METHOD("close"), &WebRTCPeerConnection::close); ClassDB::bind_method(D_METHOD("get_connection_state"), &WebRTCPeerConnection::get_connection_state); + ClassDB::bind_method(D_METHOD("get_gathering_state"), &WebRTCPeerConnection::get_gathering_state); + ClassDB::bind_method(D_METHOD("get_signaling_state"), &WebRTCPeerConnection::get_signaling_state); ADD_SIGNAL(MethodInfo("session_description_created", PropertyInfo(Variant::STRING, "type"), PropertyInfo(Variant::STRING, "sdp"))); ADD_SIGNAL(MethodInfo("ice_candidate_created", PropertyInfo(Variant::STRING, "media"), PropertyInfo(Variant::INT, "index"), PropertyInfo(Variant::STRING, "name"))); @@ -80,6 +82,17 @@ void WebRTCPeerConnection::_bind_methods() { BIND_ENUM_CONSTANT(STATE_DISCONNECTED); BIND_ENUM_CONSTANT(STATE_FAILED); BIND_ENUM_CONSTANT(STATE_CLOSED); + + BIND_ENUM_CONSTANT(GATHERING_STATE_NEW); + BIND_ENUM_CONSTANT(GATHERING_STATE_GATHERING); + BIND_ENUM_CONSTANT(GATHERING_STATE_COMPLETE); + + BIND_ENUM_CONSTANT(SIGNALING_STATE_STABLE); + BIND_ENUM_CONSTANT(SIGNALING_STATE_HAVE_LOCAL_OFFER); + BIND_ENUM_CONSTANT(SIGNALING_STATE_HAVE_REMOTE_OFFER); + BIND_ENUM_CONSTANT(SIGNALING_STATE_HAVE_LOCAL_PRANSWER); + BIND_ENUM_CONSTANT(SIGNALING_STATE_HAVE_REMOTE_PRANSWER); + BIND_ENUM_CONSTANT(SIGNALING_STATE_CLOSED); } WebRTCPeerConnection::WebRTCPeerConnection() { diff --git a/modules/webrtc/webrtc_peer_connection.h b/modules/webrtc/webrtc_peer_connection.h index 122ea3d00f8..76f29f9d685 100644 --- a/modules/webrtc/webrtc_peer_connection.h +++ b/modules/webrtc/webrtc_peer_connection.h @@ -47,6 +47,21 @@ public: STATE_CLOSED }; + enum GatheringState { + GATHERING_STATE_NEW, + GATHERING_STATE_GATHERING, + GATHERING_STATE_COMPLETE, + }; + + enum SignalingState { + SIGNALING_STATE_STABLE, + SIGNALING_STATE_HAVE_LOCAL_OFFER, + SIGNALING_STATE_HAVE_REMOTE_OFFER, + SIGNALING_STATE_HAVE_LOCAL_PRANSWER, + SIGNALING_STATE_HAVE_REMOTE_PRANSWER, + SIGNALING_STATE_CLOSED, + }; + private: static StringName default_extension; @@ -57,6 +72,8 @@ public: static void set_default_extension(const StringName &p_name); virtual ConnectionState get_connection_state() const = 0; + virtual GatheringState get_gathering_state() const = 0; + virtual SignalingState get_signaling_state() const = 0; virtual Error initialize(Dictionary p_config = Dictionary()) = 0; virtual Ref create_data_channel(String p_label, Dictionary p_options = Dictionary()) = 0; @@ -74,5 +91,7 @@ public: }; VARIANT_ENUM_CAST(WebRTCPeerConnection::ConnectionState); +VARIANT_ENUM_CAST(WebRTCPeerConnection::GatheringState); +VARIANT_ENUM_CAST(WebRTCPeerConnection::SignalingState); #endif // WEBRTC_PEER_CONNECTION_H diff --git a/modules/webrtc/webrtc_peer_connection_extension.cpp b/modules/webrtc/webrtc_peer_connection_extension.cpp index 54143e4b790..592a1f8a97b 100644 --- a/modules/webrtc/webrtc_peer_connection_extension.cpp +++ b/modules/webrtc/webrtc_peer_connection_extension.cpp @@ -32,6 +32,8 @@ void WebRTCPeerConnectionExtension::_bind_methods() { GDVIRTUAL_BIND(_get_connection_state); + GDVIRTUAL_BIND(_get_gathering_state); + GDVIRTUAL_BIND(_get_signaling_state); GDVIRTUAL_BIND(_initialize, "p_config"); GDVIRTUAL_BIND(_create_data_channel, "p_label", "p_config"); GDVIRTUAL_BIND(_create_offer); diff --git a/modules/webrtc/webrtc_peer_connection_extension.h b/modules/webrtc/webrtc_peer_connection_extension.h index 0c324ca45f3..085069debb0 100644 --- a/modules/webrtc/webrtc_peer_connection_extension.h +++ b/modules/webrtc/webrtc_peer_connection_extension.h @@ -53,6 +53,8 @@ public: /** GDExtension **/ EXBIND0RC(ConnectionState, get_connection_state); + EXBIND0RC(GatheringState, get_gathering_state); + EXBIND0RC(SignalingState, get_signaling_state); EXBIND1R(Error, initialize, Dictionary); EXBIND0R(Error, create_offer); EXBIND2R(Error, set_remote_description, String, String); diff --git a/modules/webrtc/webrtc_peer_connection_js.cpp b/modules/webrtc/webrtc_peer_connection_js.cpp index f48705253b7..a371312ae98 100644 --- a/modules/webrtc/webrtc_peer_connection_js.cpp +++ b/modules/webrtc/webrtc_peer_connection_js.cpp @@ -51,6 +51,16 @@ void WebRTCPeerConnectionJS::_on_connection_state_changed(void *p_obj, int p_sta peer->_conn_state = (ConnectionState)p_state; } +void WebRTCPeerConnectionJS::_on_gathering_state_changed(void *p_obj, int p_state) { + WebRTCPeerConnectionJS *peer = static_cast(p_obj); + peer->_gathering_state = (GatheringState)p_state; +} + +void WebRTCPeerConnectionJS::_on_signaling_state_changed(void *p_obj, int p_state) { + WebRTCPeerConnectionJS *peer = static_cast(p_obj); + peer->_signaling_state = (SignalingState)p_state; +} + void WebRTCPeerConnectionJS::_on_error(void *p_obj) { ERR_PRINT("RTCPeerConnection error!"); } @@ -100,7 +110,7 @@ Error WebRTCPeerConnectionJS::initialize(Dictionary p_config) { _conn_state = STATE_NEW; String config = Variant(p_config).to_json_string(); - _js_id = godot_js_rtc_pc_create(config.utf8().get_data(), this, &_on_connection_state_changed, &_on_ice_candidate, &_on_data_channel); + _js_id = godot_js_rtc_pc_create(config.utf8().get_data(), this, &_on_connection_state_changed, &_on_gathering_state_changed, &_on_signaling_state_changed, &_on_ice_candidate, &_on_data_channel); return _js_id ? OK : FAILED; } @@ -117,14 +127,19 @@ Error WebRTCPeerConnectionJS::poll() { return OK; } +WebRTCPeerConnection::GatheringState WebRTCPeerConnectionJS::get_gathering_state() const { + return _gathering_state; +} + +WebRTCPeerConnection::SignalingState WebRTCPeerConnectionJS::get_signaling_state() const { + return _signaling_state; +} + WebRTCPeerConnection::ConnectionState WebRTCPeerConnectionJS::get_connection_state() const { return _conn_state; } WebRTCPeerConnectionJS::WebRTCPeerConnectionJS() { - _conn_state = STATE_NEW; - _js_id = 0; - Dictionary config; initialize(config); } diff --git a/modules/webrtc/webrtc_peer_connection_js.h b/modules/webrtc/webrtc_peer_connection_js.h index 50266129e4b..e62ad6af28a 100644 --- a/modules/webrtc/webrtc_peer_connection_js.h +++ b/modules/webrtc/webrtc_peer_connection_js.h @@ -37,11 +37,13 @@ extern "C" { typedef void (*RTCOnIceConnectionStateChange)(void *p_obj, int p_state); +typedef void (*RTCOnIceGatheringStateChange)(void *p_obj, int p_state); +typedef void (*RTCOnSignalingStateChange)(void *p_obj, int p_state); typedef void (*RTCOnIceCandidate)(void *p_obj, const char *p_mid, int p_mline_idx, const char *p_candidate); typedef void (*RTCOnDataChannel)(void *p_obj, int p_id); typedef void (*RTCOnSession)(void *p_obj, const char *p_type, const char *p_sdp); typedef void (*RTCOnError)(void *p_obj); -extern int godot_js_rtc_pc_create(const char *p_config, void *p_obj, RTCOnIceConnectionStateChange p_on_state_change, RTCOnIceCandidate p_on_candidate, RTCOnDataChannel p_on_datachannel); +extern int godot_js_rtc_pc_create(const char *p_config, void *p_obj, RTCOnIceConnectionStateChange p_on_connection_state_change, RTCOnIceGatheringStateChange p_on_gathering_state_change, RTCOnSignalingStateChange p_on_signaling_state_change, RTCOnIceCandidate p_on_candidate, RTCOnDataChannel p_on_datachannel); extern void godot_js_rtc_pc_close(int p_id); extern void godot_js_rtc_pc_destroy(int p_id); extern void godot_js_rtc_pc_offer_create(int p_id, void *p_obj, RTCOnSession p_on_session, RTCOnError p_on_error); @@ -55,10 +57,14 @@ class WebRTCPeerConnectionJS : public WebRTCPeerConnection { GDCLASS(WebRTCPeerConnectionJS, WebRTCPeerConnection); private: - int _js_id; - ConnectionState _conn_state; + int _js_id = 0; + ConnectionState _conn_state = STATE_NEW; + GatheringState _gathering_state = GATHERING_STATE_NEW; + SignalingState _signaling_state = SIGNALING_STATE_STABLE; static void _on_connection_state_changed(void *p_obj, int p_state); + static void _on_gathering_state_changed(void *p_obj, int p_state); + static void _on_signaling_state_changed(void *p_obj, int p_state); static void _on_ice_candidate(void *p_obj, const char *p_mid_name, int p_mline_idx, const char *p_candidate); static void _on_data_channel(void *p_obj, int p_channel); static void _on_session_created(void *p_obj, const char *p_type, const char *p_session); @@ -66,6 +72,8 @@ private: public: virtual ConnectionState get_connection_state() const override; + virtual GatheringState get_gathering_state() const override; + virtual SignalingState get_signaling_state() const override; virtual Error initialize(Dictionary configuration = Dictionary()) override; virtual Ref create_data_channel(String p_channel_name, Dictionary p_channel_config = Dictionary()) override;