webrtchttp: whipsink: Add candidates when sending the offer

WHIP endpoint providers like Cloudflare do not support Trickle ICE
and need candidates to be send along with the initial offer. Instead
of sending the offer in create-offer promise, send it once the ICE
candidates have been gathered.

While at it add properties to set STUN and TURN server along with the
ICE transport policy as at least when testing the Cloudflare WHIP
endpoint seems unreachable without it. This has also been observed
with Cloudflare provided demos.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/949>
This commit is contained in:
Sanchayan Maity 2022-10-27 22:00:57 +05:30
parent b992596236
commit cc7419308b
2 changed files with 192 additions and 29 deletions

View file

@ -7923,6 +7923,56 @@
"type": "gchararray", "type": "gchararray",
"writable": true "writable": true
}, },
"ice-transport-policy": {
"blurb": "The policy to apply for ICE transport",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "all (0)",
"mutable": "null",
"readable": true,
"type": "GstRsWebRTCICETransportPolicy",
"writable": true
},
"stun-server": {
"blurb": "The STUN server of the form stun://hostname:port",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "NULL",
"mutable": "null",
"readable": true,
"type": "gchararray",
"writable": true
},
"timeout": {
"blurb": "Value in seconds to timeout WHIP endpoint requests (0 = No timeout).",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "15",
"max": "3600",
"min": "0",
"mutable": "ready",
"readable": true,
"type": "guint",
"writable": true
},
"turn-server": {
"blurb": "The TURN server of the form turn(s)://username:password@host:port.",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "NULL",
"mutable": "null",
"readable": true,
"type": "gchararray",
"writable": true
},
"use-link-headers": { "use-link-headers": {
"blurb": "Use link headers to configure ice-servers from the WHIP server response to the POST or OPTIONS request.\n If set to TRUE and the WHIP server returns valid ice-servers,\n this property overrides the ice-servers values set using the stun-server and turn-server properties.", "blurb": "Use link headers to configure ice-servers from the WHIP server response to the POST or OPTIONS request.\n If set to TRUE and the WHIP server returns valid ice-servers,\n this property overrides the ice-servers values set using the stun-server and turn-server properties.",
"conditionally-available": false, "conditionally-available": false,

View file

@ -8,7 +8,9 @@
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use crate::utils::{build_reqwest_client, parse_redirect_location, set_ice_servers, wait}; use crate::utils::{build_reqwest_client, parse_redirect_location, set_ice_servers, wait};
use crate::GstRsWebRTCICETransportPolicy;
use futures::future; use futures::future;
use gst::element_imp_error;
use gst::glib; use gst::glib;
use gst::prelude::*; use gst::prelude::*;
use gst::subclass::prelude::*; use gst::subclass::prelude::*;
@ -25,6 +27,8 @@ static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new("whipsink", gst::DebugColorFlags::empty(), Some("WHIP Sink")) gst::DebugCategory::new("whipsink", gst::DebugColorFlags::empty(), Some("WHIP Sink"))
}); });
const DEFAULT_ICE_TRANSPORT_POLICY: GstRsWebRTCICETransportPolicy =
GstRsWebRTCICETransportPolicy::All;
const MAX_REDIRECTS: u8 = 10; const MAX_REDIRECTS: u8 = 10;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -32,6 +36,9 @@ struct Settings {
whip_endpoint: Option<String>, whip_endpoint: Option<String>,
use_link_headers: bool, use_link_headers: bool,
auth_token: Option<String>, auth_token: Option<String>,
turn_server: Option<String>,
stun_server: Option<String>,
ice_transport_policy: GstRsWebRTCICETransportPolicy,
} }
#[allow(clippy::derivable_impls)] #[allow(clippy::derivable_impls)]
@ -41,6 +48,9 @@ impl Default for Settings {
whip_endpoint: None, whip_endpoint: None,
use_link_headers: false, use_link_headers: false,
auth_token: None, auth_token: None,
stun_server: None,
turn_server: None,
ice_transport_policy: DEFAULT_ICE_TRANSPORT_POLICY,
} }
} }
} }
@ -206,7 +216,22 @@ impl ObjectImpl for WhipSink {
.nick("Authorization Token") .nick("Authorization Token")
.blurb("Authentication token to use, will be sent in the HTTP Header as 'Bearer <auth-token>'") .blurb("Authentication token to use, will be sent in the HTTP Header as 'Bearer <auth-token>'")
.mutable_ready() .mutable_ready()
.build() .build(),
glib::ParamSpecString::builder("stun-server")
.nick("STUN Server")
.blurb("The STUN server of the form stun://hostname:port")
.build(),
glib::ParamSpecString::builder("turn-server")
.nick("TURN Server")
.blurb("The TURN server of the form turn(s)://username:password@host:port.")
.build(),
glib::ParamSpecEnum::builder::<GstRsWebRTCICETransportPolicy>("ice-transport-policy", DEFAULT_ICE_TRANSPORT_POLICY)
.nick("ICE transport policy")
.blurb("The policy to apply for ICE transport")
.build(),
] ]
}); });
PROPERTIES.as_ref() PROPERTIES.as_ref()
@ -228,6 +253,36 @@ impl ObjectImpl for WhipSink {
let mut settings = self.settings.lock().unwrap(); let mut settings = self.settings.lock().unwrap();
settings.auth_token = value.get().expect("Auth token should be a string"); settings.auth_token = value.get().expect("Auth token should be a string");
} }
"stun-server" => {
let mut settings = self.settings.lock().unwrap();
settings.stun_server = value
.get::<Option<String>>()
.expect("type checked upstream");
self.webrtcbin
.set_property("stun-server", settings.stun_server.as_ref());
}
"turn-server" => {
let mut settings = self.settings.lock().unwrap();
settings.turn_server = value
.get::<Option<String>>()
.expect("type checked upstream");
self.webrtcbin
.set_property("turn-server", settings.turn_server.as_ref());
}
"ice-transport-policy" => {
let mut settings = self.settings.lock().unwrap();
settings.ice_transport_policy = value
.get::<GstRsWebRTCICETransportPolicy>()
.expect("ice-transport-policy should be an enum value");
if settings.ice_transport_policy == GstRsWebRTCICETransportPolicy::Relay {
self.webrtcbin
.set_property_from_str("ice-transport-policy", "relay");
} else {
self.webrtcbin
.set_property_from_str("ice-transport-policy", "all");
}
}
_ => unimplemented!(), _ => unimplemented!(),
} }
} }
@ -246,6 +301,18 @@ impl ObjectImpl for WhipSink {
let settings = self.settings.lock().unwrap(); let settings = self.settings.lock().unwrap();
settings.auth_token.to_value() settings.auth_token.to_value()
} }
"stun-server" => {
let settings = self.settings.lock().unwrap();
settings.stun_server.to_value()
}
"turn-server" => {
let settings = self.settings.lock().unwrap();
settings.turn_server.to_value()
}
"ice-transport-policy" => {
let settings = self.settings.lock().unwrap();
settings.ice_transport_policy.to_value()
}
_ => unimplemented!(), _ => unimplemented!(),
} }
} }
@ -256,12 +323,34 @@ impl ObjectImpl for WhipSink {
let obj = self.obj(); let obj = self.obj();
obj.set_suppressed_flags(gst::ElementFlags::SINK | gst::ElementFlags::SOURCE); obj.set_suppressed_flags(gst::ElementFlags::SINK | gst::ElementFlags::SOURCE);
obj.set_element_flags(gst::ElementFlags::SINK); obj.set_element_flags(gst::ElementFlags::SINK);
obj.add(&self.webrtcbin).unwrap();
// The spec requires all m= lines to be bundled (section 4.2) // The spec requires all m= lines to be bundled (section 4.2)
self.webrtcbin self.webrtcbin
.set_property("bundle-policy", gst_webrtc::WebRTCBundlePolicy::MaxBundle); .set_property("bundle-policy", gst_webrtc::WebRTCBundlePolicy::MaxBundle);
let self_weak = self.downgrade();
self.webrtcbin
.connect_notify(Some("ice-gathering-state"), move |webrtcbin, _pspec| {
let self_ = match self_weak.upgrade() {
Some(self_) => self_,
None => return,
};
let state = webrtcbin.property::<WebRTCICEGatheringState>("ice-gathering-state");
match state {
WebRTCICEGatheringState::Gathering => {
gst::info!(CAT, imp: self_, "ICE gathering started")
}
WebRTCICEGatheringState::Complete => {
gst::info!(CAT, imp: self_, "ICE gathering completed");
self_.send_offer();
}
_ => (),
}
});
self.webrtcbin.connect("on-negotiation-needed", false, { self.webrtcbin.connect("on-negotiation-needed", false, {
move |args| { move |args| {
let webrtcbin = args[0].get::<gst::Element>().unwrap(); let webrtcbin = args[0].get::<gst::Element>().unwrap();
@ -318,6 +407,7 @@ impl ObjectImpl for WhipSink {
Some(ele) => ele, Some(ele) => ele,
None => return, None => return,
}; };
let whipsink = ele.imp(); let whipsink = ele.imp();
let offer_sdp = match reply { let offer_sdp = match reply {
@ -343,13 +433,11 @@ impl ObjectImpl for WhipSink {
return; return;
} }
}; };
if let Err(e) = whipsink.send_offer(offer_sdp) {
gst::element_error!( whipsink.webrtcbin.emit_by_name::<()>(
ele, "set-local-description",
gst::ResourceError::Failed, &[&offer_sdp, &None::<gst::Promise>],
["Error in 'send_offer' - {}", e.to_string()] );
);
}
}); });
whipsink whipsink
@ -368,6 +456,8 @@ impl ObjectImpl for WhipSink {
None None
} }
}); });
obj.add(&self.webrtcbin).unwrap();
} }
} }
@ -377,6 +467,7 @@ impl ObjectSubclass for WhipSink {
type Type = super::WhipSink; type Type = super::WhipSink;
type ParentType = gst::Bin; type ParentType = gst::Bin;
} }
impl WhipSink { impl WhipSink {
fn lookup_ice_servers(&self, endpoint: reqwest::Url) -> Result<(), ErrorMessage> { fn lookup_ice_servers(&self, endpoint: reqwest::Url) -> Result<(), ErrorMessage> {
let settings = self.settings.lock().unwrap(); let settings = self.settings.lock().unwrap();
@ -464,28 +555,18 @@ impl WhipSink {
} }
} }
fn send_offer( fn send_offer(&self) {
&self,
offer: gst_webrtc::WebRTCSessionDescription,
) -> Result<(), gst::ErrorMessage> {
let settings = self.settings.lock().unwrap(); let settings = self.settings.lock().unwrap();
self.webrtcbin /* Note that we check for a valid WHIP endpoint in change_state */
.emit_by_name::<()>("set-local-description", &[&offer, &None::<gst::Promise>]);
if settings.whip_endpoint.is_none() {
return Err(gst::error_msg!(
gst::ResourceError::NotFound,
["Endpoint URL must be set"]
));
}
let endpoint = reqwest::Url::parse(settings.whip_endpoint.as_ref().unwrap().as_str()); let endpoint = reqwest::Url::parse(settings.whip_endpoint.as_ref().unwrap().as_str());
if let Err(e) = endpoint { if let Err(e) = endpoint {
return Err(gst::error_msg!( element_imp_error!(
self,
gst::ResourceError::Failed, gst::ResourceError::Failed,
["Could not parse endpoint URL: {}", e] ["Could not parse endpoint URL: {}", e]
)); );
return;
} }
drop(settings); drop(settings);
@ -493,12 +574,44 @@ impl WhipSink {
*state = State::Post { redirects: 0 }; *state = State::Post { redirects: 0 };
drop(state); drop(state);
let answer = self.do_post(offer, endpoint.unwrap())?; let local_desc = self
.webrtcbin
.property::<Option<WebRTCSessionDescription>>("local-description");
self.webrtcbin let offer_sdp = match local_desc {
.emit_by_name::<()>("set-remote-description", &[&answer, &None::<gst::Promise>]); None => {
element_imp_error!(
self,
gst::ResourceError::Failed,
["Local description is not set"]
);
return;
}
Some(offer) => offer,
};
Ok(()) gst::debug!(
CAT,
imp: self,
"Sending offer SDP: {:?}",
offer_sdp.sdp().as_text()
);
match self.do_post(offer_sdp, endpoint.unwrap()) {
Ok(answer) => {
self.webrtcbin.emit_by_name::<()>(
"set-remote-description",
&[&answer, &None::<gst::Promise>],
);
}
Err(e) => {
element_imp_error!(
self,
gst::ResourceError::Failed,
["Failed to send offer: {}", e]
);
}
}
} }
fn do_post( fn do_post(