Add a configuration parameter for using the media transport for data channels.
Adds a field |use_media_transport_for_data_channels| to RTCConfiguration. PeerConnection requires a media transport factory to be set if this bit is set. As with |use_media_transport|, the value may not be modified after setting the local or remote description. If either |use_media_transport| or |use_media_transport_for_data_channel| is set, PeerConnection uses its media transport factory when creating a JSEP transport controller. PeerConnection stops unconditionally using media transport in CreateVoiceChannel, as it may be present only for use in data channels. It uses the media transport if it is present and |use_media_transport| is set. Bug: webrtc:9719 Change-Id: I59d4ce8f7531fd19d9c17eefe033f063f663ebcc Reviewed-on: https://webrtc-review.googlesource.com/c/109041 Reviewed-by: Sami Kalliomäki <sakal@webrtc.org> Reviewed-by: Kári Helgason <kthelgason@webrtc.org> Reviewed-by: Steve Anton <steveanton@webrtc.org> Reviewed-by: Peter Slatala <psla@webrtc.org> Commit-Queue: Bjorn Mellem <mellem@webrtc.org> Cr-Commit-Position: refs/heads/master@{#25507}
This commit is contained in:
@ -583,6 +583,15 @@ class PeerConnectionInterface : public rtc::RefCountInterface {
|
|||||||
// provided.
|
// provided.
|
||||||
bool use_media_transport = false;
|
bool use_media_transport = false;
|
||||||
|
|
||||||
|
// If MediaTransportFactory is provided in PeerConnectionFactory, this flag
|
||||||
|
// informs PeerConnection that it should use the MediaTransportInterface for
|
||||||
|
// data channels. It's invalid to set it to |true| if the
|
||||||
|
// MediaTransportFactory wasn't provided. Data channels over media
|
||||||
|
// transport are not compatible with RTP or SCTP data channels. Setting
|
||||||
|
// both |use_media_transport_for_data_channels| and
|
||||||
|
// |enable_rtp_data_channel| is invalid.
|
||||||
|
bool use_media_transport_for_data_channels = false;
|
||||||
|
|
||||||
// Defines advanced optional cryptographic settings related to SRTP and
|
// Defines advanced optional cryptographic settings related to SRTP and
|
||||||
// frame encryption for native WebRTC. Setting this will overwrite any
|
// frame encryption for native WebRTC. Setting this will overwrite any
|
||||||
// settings set in PeerConnectionFactory (which is deprecated).
|
// settings set in PeerConnectionFactory (which is deprecated).
|
||||||
|
|||||||
@ -706,6 +706,7 @@ bool PeerConnectionInterface::RTCConfiguration::operator==(
|
|||||||
absl::optional<rtc::AdapterType> network_preference;
|
absl::optional<rtc::AdapterType> network_preference;
|
||||||
bool active_reset_srtp_params;
|
bool active_reset_srtp_params;
|
||||||
bool use_media_transport;
|
bool use_media_transport;
|
||||||
|
bool use_media_transport_for_data_channels;
|
||||||
absl::optional<CryptoOptions> crypto_options;
|
absl::optional<CryptoOptions> crypto_options;
|
||||||
};
|
};
|
||||||
static_assert(sizeof(stuff_being_tested_for_equality) == sizeof(*this),
|
static_assert(sizeof(stuff_being_tested_for_equality) == sizeof(*this),
|
||||||
@ -756,6 +757,8 @@ bool PeerConnectionInterface::RTCConfiguration::operator==(
|
|||||||
network_preference == o.network_preference &&
|
network_preference == o.network_preference &&
|
||||||
active_reset_srtp_params == o.active_reset_srtp_params &&
|
active_reset_srtp_params == o.active_reset_srtp_params &&
|
||||||
use_media_transport == o.use_media_transport &&
|
use_media_transport == o.use_media_transport &&
|
||||||
|
use_media_transport_for_data_channels ==
|
||||||
|
o.use_media_transport_for_data_channels &&
|
||||||
crypto_options == o.crypto_options;
|
crypto_options == o.crypto_options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -946,11 +949,13 @@ bool PeerConnection::Initialize(
|
|||||||
#endif
|
#endif
|
||||||
config.active_reset_srtp_params = configuration.active_reset_srtp_params;
|
config.active_reset_srtp_params = configuration.active_reset_srtp_params;
|
||||||
|
|
||||||
if (configuration.use_media_transport) {
|
if (configuration.use_media_transport ||
|
||||||
|
configuration.use_media_transport_for_data_channels) {
|
||||||
if (!factory_->media_transport_factory()) {
|
if (!factory_->media_transport_factory()) {
|
||||||
RTC_DCHECK(false)
|
RTC_DCHECK(false)
|
||||||
<< "PeerConnecton is initialized with use_media_transport = true, "
|
<< "PeerConnecton is initialized with use_media_transport = true or "
|
||||||
<< "but media transport factory is not set in PeerConnectioFactory";
|
<< "use_media_transport_for_data_channels = true "
|
||||||
|
<< "but media transport factory is not set in PeerConnectionFactory";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2919,6 +2924,22 @@ bool PeerConnection::SetConfiguration(const RTCConfiguration& configuration,
|
|||||||
return SafeSetError(RTCErrorType::INVALID_MODIFICATION, error);
|
return SafeSetError(RTCErrorType::INVALID_MODIFICATION, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (local_description() &&
|
||||||
|
configuration.use_media_transport_for_data_channels !=
|
||||||
|
configuration_.use_media_transport_for_data_channels) {
|
||||||
|
RTC_LOG(LS_ERROR) << "Can't change media_transport_for_data_channels "
|
||||||
|
"after calling SetLocalDescription.";
|
||||||
|
return SafeSetError(RTCErrorType::INVALID_MODIFICATION, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remote_description() &&
|
||||||
|
configuration.use_media_transport_for_data_channels !=
|
||||||
|
configuration_.use_media_transport_for_data_channels) {
|
||||||
|
RTC_LOG(LS_ERROR) << "Can't change media_transport_for_data_channels "
|
||||||
|
"after calling SetRemoteDescription.";
|
||||||
|
return SafeSetError(RTCErrorType::INVALID_MODIFICATION, error);
|
||||||
|
}
|
||||||
|
|
||||||
if (local_description() &&
|
if (local_description() &&
|
||||||
configuration.crypto_options != configuration_.crypto_options) {
|
configuration.crypto_options != configuration_.crypto_options) {
|
||||||
RTC_LOG(LS_ERROR) << "Can't change crypto_options after calling "
|
RTC_LOG(LS_ERROR) << "Can't change crypto_options after calling "
|
||||||
@ -2951,6 +2972,8 @@ bool PeerConnection::SetConfiguration(const RTCConfiguration& configuration,
|
|||||||
modified_config.active_reset_srtp_params =
|
modified_config.active_reset_srtp_params =
|
||||||
configuration.active_reset_srtp_params;
|
configuration.active_reset_srtp_params;
|
||||||
modified_config.use_media_transport = configuration.use_media_transport;
|
modified_config.use_media_transport = configuration.use_media_transport;
|
||||||
|
modified_config.use_media_transport_for_data_channels =
|
||||||
|
configuration.use_media_transport_for_data_channels;
|
||||||
if (configuration != modified_config) {
|
if (configuration != modified_config) {
|
||||||
RTC_LOG(LS_ERROR) << "Modifying the configuration in an unsupported way.";
|
RTC_LOG(LS_ERROR) << "Modifying the configuration in an unsupported way.";
|
||||||
return SafeSetError(RTCErrorType::INVALID_MODIFICATION, error);
|
return SafeSetError(RTCErrorType::INVALID_MODIFICATION, error);
|
||||||
@ -3009,7 +3032,9 @@ bool PeerConnection::SetConfiguration(const RTCConfiguration& configuration,
|
|||||||
|
|
||||||
transport_controller_->SetIceConfig(ParseIceConfig(modified_config));
|
transport_controller_->SetIceConfig(ParseIceConfig(modified_config));
|
||||||
transport_controller_->SetMediaTransportFactory(
|
transport_controller_->SetMediaTransportFactory(
|
||||||
modified_config.use_media_transport ? factory_->media_transport_factory()
|
modified_config.use_media_transport ||
|
||||||
|
modified_config.use_media_transport_for_data_channels
|
||||||
|
? factory_->media_transport_factory()
|
||||||
: nullptr);
|
: nullptr);
|
||||||
|
|
||||||
if (configuration_.active_reset_srtp_params !=
|
if (configuration_.active_reset_srtp_params !=
|
||||||
@ -5597,7 +5622,10 @@ RTCError PeerConnection::CreateChannels(const SessionDescription& desc) {
|
|||||||
cricket::VoiceChannel* PeerConnection::CreateVoiceChannel(
|
cricket::VoiceChannel* PeerConnection::CreateVoiceChannel(
|
||||||
const std::string& mid) {
|
const std::string& mid) {
|
||||||
RtpTransportInternal* rtp_transport = GetRtpTransport(mid);
|
RtpTransportInternal* rtp_transport = GetRtpTransport(mid);
|
||||||
MediaTransportInterface* media_transport = GetMediaTransport(mid);
|
MediaTransportInterface* media_transport = nullptr;
|
||||||
|
if (configuration_.use_media_transport) {
|
||||||
|
media_transport = GetMediaTransport(mid);
|
||||||
|
}
|
||||||
|
|
||||||
cricket::VoiceChannel* voice_channel = channel_manager()->CreateVoiceChannel(
|
cricket::VoiceChannel* voice_channel = channel_manager()->CreateVoiceChannel(
|
||||||
call_.get(), configuration_.media_config, rtp_transport, media_transport,
|
call_.get(), configuration_.media_config, rtp_transport, media_transport,
|
||||||
|
|||||||
@ -938,10 +938,13 @@ class PeerConnection : public PeerConnectionInternal,
|
|||||||
// to use media transport. Otherwise returns nullptr.
|
// to use media transport. Otherwise returns nullptr.
|
||||||
MediaTransportInterface* GetMediaTransport(const std::string& mid) {
|
MediaTransportInterface* GetMediaTransport(const std::string& mid) {
|
||||||
auto media_transport = transport_controller_->GetMediaTransport(mid);
|
auto media_transport = transport_controller_->GetMediaTransport(mid);
|
||||||
RTC_DCHECK(configuration_.use_media_transport ==
|
RTC_DCHECK((configuration_.use_media_transport ||
|
||||||
|
configuration_.use_media_transport_for_data_channels) ==
|
||||||
(media_transport != nullptr))
|
(media_transport != nullptr))
|
||||||
<< "configuration_.use_media_transport="
|
<< "configuration_.use_media_transport="
|
||||||
<< configuration_.use_media_transport
|
<< configuration_.use_media_transport
|
||||||
|
<< ", configuration_.use_media_transport_for_data_channels="
|
||||||
|
<< configuration_.use_media_transport_for_data_channels
|
||||||
<< ", (media_transport != nullptr)=" << (media_transport != nullptr);
|
<< ", (media_transport != nullptr)=" << (media_transport != nullptr);
|
||||||
return media_transport;
|
return media_transport;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1129,6 +1129,74 @@ TEST_P(PeerConnectionMediaTest, MediaTransportPropagatedToVoiceEngine) {
|
|||||||
ASSERT_EQ(nullptr, callee_video->media_transport());
|
ASSERT_EQ(nullptr, callee_video->media_transport());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_P(PeerConnectionMediaTest, MediaTransportOnlyForDataChannels) {
|
||||||
|
RTCConfiguration config;
|
||||||
|
|
||||||
|
// Setup PeerConnection to use media transport for data channels.
|
||||||
|
config.use_media_transport_for_data_channels = true;
|
||||||
|
|
||||||
|
// Force SDES.
|
||||||
|
config.enable_dtls_srtp = false;
|
||||||
|
|
||||||
|
auto caller = CreatePeerConnectionWithAudioVideo(config);
|
||||||
|
auto callee = CreatePeerConnectionWithAudioVideo(config);
|
||||||
|
|
||||||
|
ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
|
||||||
|
ASSERT_TRUE(callee->SetLocalDescription(callee->CreateAnswer()));
|
||||||
|
|
||||||
|
auto caller_voice = caller->media_engine()->GetVoiceChannel(0);
|
||||||
|
auto callee_voice = callee->media_engine()->GetVoiceChannel(0);
|
||||||
|
ASSERT_TRUE(caller_voice);
|
||||||
|
ASSERT_TRUE(callee_voice);
|
||||||
|
|
||||||
|
// Make sure media transport is not propagated to voice channel.
|
||||||
|
EXPECT_EQ(nullptr, caller_voice->media_transport());
|
||||||
|
EXPECT_EQ(nullptr, callee_voice->media_transport());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_P(PeerConnectionMediaTest, MediaTransportForMediaAndDataChannels) {
|
||||||
|
RTCConfiguration config;
|
||||||
|
|
||||||
|
// Setup PeerConnection to use media transport for both media and data
|
||||||
|
// channels.
|
||||||
|
config.use_media_transport = true;
|
||||||
|
config.use_media_transport_for_data_channels = true;
|
||||||
|
|
||||||
|
// Force SDES.
|
||||||
|
config.enable_dtls_srtp = false;
|
||||||
|
|
||||||
|
auto caller = CreatePeerConnectionWithAudioVideo(config);
|
||||||
|
auto callee = CreatePeerConnectionWithAudioVideo(config);
|
||||||
|
|
||||||
|
ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
|
||||||
|
ASSERT_TRUE(callee->SetLocalDescription(callee->CreateAnswer()));
|
||||||
|
|
||||||
|
auto caller_voice = caller->media_engine()->GetVoiceChannel(0);
|
||||||
|
auto callee_voice = callee->media_engine()->GetVoiceChannel(0);
|
||||||
|
ASSERT_TRUE(caller_voice);
|
||||||
|
ASSERT_TRUE(callee_voice);
|
||||||
|
|
||||||
|
// Make sure media transport is propagated to voice channel.
|
||||||
|
FakeMediaTransport* caller_voice_media_transport =
|
||||||
|
static_cast<FakeMediaTransport*>(caller_voice->media_transport());
|
||||||
|
FakeMediaTransport* callee_voice_media_transport =
|
||||||
|
static_cast<FakeMediaTransport*>(callee_voice->media_transport());
|
||||||
|
ASSERT_NE(nullptr, caller_voice_media_transport);
|
||||||
|
ASSERT_NE(nullptr, callee_voice_media_transport);
|
||||||
|
|
||||||
|
// Make sure media transport is created with correct is_caller.
|
||||||
|
EXPECT_TRUE(caller_voice_media_transport->is_caller());
|
||||||
|
EXPECT_FALSE(callee_voice_media_transport->is_caller());
|
||||||
|
|
||||||
|
// TODO(sukhanov): Propagate media transport to video channel. This test
|
||||||
|
// will fail once media transport is propagated to video channel and it will
|
||||||
|
// serve as a reminder to add a test for video channel propagation.
|
||||||
|
auto caller_video = caller->media_engine()->GetVideoChannel(0);
|
||||||
|
auto callee_video = callee->media_engine()->GetVideoChannel(0);
|
||||||
|
ASSERT_EQ(nullptr, caller_video->media_transport());
|
||||||
|
ASSERT_EQ(nullptr, callee_video->media_transport());
|
||||||
|
}
|
||||||
|
|
||||||
TEST_P(PeerConnectionMediaTest, MediaTransportNotPropagatedToVoiceEngine) {
|
TEST_P(PeerConnectionMediaTest, MediaTransportNotPropagatedToVoiceEngine) {
|
||||||
auto caller = CreatePeerConnectionWithAudioVideo();
|
auto caller = CreatePeerConnectionWithAudioVideo();
|
||||||
auto callee = CreatePeerConnectionWithAudioVideo();
|
auto callee = CreatePeerConnectionWithAudioVideo();
|
||||||
|
|||||||
@ -1431,12 +1431,14 @@ TEST_P(PeerConnectionInterfaceTest, GetConfigurationAfterSetConfiguration) {
|
|||||||
PeerConnectionInterface::RTCConfiguration config = pc_->GetConfiguration();
|
PeerConnectionInterface::RTCConfiguration config = pc_->GetConfiguration();
|
||||||
config.type = PeerConnectionInterface::kRelay;
|
config.type = PeerConnectionInterface::kRelay;
|
||||||
config.use_media_transport = true;
|
config.use_media_transport = true;
|
||||||
|
config.use_media_transport_for_data_channels = true;
|
||||||
EXPECT_TRUE(pc_->SetConfiguration(config));
|
EXPECT_TRUE(pc_->SetConfiguration(config));
|
||||||
|
|
||||||
PeerConnectionInterface::RTCConfiguration returned_config =
|
PeerConnectionInterface::RTCConfiguration returned_config =
|
||||||
pc_->GetConfiguration();
|
pc_->GetConfiguration();
|
||||||
EXPECT_EQ(PeerConnectionInterface::kRelay, returned_config.type);
|
EXPECT_EQ(PeerConnectionInterface::kRelay, returned_config.type);
|
||||||
EXPECT_TRUE(returned_config.use_media_transport);
|
EXPECT_TRUE(returned_config.use_media_transport);
|
||||||
|
EXPECT_TRUE(returned_config.use_media_transport_for_data_channels);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_P(PeerConnectionInterfaceTest, SetConfigurationFailsAfterClose) {
|
TEST_P(PeerConnectionInterfaceTest, SetConfigurationFailsAfterClose) {
|
||||||
|
|||||||
@ -468,6 +468,12 @@ public class PeerConnection {
|
|||||||
*/
|
*/
|
||||||
public boolean useMediaTransport;
|
public boolean useMediaTransport;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Experimental flag that enables a use of media transport for data channels. If this is true,
|
||||||
|
* the media transport factory MUST be provided to the PeerConnectionFactory.
|
||||||
|
*/
|
||||||
|
public boolean useMediaTransportForDataChannels;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines advanced optional cryptographic settings related to SRTP and
|
* Defines advanced optional cryptographic settings related to SRTP and
|
||||||
* frame encryption for native WebRTC. Setting this will overwrite any
|
* frame encryption for native WebRTC. Setting this will overwrite any
|
||||||
@ -515,6 +521,7 @@ public class PeerConnection {
|
|||||||
sdpSemantics = SdpSemantics.PLAN_B;
|
sdpSemantics = SdpSemantics.PLAN_B;
|
||||||
activeResetSrtpParams = false;
|
activeResetSrtpParams = false;
|
||||||
useMediaTransport = false;
|
useMediaTransport = false;
|
||||||
|
useMediaTransportForDataChannels = false;
|
||||||
cryptoOptions = null;
|
cryptoOptions = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -720,6 +727,11 @@ public class PeerConnection {
|
|||||||
return useMediaTransport;
|
return useMediaTransport;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CalledByNative("RTCConfiguration")
|
||||||
|
boolean getUseMediaTransportForDataChannels() {
|
||||||
|
return useMediaTransportForDataChannels;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@CalledByNative("RTCConfiguration")
|
@CalledByNative("RTCConfiguration")
|
||||||
CryptoOptions getCryptoOptions() {
|
CryptoOptions getCryptoOptions() {
|
||||||
|
|||||||
@ -249,6 +249,9 @@ void JavaToNativeRTCConfiguration(
|
|||||||
Java_RTCConfiguration_getActiveResetSrtpParams(jni, j_rtc_config);
|
Java_RTCConfiguration_getActiveResetSrtpParams(jni, j_rtc_config);
|
||||||
rtc_config->use_media_transport =
|
rtc_config->use_media_transport =
|
||||||
Java_RTCConfiguration_getUseMediaTransport(jni, j_rtc_config);
|
Java_RTCConfiguration_getUseMediaTransport(jni, j_rtc_config);
|
||||||
|
rtc_config->use_media_transport_for_data_channels =
|
||||||
|
Java_RTCConfiguration_getUseMediaTransportForDataChannels(jni,
|
||||||
|
j_rtc_config);
|
||||||
rtc_config->crypto_options =
|
rtc_config->crypto_options =
|
||||||
JavaToNativeOptionalCryptoOptions(jni, j_crypto_options);
|
JavaToNativeOptionalCryptoOptions(jni, j_crypto_options);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -188,6 +188,12 @@ RTC_OBJC_EXPORT
|
|||||||
*/
|
*/
|
||||||
@property(nonatomic, assign) BOOL useMediaTransport;
|
@property(nonatomic, assign) BOOL useMediaTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If MediaTransportFactory is provided in PeerConnectionFactory, this flag informs PeerConnection
|
||||||
|
* that it should use the MediaTransportInterface for data channels.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, assign) BOOL useMediaTransportForDataChannels;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines advanced optional cryptographic settings related to SRTP and
|
* Defines advanced optional cryptographic settings related to SRTP and
|
||||||
* frame encryption for native WebRTC. Setting this will overwrite any
|
* frame encryption for native WebRTC. Setting this will overwrite any
|
||||||
|
|||||||
@ -51,6 +51,7 @@
|
|||||||
@synthesize turnCustomizer = _turnCustomizer;
|
@synthesize turnCustomizer = _turnCustomizer;
|
||||||
@synthesize activeResetSrtpParams = _activeResetSrtpParams;
|
@synthesize activeResetSrtpParams = _activeResetSrtpParams;
|
||||||
@synthesize useMediaTransport = _useMediaTransport;
|
@synthesize useMediaTransport = _useMediaTransport;
|
||||||
|
@synthesize useMediaTransportForDataChannels = _useMediaTransportForDataChannels;
|
||||||
@synthesize cryptoOptions = _cryptoOptions;
|
@synthesize cryptoOptions = _cryptoOptions;
|
||||||
|
|
||||||
- (instancetype)init {
|
- (instancetype)init {
|
||||||
@ -100,6 +101,7 @@
|
|||||||
_iceBackupCandidatePairPingInterval =
|
_iceBackupCandidatePairPingInterval =
|
||||||
config.ice_backup_candidate_pair_ping_interval;
|
config.ice_backup_candidate_pair_ping_interval;
|
||||||
_useMediaTransport = config.use_media_transport;
|
_useMediaTransport = config.use_media_transport;
|
||||||
|
_useMediaTransportForDataChannels = config.use_media_transport_for_data_channels;
|
||||||
_keyType = RTCEncryptionKeyTypeECDSA;
|
_keyType = RTCEncryptionKeyTypeECDSA;
|
||||||
_iceCandidatePoolSize = config.ice_candidate_pool_size;
|
_iceCandidatePoolSize = config.ice_candidate_pool_size;
|
||||||
_shouldPruneTurnPorts = config.prune_turn_ports;
|
_shouldPruneTurnPorts = config.prune_turn_ports;
|
||||||
@ -199,6 +201,7 @@
|
|||||||
nativeConfig->ice_backup_candidate_pair_ping_interval =
|
nativeConfig->ice_backup_candidate_pair_ping_interval =
|
||||||
_iceBackupCandidatePairPingInterval;
|
_iceBackupCandidatePairPingInterval;
|
||||||
nativeConfig->use_media_transport = _useMediaTransport;
|
nativeConfig->use_media_transport = _useMediaTransport;
|
||||||
|
nativeConfig->use_media_transport_for_data_channels = _useMediaTransportForDataChannels;
|
||||||
rtc::KeyType keyType =
|
rtc::KeyType keyType =
|
||||||
[[self class] nativeEncryptionKeyTypeForKeyType:_keyType];
|
[[self class] nativeEncryptionKeyTypeForKeyType:_keyType];
|
||||||
if (_certificate != nullptr) {
|
if (_certificate != nullptr) {
|
||||||
|
|||||||
Reference in New Issue
Block a user