mirror of
https://github.com/mollyim/webrtc.git
synced 2025-05-13 05:40:42 +01:00
Datagram Transport Integration
- Implement datagram transport adaptor, which wraps datagram transport in DtlsTransportInternal. Datagram adaptor owns both ICE and Datagram Transports. - Implement setup of datagram transport based on RTCConfiguration flag use_datagram_transport. This is very similar to MediaTransport setup with the exception that we create DTLS datagram adaptor. - Propagate maximum datagram size to video encoder via MediaTransportConfig. TODO: Currently this CL can only be tested in downstream projects. Once we add fake datagram transport, we will be able to implement unit tests similar to loopback media transport. Bug: webrtc:9719 Change-Id: I4fa4a5725598dfee5da4f0f374269a7e289d48ed Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/138100 Commit-Queue: Anton Sukhanov <sukhanov@webrtc.org> Reviewed-by: Bjorn Mellem <mellem@webrtc.org> Reviewed-by: Steve Anton <steveanton@webrtc.org> Cr-Commit-Position: refs/heads/master@{#28047}
This commit is contained in:
parent
c1c0d6d8ad
commit
316f3ac13b
17 changed files with 917 additions and 98 deletions
|
@ -10,11 +10,30 @@
|
|||
|
||||
#include "api/media_transport_config.h"
|
||||
|
||||
#include "rtc_base/checks.h"
|
||||
#include "rtc_base/string_utils.h"
|
||||
#include "rtc_base/strings/string_builder.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
std::string MediaTransportConfig::DebugString() const {
|
||||
return (media_transport != nullptr ? "{media_transport: (Transport)}"
|
||||
: "{media_transport: null}");
|
||||
MediaTransportConfig::MediaTransportConfig(
|
||||
MediaTransportInterface* media_transport)
|
||||
: media_transport(media_transport) {
|
||||
RTC_DCHECK(media_transport != nullptr);
|
||||
}
|
||||
|
||||
MediaTransportConfig::MediaTransportConfig(size_t rtp_max_packet_size)
|
||||
: rtp_max_packet_size(rtp_max_packet_size) {
|
||||
RTC_DCHECK_GT(rtp_max_packet_size, 0);
|
||||
}
|
||||
|
||||
std::string MediaTransportConfig::DebugString()
|
||||
const { // TODO(sukhanov): Add rtp_max_packet_size (requires fixing
|
||||
// audio_send/receive_stream_unittest.cc).
|
||||
rtc::StringBuilder result;
|
||||
result << "{media_transport: "
|
||||
<< (media_transport != nullptr ? "(Transport)" : "null") << "}";
|
||||
return result.Release();
|
||||
}
|
||||
|
||||
} // namespace webrtc
|
||||
|
|
|
@ -13,28 +13,33 @@
|
|||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "absl/types/optional.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
class MediaTransportInterface;
|
||||
|
||||
// MediaTransportConfig contains meida transport (if provided) and passed from
|
||||
// PeerConnection to call obeject and media layers that require access to media
|
||||
// transport. In the future we can add other transport (for example, datagram
|
||||
// transport) and related configuration.
|
||||
// Media transport config is made available to both transport and audio / video
|
||||
// layers, but access to individual interfaces should not be open without
|
||||
// necessity.
|
||||
struct MediaTransportConfig {
|
||||
// Default constructor for no-media transport scenarios.
|
||||
MediaTransportConfig() = default;
|
||||
|
||||
// TODO(sukhanov): Consider adding RtpTransport* to MediaTransportConfig,
|
||||
// because it's almost always passes along with media_transport.
|
||||
// Does not own media_transport.
|
||||
explicit MediaTransportConfig(MediaTransportInterface* media_transport)
|
||||
: media_transport(media_transport) {}
|
||||
// Constructor for media transport scenarios.
|
||||
// Note that |media_transport| may not be nullptr.
|
||||
explicit MediaTransportConfig(MediaTransportInterface* media_transport);
|
||||
|
||||
// Constructor for datagram transport scenarios.
|
||||
explicit MediaTransportConfig(size_t rtp_max_packet_size);
|
||||
|
||||
std::string DebugString() const;
|
||||
|
||||
// If provided, all media is sent through media_transport.
|
||||
MediaTransportInterface* media_transport = nullptr;
|
||||
|
||||
// If provided, limits RTP packet size (excludes ICE, IP or network overhead).
|
||||
absl::optional<size_t> rtp_max_packet_size;
|
||||
};
|
||||
|
||||
} // namespace webrtc
|
||||
|
|
|
@ -74,8 +74,7 @@ class RtpDataMediaChannelTest : public ::testing::Test {
|
|||
cricket::MediaConfig config;
|
||||
cricket::RtpDataMediaChannel* channel =
|
||||
static_cast<cricket::RtpDataMediaChannel*>(dme->CreateChannel(config));
|
||||
channel->SetInterface(iface_.get(), webrtc::MediaTransportConfig(
|
||||
/*media_transport=*/nullptr));
|
||||
channel->SetInterface(iface_.get(), webrtc::MediaTransportConfig());
|
||||
channel->SignalDataReceived.connect(receiver_.get(),
|
||||
&FakeDataReceiver::OnDataReceived);
|
||||
return channel;
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
#include "absl/algorithm/container.h"
|
||||
#include "absl/strings/match.h"
|
||||
#include "api/datagram_transport_interface.h"
|
||||
#include "api/video/video_codec_constants.h"
|
||||
#include "api/video_codecs/sdp_video_format.h"
|
||||
#include "api/video_codecs/video_decoder_factory.h"
|
||||
|
@ -1101,6 +1102,13 @@ bool WebRtcVideoChannel::AddSendStream(const StreamParams& sp) {
|
|||
config.rtp.extmap_allow_mixed = ExtmapAllowMixed();
|
||||
config.rtcp_report_interval_ms = video_config_.rtcp_report_interval_ms;
|
||||
|
||||
// If sending through Datagram Transport, limit packet size to maximum
|
||||
// packet size supported by datagram_transport.
|
||||
if (media_transport_config().rtp_max_packet_size) {
|
||||
config.rtp.max_packet_size =
|
||||
media_transport_config().rtp_max_packet_size.value();
|
||||
}
|
||||
|
||||
WebRtcVideoSendStream* stream = new WebRtcVideoSendStream(
|
||||
call_, sp, std::move(config), default_send_options_,
|
||||
video_config_.enable_cpu_adaptation, bitrate_config_.max_bitrate_bps,
|
||||
|
|
|
@ -25,6 +25,8 @@ rtc_static_library("rtc_p2p") {
|
|||
"base/basic_packet_socket_factory.cc",
|
||||
"base/basic_packet_socket_factory.h",
|
||||
"base/candidate_pair_interface.h",
|
||||
"base/datagram_dtls_adaptor.cc",
|
||||
"base/datagram_dtls_adaptor.h",
|
||||
"base/dtls_transport.cc",
|
||||
"base/dtls_transport.h",
|
||||
"base/dtls_transport_internal.cc",
|
||||
|
|
405
p2p/base/datagram_dtls_adaptor.cc
Normal file
405
p2p/base/datagram_dtls_adaptor.cc
Normal file
|
@ -0,0 +1,405 @@
|
|||
/*
|
||||
* Copyright 2018 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
#include "p2p/base/datagram_dtls_adaptor.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "absl/memory/memory.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "api/rtc_error.h"
|
||||
#include "logging/rtc_event_log/events/rtc_event_dtls_transport_state.h"
|
||||
#include "logging/rtc_event_log/events/rtc_event_dtls_writable_state.h"
|
||||
#include "logging/rtc_event_log/rtc_event_log.h"
|
||||
#include "p2p/base/dtls_transport_internal.h"
|
||||
#include "p2p/base/packet_transport_internal.h"
|
||||
#include "rtc_base/buffer.h"
|
||||
#include "rtc_base/checks.h"
|
||||
#include "rtc_base/dscp.h"
|
||||
#include "rtc_base/flags.h"
|
||||
#include "rtc_base/logging.h"
|
||||
#include "rtc_base/message_queue.h"
|
||||
#include "rtc_base/rtc_certificate.h"
|
||||
#include "rtc_base/ssl_stream_adapter.h"
|
||||
#include "rtc_base/stream.h"
|
||||
#include "rtc_base/thread.h"
|
||||
|
||||
#ifdef BYPASS_DATAGRAM_DTLS_TEST_ONLY
|
||||
// Send unencrypted packets directly to ICE, bypassing datagtram
|
||||
// transport. Use in tests only.
|
||||
constexpr bool kBypassDatagramDtlsTestOnly = true;
|
||||
#else
|
||||
constexpr bool kBypassDatagramDtlsTestOnly = false;
|
||||
#endif
|
||||
|
||||
namespace cricket {
|
||||
|
||||
DatagramDtlsAdaptor::DatagramDtlsAdaptor(
|
||||
std::unique_ptr<IceTransportInternal> ice_transport,
|
||||
std::unique_ptr<webrtc::DatagramTransportInterface> datagram_transport,
|
||||
const webrtc::CryptoOptions& crypto_options,
|
||||
webrtc::RtcEventLog* event_log)
|
||||
: crypto_options_(crypto_options),
|
||||
ice_transport_(std::move(ice_transport)),
|
||||
datagram_transport_(std::move(datagram_transport)),
|
||||
event_log_(event_log) {
|
||||
RTC_DCHECK(ice_transport_);
|
||||
RTC_DCHECK(datagram_transport_);
|
||||
ConnectToIceTransport();
|
||||
}
|
||||
|
||||
void DatagramDtlsAdaptor::ConnectToIceTransport() {
|
||||
if (kBypassDatagramDtlsTestOnly) {
|
||||
// In bypass mode we have to subscribe to ICE read and sent events.
|
||||
// Test only case to use ICE directly instead of data transport.
|
||||
ice_transport_->SignalReadPacket.connect(
|
||||
this, &DatagramDtlsAdaptor::OnReadPacket);
|
||||
|
||||
ice_transport_->SignalSentPacket.connect(
|
||||
this, &DatagramDtlsAdaptor::OnSentPacket);
|
||||
|
||||
ice_transport_->SignalWritableState.connect(
|
||||
this, &DatagramDtlsAdaptor::OnWritableState);
|
||||
ice_transport_->SignalReadyToSend.connect(
|
||||
this, &DatagramDtlsAdaptor::OnReadyToSend);
|
||||
ice_transport_->SignalReceivingState.connect(
|
||||
this, &DatagramDtlsAdaptor::OnReceivingState);
|
||||
} else {
|
||||
// Subscribe to Data Transport read packets.
|
||||
datagram_transport_->SetDatagramSink(this);
|
||||
datagram_transport_->SetTransportStateCallback(this);
|
||||
|
||||
// Datagram transport does not propagate network route change.
|
||||
ice_transport_->SignalNetworkRouteChanged.connect(
|
||||
this, &DatagramDtlsAdaptor::OnNetworkRouteChanged);
|
||||
}
|
||||
}
|
||||
|
||||
DatagramDtlsAdaptor::~DatagramDtlsAdaptor() {
|
||||
// Unsubscribe from Datagram Transport dinks.
|
||||
datagram_transport_->SetDatagramSink(nullptr);
|
||||
datagram_transport_->SetTransportStateCallback(nullptr);
|
||||
|
||||
// Make sure datagram transport is destroyed before ICE.
|
||||
datagram_transport_.reset();
|
||||
ice_transport_.reset();
|
||||
}
|
||||
|
||||
const webrtc::CryptoOptions& DatagramDtlsAdaptor::crypto_options() const {
|
||||
return crypto_options_;
|
||||
}
|
||||
|
||||
int DatagramDtlsAdaptor::SendPacket(const char* data,
|
||||
size_t len,
|
||||
const rtc::PacketOptions& options,
|
||||
int flags) {
|
||||
// TODO(sukhanov): Handle options and flags.
|
||||
if (kBypassDatagramDtlsTestOnly) {
|
||||
// In bypass mode sent directly to ICE.
|
||||
return ice_transport_->SendPacket(data, len, options);
|
||||
}
|
||||
|
||||
// Send datagram with id equal to options.packet_id, so we get it back
|
||||
// in DatagramDtlsAdaptor::OnDatagramSent() and propagate notification
|
||||
// up.
|
||||
webrtc::RTCError error = datagram_transport_->SendDatagram(
|
||||
rtc::MakeArrayView(reinterpret_cast<const uint8_t*>(data), len),
|
||||
/*datagram_id=*/options.packet_id);
|
||||
|
||||
return (error.ok() ? len : -1);
|
||||
}
|
||||
|
||||
void DatagramDtlsAdaptor::OnReadPacket(rtc::PacketTransportInternal* transport,
|
||||
const char* data,
|
||||
size_t size,
|
||||
const int64_t& packet_time_us,
|
||||
int flags) {
|
||||
// Only used in bypass mode.
|
||||
RTC_DCHECK(kBypassDatagramDtlsTestOnly);
|
||||
|
||||
RTC_DCHECK_RUN_ON(&thread_checker_);
|
||||
RTC_DCHECK_EQ(transport, ice_transport_.get());
|
||||
RTC_DCHECK(flags == 0);
|
||||
|
||||
PropagateReadPacket(
|
||||
rtc::MakeArrayView(reinterpret_cast<const uint8_t*>(data), size),
|
||||
packet_time_us);
|
||||
}
|
||||
|
||||
void DatagramDtlsAdaptor::OnDatagramReceived(
|
||||
rtc::ArrayView<const uint8_t> data) {
|
||||
RTC_DCHECK_RUN_ON(&thread_checker_);
|
||||
RTC_DCHECK(!kBypassDatagramDtlsTestOnly);
|
||||
|
||||
// TODO(sukhanov): I am not filling out time, but on my video quality
|
||||
// test in WebRTC the time was not set either and higher layers of the stack
|
||||
// overwrite -1 with current current rtc time. Leaveing comment for now to
|
||||
// make sure it works as expected.
|
||||
int64_t packet_time_us = -1;
|
||||
|
||||
PropagateReadPacket(data, packet_time_us);
|
||||
}
|
||||
|
||||
void DatagramDtlsAdaptor::OnDatagramSent(webrtc::DatagramId datagram_id) {
|
||||
// When we called DatagramTransportInterface::SendDatagram, we passed
|
||||
// packet_id as datagram_id, so we simply need to set it in sent_packet
|
||||
// and propagate notification up the stack.
|
||||
|
||||
// Also see how DatagramDtlsAdaptor::OnSentPacket handles OnSentPacket
|
||||
// notification from ICE in bypass mode.
|
||||
rtc::SentPacket sent_packet(/*packet_id=*/datagram_id, rtc::TimeMillis());
|
||||
|
||||
PropagateOnSentNotification(sent_packet);
|
||||
}
|
||||
|
||||
void DatagramDtlsAdaptor::OnSentPacket(rtc::PacketTransportInternal* transport,
|
||||
const rtc::SentPacket& sent_packet) {
|
||||
// Only used in bypass mode.
|
||||
RTC_DCHECK(kBypassDatagramDtlsTestOnly);
|
||||
RTC_DCHECK_RUN_ON(&thread_checker_);
|
||||
|
||||
PropagateOnSentNotification(sent_packet);
|
||||
}
|
||||
|
||||
void DatagramDtlsAdaptor::PropagateOnSentNotification(
|
||||
const rtc::SentPacket& sent_packet) {
|
||||
RTC_DCHECK_RUN_ON(&thread_checker_);
|
||||
SignalSentPacket(this, sent_packet);
|
||||
}
|
||||
|
||||
void DatagramDtlsAdaptor::PropagateReadPacket(
|
||||
rtc::ArrayView<const uint8_t> data,
|
||||
const int64_t& packet_time_us) {
|
||||
RTC_DCHECK_RUN_ON(&thread_checker_);
|
||||
SignalReadPacket(this, reinterpret_cast<const char*>(data.data()),
|
||||
data.size(), packet_time_us, /*flags=*/0);
|
||||
}
|
||||
|
||||
int DatagramDtlsAdaptor::component() const {
|
||||
return kDatagramDtlsAdaptorComponent;
|
||||
}
|
||||
bool DatagramDtlsAdaptor::IsDtlsActive() const {
|
||||
return false;
|
||||
}
|
||||
bool DatagramDtlsAdaptor::GetDtlsRole(rtc::SSLRole* role) const {
|
||||
return false;
|
||||
}
|
||||
bool DatagramDtlsAdaptor::SetDtlsRole(rtc::SSLRole role) {
|
||||
return false;
|
||||
}
|
||||
bool DatagramDtlsAdaptor::GetSrtpCryptoSuite(int* cipher) {
|
||||
return false;
|
||||
}
|
||||
bool DatagramDtlsAdaptor::GetSslCipherSuite(int* cipher) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rtc::scoped_refptr<rtc::RTCCertificate>
|
||||
DatagramDtlsAdaptor::GetLocalCertificate() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool DatagramDtlsAdaptor::SetLocalCertificate(
|
||||
const rtc::scoped_refptr<rtc::RTCCertificate>& certificate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<rtc::SSLCertChain> DatagramDtlsAdaptor::GetRemoteSSLCertChain()
|
||||
const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool DatagramDtlsAdaptor::ExportKeyingMaterial(const std::string& label,
|
||||
const uint8_t* context,
|
||||
size_t context_len,
|
||||
bool use_context,
|
||||
uint8_t* result,
|
||||
size_t result_len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DatagramDtlsAdaptor::SetRemoteFingerprint(const std::string& digest_alg,
|
||||
const uint8_t* digest,
|
||||
size_t digest_len) {
|
||||
// TODO(sukhanov): We probably should not called with fingerptints in
|
||||
// datagram scenario, but we may need to change code up the stack before
|
||||
// we can return false or DCHECK.
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DatagramDtlsAdaptor::SetSslMaxProtocolVersion(
|
||||
rtc::SSLProtocolVersion version) {
|
||||
// TODO(sukhanov): We may be able to return false and/or DCHECK that we
|
||||
// are not called if datagram transport is used, but we need to change
|
||||
// integration before we can do it.
|
||||
return true;
|
||||
}
|
||||
|
||||
IceTransportInternal* DatagramDtlsAdaptor::ice_transport() {
|
||||
return ice_transport_.get();
|
||||
}
|
||||
|
||||
webrtc::DatagramTransportInterface* DatagramDtlsAdaptor::datagram_transport() {
|
||||
return datagram_transport_.get();
|
||||
}
|
||||
|
||||
// Similar implementaton as in p2p/base/dtls_transport.cc.
|
||||
void DatagramDtlsAdaptor::OnReadyToSend(
|
||||
rtc::PacketTransportInternal* transport) {
|
||||
RTC_DCHECK_RUN_ON(&thread_checker_);
|
||||
if (writable()) {
|
||||
SignalReadyToSend(this);
|
||||
}
|
||||
}
|
||||
|
||||
void DatagramDtlsAdaptor::OnWritableState(
|
||||
rtc::PacketTransportInternal* transport) {
|
||||
RTC_DCHECK_RUN_ON(&thread_checker_);
|
||||
RTC_DCHECK(transport == ice_transport_.get());
|
||||
RTC_LOG(LS_VERBOSE) << ": ice_transport writable state changed to "
|
||||
<< ice_transport_->writable();
|
||||
|
||||
if (kBypassDatagramDtlsTestOnly) {
|
||||
// Note: SignalWritableState fired by set_writable.
|
||||
set_writable(ice_transport_->writable());
|
||||
return;
|
||||
}
|
||||
|
||||
switch (dtls_state()) {
|
||||
case DTLS_TRANSPORT_NEW:
|
||||
break;
|
||||
case DTLS_TRANSPORT_CONNECTED:
|
||||
// Note: SignalWritableState fired by set_writable.
|
||||
// Do we also need set_receiving(ice_transport_->receiving()) here now, in
|
||||
// case we lose that signal before "DTLS" connects?
|
||||
// DtlsTransport::OnWritableState does not set_receiving in a similar
|
||||
// case, so leaving it out for the time being, but it would be good to
|
||||
// understand why.
|
||||
set_writable(ice_transport_->writable());
|
||||
break;
|
||||
case DTLS_TRANSPORT_CONNECTING:
|
||||
// Do nothing.
|
||||
break;
|
||||
case DTLS_TRANSPORT_FAILED:
|
||||
case DTLS_TRANSPORT_CLOSED:
|
||||
// Should not happen. Do nothing.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DatagramDtlsAdaptor::OnStateChanged(webrtc::MediaTransportState state) {
|
||||
// Convert MediaTransportState to DTLS state.
|
||||
switch (state) {
|
||||
case webrtc::MediaTransportState::kPending:
|
||||
set_dtls_state(DTLS_TRANSPORT_CONNECTING);
|
||||
break;
|
||||
|
||||
case webrtc::MediaTransportState::kWritable:
|
||||
// Since we do not set writable state until datagram transport is
|
||||
// connected, we need to call set_writable first.
|
||||
set_writable(ice_transport_->writable());
|
||||
set_dtls_state(DTLS_TRANSPORT_CONNECTED);
|
||||
break;
|
||||
|
||||
case webrtc::MediaTransportState::kClosed:
|
||||
set_dtls_state(DTLS_TRANSPORT_CLOSED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
DtlsTransportState DatagramDtlsAdaptor::dtls_state() const {
|
||||
return dtls_state_;
|
||||
}
|
||||
|
||||
const std::string& DatagramDtlsAdaptor::transport_name() const {
|
||||
return ice_transport_->transport_name();
|
||||
}
|
||||
|
||||
bool DatagramDtlsAdaptor::writable() const {
|
||||
// NOTE that even if ice is writable, writable_ maybe false, because we
|
||||
// propagte writable only after DTLS is connect (this is consistent with
|
||||
// implementation in dtls_transport.cc).
|
||||
return writable_;
|
||||
}
|
||||
|
||||
bool DatagramDtlsAdaptor::receiving() const {
|
||||
return receiving_;
|
||||
}
|
||||
|
||||
int DatagramDtlsAdaptor::SetOption(rtc::Socket::Option opt, int value) {
|
||||
return ice_transport_->SetOption(opt, value);
|
||||
}
|
||||
|
||||
int DatagramDtlsAdaptor::GetError() {
|
||||
return ice_transport_->GetError();
|
||||
}
|
||||
|
||||
void DatagramDtlsAdaptor::OnNetworkRouteChanged(
|
||||
absl::optional<rtc::NetworkRoute> network_route) {
|
||||
RTC_DCHECK_RUN_ON(&thread_checker_);
|
||||
SignalNetworkRouteChanged(network_route);
|
||||
}
|
||||
|
||||
void DatagramDtlsAdaptor::OnReceivingState(
|
||||
rtc::PacketTransportInternal* transport) {
|
||||
RTC_DCHECK_RUN_ON(&thread_checker_);
|
||||
RTC_DCHECK(transport == ice_transport_.get());
|
||||
RTC_LOG(LS_VERBOSE) << "ice_transport receiving state changed to "
|
||||
<< ice_transport_->receiving();
|
||||
|
||||
if (kBypassDatagramDtlsTestOnly || dtls_state() == DTLS_TRANSPORT_CONNECTED) {
|
||||
// Note: SignalReceivingState fired by set_receiving.
|
||||
set_receiving(ice_transport_->receiving());
|
||||
}
|
||||
}
|
||||
|
||||
void DatagramDtlsAdaptor::set_receiving(bool receiving) {
|
||||
if (receiving_ == receiving) {
|
||||
return;
|
||||
}
|
||||
receiving_ = receiving;
|
||||
SignalReceivingState(this);
|
||||
}
|
||||
|
||||
// Similar implementaton as in p2p/base/dtls_transport.cc.
|
||||
void DatagramDtlsAdaptor::set_writable(bool writable) {
|
||||
if (writable_ == writable) {
|
||||
return;
|
||||
}
|
||||
if (event_log_) {
|
||||
event_log_->Log(
|
||||
absl::make_unique<webrtc::RtcEventDtlsWritableState>(writable));
|
||||
}
|
||||
RTC_LOG(LS_VERBOSE) << "set_writable to: " << writable;
|
||||
writable_ = writable;
|
||||
if (writable_) {
|
||||
SignalReadyToSend(this);
|
||||
}
|
||||
SignalWritableState(this);
|
||||
}
|
||||
|
||||
// Similar implementaton as in p2p/base/dtls_transport.cc.
|
||||
void DatagramDtlsAdaptor::set_dtls_state(DtlsTransportState state) {
|
||||
if (dtls_state_ == state) {
|
||||
return;
|
||||
}
|
||||
if (event_log_) {
|
||||
event_log_->Log(absl::make_unique<webrtc::RtcEventDtlsTransportState>(
|
||||
ConvertDtlsTransportState(state)));
|
||||
}
|
||||
RTC_LOG(LS_VERBOSE) << "set_dtls_state from:" << dtls_state_ << " to "
|
||||
<< state;
|
||||
dtls_state_ = state;
|
||||
SignalDtlsState(this, state);
|
||||
}
|
||||
|
||||
} // namespace cricket
|
154
p2p/base/datagram_dtls_adaptor.h
Normal file
154
p2p/base/datagram_dtls_adaptor.h
Normal file
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright 2019 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
#ifndef P2P_BASE_DATAGRAM_DTLS_ADAPTOR_H_
|
||||
#define P2P_BASE_DATAGRAM_DTLS_ADAPTOR_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "api/crypto/crypto_options.h"
|
||||
#include "api/datagram_transport_interface.h"
|
||||
#include "p2p/base/dtls_transport_internal.h"
|
||||
#include "p2p/base/ice_transport_internal.h"
|
||||
#include "p2p/base/packet_transport_internal.h"
|
||||
#include "rtc_base/buffer.h"
|
||||
#include "rtc_base/buffer_queue.h"
|
||||
#include "rtc_base/constructor_magic.h"
|
||||
#include "rtc_base/ssl_stream_adapter.h"
|
||||
#include "rtc_base/stream.h"
|
||||
#include "rtc_base/strings/string_builder.h"
|
||||
#include "rtc_base/thread_checker.h"
|
||||
|
||||
namespace cricket {
|
||||
|
||||
constexpr int kDatagramDtlsAdaptorComponent = -1;
|
||||
|
||||
// DTLS wrapper around DatagramTransportInterface.
|
||||
// Does not encrypt.
|
||||
// Owns Datagram and Ice transports.
|
||||
class DatagramDtlsAdaptor : public DtlsTransportInternal,
|
||||
public webrtc::DatagramSinkInterface,
|
||||
public webrtc::MediaTransportStateCallback {
|
||||
public:
|
||||
// TODO(sukhanov): Taking crypto options, because DtlsTransportInternal
|
||||
// has a virtual getter crypto_options(). Consider removing getter and
|
||||
// removing crypto_options from DatagramDtlsAdaptor.
|
||||
DatagramDtlsAdaptor(
|
||||
std::unique_ptr<IceTransportInternal> ice_transport,
|
||||
std::unique_ptr<webrtc::DatagramTransportInterface> datagram_transport,
|
||||
const webrtc::CryptoOptions& crypto_options,
|
||||
webrtc::RtcEventLog* event_log);
|
||||
|
||||
~DatagramDtlsAdaptor() override;
|
||||
|
||||
// Connects to ICE transport callbacks.
|
||||
void ConnectToIceTransport();
|
||||
|
||||
// =====================================================
|
||||
// Overrides for webrtc::DatagramTransportSinkInterface
|
||||
// and MediaTransportStateCallback
|
||||
// =====================================================
|
||||
void OnDatagramReceived(rtc::ArrayView<const uint8_t> data) override;
|
||||
|
||||
void OnDatagramSent(webrtc::DatagramId datagram_id) override;
|
||||
|
||||
void OnStateChanged(webrtc::MediaTransportState state) override;
|
||||
|
||||
// =====================================================
|
||||
// DtlsTransportInternal overrides
|
||||
// =====================================================
|
||||
const webrtc::CryptoOptions& crypto_options() const override;
|
||||
DtlsTransportState dtls_state() const override;
|
||||
int component() const override;
|
||||
bool IsDtlsActive() const override;
|
||||
bool GetDtlsRole(rtc::SSLRole* role) const override;
|
||||
bool SetDtlsRole(rtc::SSLRole role) override;
|
||||
bool GetSrtpCryptoSuite(int* cipher) override;
|
||||
bool GetSslCipherSuite(int* cipher) override;
|
||||
rtc::scoped_refptr<rtc::RTCCertificate> GetLocalCertificate() const override;
|
||||
bool SetLocalCertificate(
|
||||
const rtc::scoped_refptr<rtc::RTCCertificate>& certificate) override;
|
||||
std::unique_ptr<rtc::SSLCertChain> GetRemoteSSLCertChain() const override;
|
||||
bool ExportKeyingMaterial(const std::string& label,
|
||||
const uint8_t* context,
|
||||
size_t context_len,
|
||||
bool use_context,
|
||||
uint8_t* result,
|
||||
size_t result_len) override;
|
||||
bool SetRemoteFingerprint(const std::string& digest_alg,
|
||||
const uint8_t* digest,
|
||||
size_t digest_len) override;
|
||||
bool SetSslMaxProtocolVersion(rtc::SSLProtocolVersion version) override;
|
||||
IceTransportInternal* ice_transport() override;
|
||||
webrtc::DatagramTransportInterface* datagram_transport() override;
|
||||
|
||||
const std::string& transport_name() const override;
|
||||
bool writable() const override;
|
||||
bool receiving() const override;
|
||||
|
||||
private:
|
||||
void set_receiving(bool receiving);
|
||||
void set_writable(bool writable);
|
||||
void set_dtls_state(DtlsTransportState state);
|
||||
|
||||
// Forwards incoming packet up the stack.
|
||||
void PropagateReadPacket(rtc::ArrayView<const uint8_t> data,
|
||||
const int64_t& packet_time_us);
|
||||
|
||||
// Signals SentPacket notification.
|
||||
void PropagateOnSentNotification(const rtc::SentPacket& sent_packet);
|
||||
|
||||
// Listens to read packet notifications from ICE (only used in bypass mode).
|
||||
void OnReadPacket(rtc::PacketTransportInternal* transport,
|
||||
const char* data,
|
||||
size_t size,
|
||||
const int64_t& packet_time_us,
|
||||
int flags);
|
||||
|
||||
void OnReadyToSend(rtc::PacketTransportInternal* transport);
|
||||
void OnWritableState(rtc::PacketTransportInternal* transport);
|
||||
void OnNetworkRouteChanged(absl::optional<rtc::NetworkRoute> network_route);
|
||||
void OnReceivingState(rtc::PacketTransportInternal* transport);
|
||||
|
||||
int SendPacket(const char* data,
|
||||
size_t len,
|
||||
const rtc::PacketOptions& options,
|
||||
int flags) override;
|
||||
int SetOption(rtc::Socket::Option opt, int value) override;
|
||||
int GetError() override;
|
||||
void OnSentPacket(rtc::PacketTransportInternal* transport,
|
||||
const rtc::SentPacket& sent_packet);
|
||||
|
||||
rtc::ThreadChecker thread_checker_;
|
||||
webrtc::CryptoOptions crypto_options_;
|
||||
std::unique_ptr<IceTransportInternal> ice_transport_;
|
||||
|
||||
std::unique_ptr<webrtc::DatagramTransportInterface> datagram_transport_;
|
||||
|
||||
// Current ICE writable state. Must be modified by calling set_ice_writable(),
|
||||
// which propagates change notifications.
|
||||
bool writable_ = false;
|
||||
|
||||
// Current receiving state. Must be modified by calling set_receiving(), which
|
||||
// propagates change notifications.
|
||||
bool receiving_ = false;
|
||||
|
||||
// Current DTLS state. Must be modified by calling set_dtls_state(), which
|
||||
// propagates change notifications.
|
||||
DtlsTransportState dtls_state_ = DTLS_TRANSPORT_NEW;
|
||||
|
||||
webrtc::RtcEventLog* const event_log_;
|
||||
};
|
||||
|
||||
} // namespace cricket
|
||||
|
||||
#endif // P2P_BASE_DATAGRAM_DTLS_ADAPTOR_H_
|
|
@ -764,24 +764,6 @@ void DtlsTransport::set_writable(bool writable) {
|
|||
SignalWritableState(this);
|
||||
}
|
||||
|
||||
static webrtc::DtlsTransportState ConvertDtlsTransportState(
|
||||
cricket::DtlsTransportState cricket_state) {
|
||||
switch (cricket_state) {
|
||||
case DtlsTransportState::DTLS_TRANSPORT_NEW:
|
||||
return webrtc::DtlsTransportState::kNew;
|
||||
case DtlsTransportState::DTLS_TRANSPORT_CONNECTING:
|
||||
return webrtc::DtlsTransportState::kConnecting;
|
||||
case DtlsTransportState::DTLS_TRANSPORT_CONNECTED:
|
||||
return webrtc::DtlsTransportState::kConnected;
|
||||
case DtlsTransportState::DTLS_TRANSPORT_CLOSED:
|
||||
return webrtc::DtlsTransportState::kClosed;
|
||||
case DtlsTransportState::DTLS_TRANSPORT_FAILED:
|
||||
return webrtc::DtlsTransportState::kFailed;
|
||||
}
|
||||
RTC_NOTREACHED();
|
||||
return webrtc::DtlsTransportState::kNew;
|
||||
}
|
||||
|
||||
void DtlsTransport::set_dtls_state(DtlsTransportState state) {
|
||||
if (dtls_state_ == state) {
|
||||
return;
|
||||
|
|
|
@ -16,4 +16,22 @@ DtlsTransportInternal::DtlsTransportInternal() = default;
|
|||
|
||||
DtlsTransportInternal::~DtlsTransportInternal() = default;
|
||||
|
||||
webrtc::DtlsTransportState ConvertDtlsTransportState(
|
||||
cricket::DtlsTransportState cricket_state) {
|
||||
switch (cricket_state) {
|
||||
case DtlsTransportState::DTLS_TRANSPORT_NEW:
|
||||
return webrtc::DtlsTransportState::kNew;
|
||||
case DtlsTransportState::DTLS_TRANSPORT_CONNECTING:
|
||||
return webrtc::DtlsTransportState::kConnecting;
|
||||
case DtlsTransportState::DTLS_TRANSPORT_CONNECTED:
|
||||
return webrtc::DtlsTransportState::kConnected;
|
||||
case DtlsTransportState::DTLS_TRANSPORT_CLOSED:
|
||||
return webrtc::DtlsTransportState::kClosed;
|
||||
case DtlsTransportState::DTLS_TRANSPORT_FAILED:
|
||||
return webrtc::DtlsTransportState::kFailed;
|
||||
}
|
||||
RTC_NOTREACHED();
|
||||
return webrtc::DtlsTransportState::kNew;
|
||||
}
|
||||
|
||||
} // namespace cricket
|
||||
|
|
|
@ -13,10 +13,13 @@
|
|||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "api/crypto/crypto_options.h"
|
||||
#include "api/datagram_transport_interface.h"
|
||||
#include "api/dtls_transport_interface.h"
|
||||
#include "api/scoped_refptr.h"
|
||||
#include "p2p/base/ice_transport_internal.h"
|
||||
#include "p2p/base/packet_transport_internal.h"
|
||||
|
@ -41,6 +44,9 @@ enum DtlsTransportState {
|
|||
DTLS_TRANSPORT_FAILED,
|
||||
};
|
||||
|
||||
webrtc::DtlsTransportState ConvertDtlsTransportState(
|
||||
cricket::DtlsTransportState cricket_state);
|
||||
|
||||
enum PacketFlags {
|
||||
PF_NORMAL = 0x00, // A normal packet.
|
||||
PF_SRTP_BYPASS = 0x01, // An encrypted SRTP packet; bypass any additional
|
||||
|
@ -59,6 +65,14 @@ class DtlsTransportInternal : public rtc::PacketTransportInternal {
|
|||
|
||||
virtual const webrtc::CryptoOptions& crypto_options() const = 0;
|
||||
|
||||
// Returns datagram transport or nullptr if not using datagram transport.
|
||||
// TODO(sukhanov): Make pure virtual.
|
||||
// TODO(sukhanov): Consider moving ownership of datagram transport and ICE
|
||||
// to JsepTransport.
|
||||
virtual webrtc::DatagramTransportInterface* datagram_transport() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
virtual DtlsTransportState dtls_state() const = 0;
|
||||
|
||||
virtual int component() const = 0;
|
||||
|
|
|
@ -116,6 +116,7 @@ JsepTransport::JsepTransport(
|
|||
: nullptr),
|
||||
media_transport_(std::move(media_transport)) {
|
||||
RTC_DCHECK(rtp_dtls_transport_);
|
||||
RTC_DCHECK(!datagram_transport() || !media_transport_);
|
||||
// Verify the "only one out of these three can be set" invariant.
|
||||
if (unencrypted_rtp_transport_) {
|
||||
RTC_DCHECK(!sdes_transport);
|
||||
|
@ -135,12 +136,13 @@ JsepTransport::JsepTransport(
|
|||
}
|
||||
|
||||
JsepTransport::~JsepTransport() {
|
||||
// Disconnect media transport state callbacks and make sure we delete media
|
||||
// transports before ICE.
|
||||
if (media_transport_) {
|
||||
media_transport_->SetMediaTransportStateCallback(nullptr);
|
||||
|
||||
// Make sure we delete media transport before ICE.
|
||||
media_transport_.reset();
|
||||
}
|
||||
|
||||
// Clear all DtlsTransports. There may be pointers to these from
|
||||
// other places, so we can't assume they'll be deleted by the destructor.
|
||||
rtp_dtls_transport_->Clear();
|
||||
|
@ -717,5 +719,4 @@ void JsepTransport::OnStateChanged(webrtc::MediaTransportState state) {
|
|||
}
|
||||
SignalMediaTransportStateChanged();
|
||||
}
|
||||
|
||||
} // namespace cricket
|
||||
|
|
|
@ -217,6 +217,12 @@ class JsepTransport : public sigslot::has_slots<>,
|
|||
return media_transport_.get();
|
||||
}
|
||||
|
||||
// Returns datagram transport, if available.
|
||||
webrtc::DatagramTransportInterface* datagram_transport() const {
|
||||
rtc::CritScope scope(&accessor_lock_);
|
||||
return rtp_dtls_transport_->internal()->datagram_transport();
|
||||
}
|
||||
|
||||
// Returns the latest media transport state.
|
||||
webrtc::MediaTransportState media_transport_state() const {
|
||||
rtc::CritScope scope(&accessor_lock_);
|
||||
|
@ -332,6 +338,10 @@ class JsepTransport : public sigslot::has_slots<>,
|
|||
|
||||
// If |media_transport_| is provided, this variable represents the state of
|
||||
// media transport.
|
||||
//
|
||||
// NOTE: datagram transport state is handled by DatagramDtlsAdaptor, because
|
||||
// DatagramDtlsAdaptor owns DatagramTransport. This state only represents
|
||||
// media transport.
|
||||
webrtc::MediaTransportState media_transport_state_
|
||||
RTC_GUARDED_BY(accessor_lock_) = webrtc::MediaTransportState::kPending;
|
||||
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
|
||||
#include "absl/algorithm/container.h"
|
||||
#include "absl/memory/memory.h"
|
||||
#include "api/datagram_transport_interface.h"
|
||||
#include "api/media_transport_interface.h"
|
||||
#include "p2p/base/datagram_dtls_adaptor.h"
|
||||
#include "p2p/base/ice_transport_internal.h"
|
||||
#include "p2p/base/no_op_dtls_transport.h"
|
||||
#include "p2p/base/port.h"
|
||||
|
@ -136,12 +139,42 @@ RtpTransportInternal* JsepTransportController::GetRtpTransport(
|
|||
return jsep_transport->rtp_transport();
|
||||
}
|
||||
|
||||
MediaTransportInterface* JsepTransportController::GetMediaTransport(
|
||||
MediaTransportConfig JsepTransportController::GetMediaTransportConfig(
|
||||
const std::string& mid) const {
|
||||
auto jsep_transport = GetJsepTransportForMid(mid);
|
||||
if (!jsep_transport) {
|
||||
return MediaTransportConfig();
|
||||
}
|
||||
|
||||
MediaTransportInterface* media_transport = nullptr;
|
||||
if (config_.use_media_transport_for_media) {
|
||||
media_transport = jsep_transport->media_transport();
|
||||
}
|
||||
|
||||
DatagramTransportInterface* datagram_transport =
|
||||
jsep_transport->datagram_transport();
|
||||
|
||||
// Media transport and datagram transports can not be used together.
|
||||
RTC_DCHECK(!media_transport || !datagram_transport);
|
||||
|
||||
if (media_transport) {
|
||||
return MediaTransportConfig(media_transport);
|
||||
} else if (datagram_transport) {
|
||||
return MediaTransportConfig(
|
||||
/*rtp_max_packet_size=*/datagram_transport->GetLargestDatagramSize());
|
||||
} else {
|
||||
return MediaTransportConfig();
|
||||
}
|
||||
}
|
||||
|
||||
MediaTransportInterface*
|
||||
JsepTransportController::GetMediaTransportForDataChannel(
|
||||
const std::string& mid) const {
|
||||
auto jsep_transport = GetJsepTransportForMid(mid);
|
||||
if (!jsep_transport || !config_.use_media_transport_for_data_channels) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return jsep_transport->media_transport();
|
||||
}
|
||||
|
||||
|
@ -403,7 +436,8 @@ void JsepTransportController::SetActiveResetSrtpParams(
|
|||
|
||||
void JsepTransportController::SetMediaTransportSettings(
|
||||
bool use_media_transport_for_media,
|
||||
bool use_media_transport_for_data_channels) {
|
||||
bool use_media_transport_for_data_channels,
|
||||
bool use_datagram_transport) {
|
||||
RTC_DCHECK(use_media_transport_for_media ==
|
||||
config_.use_media_transport_for_media ||
|
||||
jsep_transports_by_name_.empty())
|
||||
|
@ -419,6 +453,7 @@ void JsepTransportController::SetMediaTransportSettings(
|
|||
config_.use_media_transport_for_media = use_media_transport_for_media;
|
||||
config_.use_media_transport_for_data_channels =
|
||||
use_media_transport_for_data_channels;
|
||||
config_.use_datagram_transport = use_datagram_transport;
|
||||
}
|
||||
|
||||
std::unique_ptr<cricket::IceTransportInternal>
|
||||
|
@ -439,16 +474,25 @@ JsepTransportController::CreateIceTransport(const std::string transport_name,
|
|||
|
||||
std::unique_ptr<cricket::DtlsTransportInternal>
|
||||
JsepTransportController::CreateDtlsTransport(
|
||||
std::unique_ptr<cricket::IceTransportInternal> ice) {
|
||||
std::unique_ptr<cricket::IceTransportInternal> ice,
|
||||
std::unique_ptr<DatagramTransportInterface> datagram_transport) {
|
||||
RTC_DCHECK(network_thread_->IsCurrent());
|
||||
|
||||
std::unique_ptr<cricket::DtlsTransportInternal> dtls;
|
||||
|
||||
if (datagram_transport) {
|
||||
RTC_DCHECK(config_.use_datagram_transport);
|
||||
|
||||
// Create DTLS wrapper around DatagramTransportInterface.
|
||||
dtls = absl::make_unique<cricket::DatagramDtlsAdaptor>(
|
||||
std::move(ice), std::move(datagram_transport), config_.crypto_options,
|
||||
config_.event_log);
|
||||
} else if (config_.media_transport_factory &&
|
||||
config_.use_media_transport_for_media &&
|
||||
config_.use_media_transport_for_data_channels) {
|
||||
// If media transport is used for both media and data channels,
|
||||
// then we don't need to create DTLS.
|
||||
// Otherwise, DTLS is still created.
|
||||
if (config_.media_transport_factory &&
|
||||
config_.use_media_transport_for_media &&
|
||||
config_.use_media_transport_for_data_channels) {
|
||||
dtls = absl::make_unique<cricket::NoOpDtlsTransport>(
|
||||
std::move(ice), config_.crypto_options);
|
||||
} else if (config_.external_transport_factory) {
|
||||
|
@ -1024,6 +1068,72 @@ JsepTransportController::MaybeCreateMediaTransport(
|
|||
return media_transport_result.MoveValue();
|
||||
}
|
||||
|
||||
// TODO(sukhanov): Refactor to avoid code duplication for Media and Datagram
|
||||
// transports setup.
|
||||
std::unique_ptr<webrtc::DatagramTransportInterface>
|
||||
JsepTransportController::MaybeCreateDatagramTransport(
|
||||
const cricket::ContentInfo& content_info,
|
||||
const cricket::SessionDescription& description,
|
||||
bool local) {
|
||||
if (config_.media_transport_factory == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!config_.use_datagram_transport) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Caller (offerer) datagram transport.
|
||||
if (local) {
|
||||
if (offer_datagram_transport_) {
|
||||
RTC_LOG(LS_INFO) << "Offered datagram transport has now been activated.";
|
||||
return std::move(offer_datagram_transport_);
|
||||
} else {
|
||||
RTC_LOG(LS_INFO)
|
||||
<< "Not returning datagram transport. Either SDES wasn't enabled, or "
|
||||
"datagram transport didn't return an offer earlier.";
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Remote offer. If no x-mt lines, do not create datagram transport.
|
||||
if (description.MediaTransportSettings().empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// When bundle is enabled, two JsepTransports are created, and then
|
||||
// the second transport is destroyed (right away).
|
||||
// For datagram transport, we don't want to create the second
|
||||
// datagram transport in the first place.
|
||||
RTC_LOG(LS_INFO) << "Returning new, client datagram transport.";
|
||||
|
||||
RTC_DCHECK(!local)
|
||||
<< "If datagram transport is used, you must call "
|
||||
"GenerateOrGetLastMediaTransportOffer before SetLocalDescription. You "
|
||||
"also must use kRtcpMuxPolicyRequire and kBundlePolicyMaxBundle with "
|
||||
"datagram transport.";
|
||||
MediaTransportSettings settings;
|
||||
settings.is_caller = local;
|
||||
settings.event_log = config_.event_log;
|
||||
|
||||
// Assume there is only one media transport (or if more, use the first one).
|
||||
if (!local && !description.MediaTransportSettings().empty() &&
|
||||
config_.media_transport_factory->GetTransportName() ==
|
||||
description.MediaTransportSettings()[0].transport_name) {
|
||||
settings.remote_transport_parameters =
|
||||
description.MediaTransportSettings()[0].transport_setting;
|
||||
}
|
||||
|
||||
auto datagram_transport_result =
|
||||
config_.media_transport_factory->CreateDatagramTransport(network_thread_,
|
||||
settings);
|
||||
|
||||
// TODO(sukhanov): Proper error handling.
|
||||
RTC_CHECK(datagram_transport_result.ok());
|
||||
|
||||
return datagram_transport_result.MoveValue();
|
||||
}
|
||||
|
||||
RTCError JsepTransportController::MaybeCreateJsepTransport(
|
||||
bool local,
|
||||
const cricket::ContentInfo& content_info,
|
||||
|
@ -1052,8 +1162,15 @@ RTCError JsepTransportController::MaybeCreateJsepTransport(
|
|||
media_transport->Connect(ice.get());
|
||||
}
|
||||
|
||||
std::unique_ptr<DatagramTransportInterface> datagram_transport =
|
||||
MaybeCreateDatagramTransport(content_info, description, local);
|
||||
if (datagram_transport) {
|
||||
datagram_transport_created_once_ = true;
|
||||
datagram_transport->Connect(ice.get());
|
||||
}
|
||||
|
||||
std::unique_ptr<cricket::DtlsTransportInternal> rtp_dtls_transport =
|
||||
CreateDtlsTransport(std::move(ice));
|
||||
CreateDtlsTransport(std::move(ice), std::move(datagram_transport));
|
||||
|
||||
std::unique_ptr<cricket::DtlsTransportInternal> rtcp_dtls_transport;
|
||||
std::unique_ptr<RtpTransport> unencrypted_rtp_transport;
|
||||
|
@ -1064,19 +1181,36 @@ RTCError JsepTransportController::MaybeCreateJsepTransport(
|
|||
PeerConnectionInterface::kRtcpMuxPolicyRequire &&
|
||||
content_info.type == cricket::MediaProtocolType::kRtp) {
|
||||
RTC_DCHECK(media_transport == nullptr);
|
||||
RTC_DCHECK(datagram_transport == nullptr);
|
||||
rtcp_dtls_transport = CreateDtlsTransport(
|
||||
CreateIceTransport(content_info.name, /*rtcp=*/true));
|
||||
CreateIceTransport(content_info.name, /*rtcp=*/true),
|
||||
/*datagram_transport=*/nullptr);
|
||||
}
|
||||
|
||||
// TODO(sukhanov): Do not create RTP/RTCP transports if media transport is
|
||||
// used, and remove the no-op dtls transport when that's done.
|
||||
if (config_.disable_encryption) {
|
||||
if (datagram_transport) {
|
||||
// TODO(sukhanov): We use unencrypted RTP transport over DatagramTransport,
|
||||
// because MediaTransport encrypts. In the future we may want to
|
||||
// implement our own version of RtpTransport over MediaTransport, because
|
||||
// it will give us more control over things like:
|
||||
// - Fusing
|
||||
// - Rtp header compression
|
||||
// - Handling Rtcp feedback.
|
||||
RTC_LOG(LS_INFO) << "Creating UnencryptedRtpTransport, because datagram "
|
||||
"transport is used.";
|
||||
RTC_DCHECK(!rtcp_dtls_transport);
|
||||
unencrypted_rtp_transport = CreateUnencryptedRtpTransport(
|
||||
content_info.name, rtp_dtls_transport.get(), rtcp_dtls_transport.get());
|
||||
} else if (config_.disable_encryption) {
|
||||
RTC_LOG(LS_INFO)
|
||||
<< "Creating UnencryptedRtpTransport, becayse encryption is disabled.";
|
||||
unencrypted_rtp_transport = CreateUnencryptedRtpTransport(
|
||||
content_info.name, rtp_dtls_transport.get(), rtcp_dtls_transport.get());
|
||||
} else if (!content_desc->cryptos().empty()) {
|
||||
sdes_transport = CreateSdesTransport(
|
||||
content_info.name, rtp_dtls_transport.get(), rtcp_dtls_transport.get());
|
||||
RTC_LOG(LS_INFO) << "Creating SdesTransport.";
|
||||
} else {
|
||||
RTC_LOG(LS_INFO) << "Creating DtlsSrtpTransport.";
|
||||
dtls_srtp_transport = CreateDtlsSrtpTransport(
|
||||
content_info.name, rtp_dtls_transport.get(), rtcp_dtls_transport.get());
|
||||
}
|
||||
|
@ -1087,6 +1221,7 @@ RTCError JsepTransportController::MaybeCreateJsepTransport(
|
|||
std::move(sdes_transport), std::move(dtls_srtp_transport),
|
||||
std::move(rtp_dtls_transport), std::move(rtcp_dtls_transport),
|
||||
std::move(media_transport));
|
||||
|
||||
jsep_transport->SignalRtcpMuxActive.connect(
|
||||
this, &JsepTransportController::UpdateAggregateStates_n);
|
||||
jsep_transport->SignalMediaTransportStateChanged.connect(
|
||||
|
@ -1508,20 +1643,25 @@ void JsepTransportController::OnDtlsHandshakeError(
|
|||
|
||||
absl::optional<cricket::SessionDescription::MediaTransportSetting>
|
||||
JsepTransportController::GenerateOrGetLastMediaTransportOffer() {
|
||||
if (media_transport_created_once_) {
|
||||
if (media_transport_created_once_ || datagram_transport_created_once_) {
|
||||
RTC_LOG(LS_INFO) << "Not regenerating media transport for the new offer in "
|
||||
"existing session.";
|
||||
return media_transport_offer_settings_;
|
||||
}
|
||||
|
||||
RTC_LOG(LS_INFO) << "Generating media transport offer!";
|
||||
|
||||
absl::optional<std::string> transport_parameters;
|
||||
|
||||
// Check that media transport is supposed to be used.
|
||||
// Note that ICE is not available when media transport is created. It will
|
||||
// only be available in 'Connect'. This may be a potential server config, if
|
||||
// we decide to use this peer connection as a caller, not as a callee.
|
||||
// TODO(sukhanov): Avoid code duplication with CreateMedia/MediaTransport.
|
||||
if (config_.use_media_transport_for_media ||
|
||||
config_.use_media_transport_for_data_channels) {
|
||||
RTC_DCHECK(config_.media_transport_factory != nullptr);
|
||||
// ICE is not available when media transport is created. It will only be
|
||||
// available in 'Connect'. This may be a potential server config, if we
|
||||
// decide to use this peer connection as a caller, not as a callee.
|
||||
RTC_DCHECK(!config_.use_datagram_transport);
|
||||
webrtc::MediaTransportSettings settings;
|
||||
settings.is_caller = true;
|
||||
settings.pre_shared_key = rtc::CreateRandomString(32);
|
||||
|
@ -1532,19 +1672,37 @@ JsepTransportController::GenerateOrGetLastMediaTransportOffer() {
|
|||
|
||||
if (media_transport_or_error.ok()) {
|
||||
offer_media_transport_ = std::move(media_transport_or_error.value());
|
||||
transport_parameters =
|
||||
offer_media_transport_->GetTransportParametersOffer();
|
||||
} else {
|
||||
RTC_LOG(LS_INFO) << "Unable to create media transport, error="
|
||||
<< media_transport_or_error.error().message();
|
||||
}
|
||||
} else if (config_.use_datagram_transport) {
|
||||
webrtc::MediaTransportSettings settings;
|
||||
settings.is_caller = true;
|
||||
settings.pre_shared_key = rtc::CreateRandomString(32);
|
||||
settings.event_log = config_.event_log;
|
||||
auto datagram_transport_or_error =
|
||||
config_.media_transport_factory->CreateDatagramTransport(
|
||||
network_thread_, settings);
|
||||
|
||||
if (datagram_transport_or_error.ok()) {
|
||||
offer_datagram_transport_ =
|
||||
std::move(datagram_transport_or_error.value());
|
||||
transport_parameters =
|
||||
offer_datagram_transport_->GetTransportParametersOffer();
|
||||
} else {
|
||||
RTC_LOG(LS_INFO) << "Unable to create media transport, error="
|
||||
<< datagram_transport_or_error.error().message();
|
||||
}
|
||||
}
|
||||
|
||||
if (!offer_media_transport_) {
|
||||
RTC_LOG(LS_INFO) << "Media transport doesn't exist";
|
||||
if (!offer_media_transport_ && !offer_datagram_transport_) {
|
||||
RTC_LOG(LS_INFO) << "Media and data transports do not exist";
|
||||
return absl::nullopt;
|
||||
}
|
||||
|
||||
absl::optional<std::string> transport_parameters =
|
||||
offer_media_transport_->GetTransportParametersOffer();
|
||||
if (!transport_parameters) {
|
||||
RTC_LOG(LS_INFO) << "Media transport didn't generate the offer";
|
||||
// Media transport didn't generate the offer, and is not supposed to be
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
#include "api/candidate.h"
|
||||
#include "api/crypto/crypto_options.h"
|
||||
#include "api/media_transport_config.h"
|
||||
#include "api/media_transport_interface.h"
|
||||
#include "api/peer_connection_interface.h"
|
||||
#include "logging/rtc_event_log/rtc_event_log.h"
|
||||
|
@ -93,6 +94,9 @@ class JsepTransportController : public sigslot::has_slots<> {
|
|||
// MediaTransportFactory is provided.
|
||||
bool use_rtp_media_transport = false;
|
||||
|
||||
// Use encrypted datagram transport to send packets.
|
||||
bool use_datagram_transport = false;
|
||||
|
||||
// Optional media transport factory (experimental). If provided it will be
|
||||
// used to create media_transport (as long as either
|
||||
// |use_media_transport_for_media| or
|
||||
|
@ -133,7 +137,16 @@ class JsepTransportController : public sigslot::has_slots<> {
|
|||
rtc::scoped_refptr<webrtc::DtlsTransport> LookupDtlsTransportByMid(
|
||||
const std::string& mid);
|
||||
|
||||
MediaTransportInterface* GetMediaTransport(const std::string& mid) const;
|
||||
MediaTransportConfig GetMediaTransportConfig(const std::string& mid) const;
|
||||
|
||||
MediaTransportInterface* GetMediaTransportForDataChannel(
|
||||
const std::string& mid) const;
|
||||
|
||||
// TODO(sukhanov): Deprecate, return only config.
|
||||
MediaTransportInterface* GetMediaTransport(const std::string& mid) const {
|
||||
return GetMediaTransportConfig(mid).media_transport;
|
||||
}
|
||||
|
||||
MediaTransportState GetMediaTransportState(const std::string& mid) const;
|
||||
|
||||
/*********************
|
||||
|
@ -190,7 +203,8 @@ class JsepTransportController : public sigslot::has_slots<> {
|
|||
// you did not call 'GetMediaTransport' or 'MaybeCreateJsepTransport'. Once
|
||||
// Jsep transport is created, you can't change this setting.
|
||||
void SetMediaTransportSettings(bool use_media_transport_for_media,
|
||||
bool use_media_transport_for_data_channels);
|
||||
bool use_media_transport_for_data_channels,
|
||||
bool use_datagram_transport);
|
||||
|
||||
// If media transport is present enabled and supported,
|
||||
// when this method is called, it creates a media transport and generates its
|
||||
|
@ -308,6 +322,17 @@ class JsepTransportController : public sigslot::has_slots<> {
|
|||
const cricket::ContentInfo& content_info,
|
||||
const cricket::SessionDescription& description,
|
||||
bool local);
|
||||
|
||||
// Creates datagram transport if config wants to use it, and a=x-mt line is
|
||||
// present for the current media transport. Returned
|
||||
// DatagramTransportInterface is not connected, and must be connected to ICE.
|
||||
// You must call |GenerateOrGetLastMediaTransportOffer| on the caller before
|
||||
// calling MaybeCreateDatagramTransport.
|
||||
std::unique_ptr<webrtc::DatagramTransportInterface>
|
||||
MaybeCreateDatagramTransport(const cricket::ContentInfo& content_info,
|
||||
const cricket::SessionDescription& description,
|
||||
bool local);
|
||||
|
||||
void MaybeDestroyJsepTransport(const std::string& mid);
|
||||
void DestroyAllJsepTransports_n();
|
||||
|
||||
|
@ -320,7 +345,8 @@ class JsepTransportController : public sigslot::has_slots<> {
|
|||
bool local);
|
||||
|
||||
std::unique_ptr<cricket::DtlsTransportInternal> CreateDtlsTransport(
|
||||
std::unique_ptr<cricket::IceTransportInternal> ice);
|
||||
std::unique_ptr<cricket::IceTransportInternal> ice,
|
||||
std::unique_ptr<DatagramTransportInterface> datagram_transport);
|
||||
std::unique_ptr<cricket::IceTransportInternal> CreateIceTransport(
|
||||
const std::string transport_name,
|
||||
bool rtcp);
|
||||
|
@ -399,6 +425,22 @@ class JsepTransportController : public sigslot::has_slots<> {
|
|||
absl::optional<cricket::SessionDescription::MediaTransportSetting>
|
||||
media_transport_offer_settings_;
|
||||
|
||||
// Early on in the call we don't know if datagram transport is going to be
|
||||
// used, but we need to get the server-supported parameters to add to an SDP.
|
||||
// This server datagram transport will be promoted to the used datagram
|
||||
// transport after the local description is set, and the ownership will be
|
||||
// transferred to the actual JsepTransport. This "offer" datagram transport is
|
||||
// not created if it's done on the party that provides answer. This offer
|
||||
// datagram transport is only created once at the beginning of the connection,
|
||||
// and never again.
|
||||
std::unique_ptr<DatagramTransportInterface> offer_datagram_transport_ =
|
||||
nullptr;
|
||||
|
||||
// Contains the offer of the |offer_datagram_transport_|, in case if it needs
|
||||
// to be repeated.
|
||||
absl::optional<cricket::SessionDescription::MediaTransportSetting>
|
||||
datagram_transport_offer_settings_;
|
||||
|
||||
// When the new offer is regenerated (due to upgrade), we don't want to
|
||||
// re-create media transport. New streams might be created; but media
|
||||
// transport stays the same. This flag prevents re-creation of the transport
|
||||
|
@ -411,6 +453,7 @@ class JsepTransportController : public sigslot::has_slots<> {
|
|||
// recreate the Offer (e.g. after adding streams in Plan B), and so we want to
|
||||
// prevent recreation of the media transport when that happens.
|
||||
bool media_transport_created_once_ = false;
|
||||
bool datagram_transport_created_once_ = false;
|
||||
|
||||
const cricket::SessionDescription* local_desc_ = nullptr;
|
||||
const cricket::SessionDescription* remote_desc_ = nullptr;
|
||||
|
|
|
@ -442,7 +442,7 @@ TEST_F(JsepTransportControllerTest,
|
|||
.ok());
|
||||
|
||||
FakeMediaTransport* media_transport = static_cast<FakeMediaTransport*>(
|
||||
transport_controller_->GetMediaTransport(kAudioMid1));
|
||||
transport_controller_->GetMediaTransportForDataChannel(kAudioMid1));
|
||||
|
||||
ASSERT_NE(nullptr, media_transport);
|
||||
|
||||
|
@ -451,7 +451,8 @@ TEST_F(JsepTransportControllerTest,
|
|||
EXPECT_TRUE(media_transport->pre_shared_key().has_value());
|
||||
|
||||
// Return nullptr for non-existing mids.
|
||||
EXPECT_EQ(nullptr, transport_controller_->GetMediaTransport(kVideoMid2));
|
||||
EXPECT_EQ(nullptr,
|
||||
transport_controller_->GetMediaTransportForDataChannel(kVideoMid2));
|
||||
|
||||
EXPECT_EQ(cricket::ICE_CANDIDATE_COMPONENT_RTP,
|
||||
transport_controller_->GetDtlsTransport(kAudioMid1)->component())
|
||||
|
@ -563,8 +564,6 @@ TEST_F(JsepTransportControllerTest, GetMediaTransportInCallee) {
|
|||
EXPECT_EQ(absl::nullopt, media_transport->settings().pre_shared_key);
|
||||
EXPECT_TRUE(media_transport->is_connected());
|
||||
|
||||
EXPECT_EQ("fake-remote-settings",
|
||||
media_transport->remote_transport_parameters());
|
||||
// Return nullptr for non-existing mids.
|
||||
EXPECT_EQ(nullptr, transport_controller_->GetMediaTransport(kVideoMid2));
|
||||
|
||||
|
|
|
@ -764,6 +764,7 @@ bool PeerConnectionInterface::RTCConfiguration::operator==(
|
|||
bool active_reset_srtp_params;
|
||||
bool use_media_transport;
|
||||
bool use_media_transport_for_data_channels;
|
||||
bool use_datagram_transport;
|
||||
absl::optional<CryptoOptions> crypto_options;
|
||||
bool offer_extmap_allow_mixed;
|
||||
};
|
||||
|
@ -822,6 +823,7 @@ bool PeerConnectionInterface::RTCConfiguration::operator==(
|
|||
use_media_transport == o.use_media_transport &&
|
||||
use_media_transport_for_data_channels ==
|
||||
o.use_media_transport_for_data_channels &&
|
||||
use_datagram_transport == o.use_datagram_transport &&
|
||||
crypto_options == o.crypto_options &&
|
||||
offer_extmap_allow_mixed == o.offer_extmap_allow_mixed;
|
||||
}
|
||||
|
@ -1021,7 +1023,8 @@ bool PeerConnection::Initialize(
|
|||
#endif
|
||||
config.active_reset_srtp_params = configuration.active_reset_srtp_params;
|
||||
|
||||
if (configuration.use_media_transport ||
|
||||
if (configuration.use_datagram_transport ||
|
||||
configuration.use_media_transport ||
|
||||
configuration.use_media_transport_for_data_channels) {
|
||||
if (!factory_->media_transport_factory()) {
|
||||
RTC_DCHECK(false)
|
||||
|
@ -1051,6 +1054,7 @@ bool PeerConnection::Initialize(
|
|||
config.use_media_transport_for_media = configuration.use_media_transport;
|
||||
config.use_media_transport_for_data_channels =
|
||||
configuration.use_media_transport_for_data_channels;
|
||||
config.use_datagram_transport = configuration.use_datagram_transport;
|
||||
config.media_transport_factory = factory_->media_transport_factory();
|
||||
}
|
||||
|
||||
|
@ -3412,8 +3416,23 @@ bool PeerConnection::SetConfiguration(const RTCConfiguration& configuration,
|
|||
return SafeSetError(RTCErrorType::INVALID_MODIFICATION, error);
|
||||
}
|
||||
|
||||
if (local_description() && configuration.use_datagram_transport !=
|
||||
configuration_.use_datagram_transport) {
|
||||
RTC_LOG(LS_ERROR) << "Can't change use_datagram_transport "
|
||||
"after calling SetLocalDescription.";
|
||||
return SafeSetError(RTCErrorType::INVALID_MODIFICATION, error);
|
||||
}
|
||||
|
||||
if (remote_description() && configuration.use_datagram_transport !=
|
||||
configuration_.use_datagram_transport) {
|
||||
RTC_LOG(LS_ERROR) << "Can't change use_datagram_transport "
|
||||
"after calling SetRemoteDescription.";
|
||||
return SafeSetError(RTCErrorType::INVALID_MODIFICATION, error);
|
||||
}
|
||||
|
||||
if (configuration.use_media_transport_for_data_channels ||
|
||||
configuration.use_media_transport) {
|
||||
configuration.use_media_transport ||
|
||||
configuration.use_datagram_transport) {
|
||||
RTC_CHECK(configuration.bundle_policy == kBundlePolicyMaxBundle)
|
||||
<< "Media transport requires MaxBundle policy.";
|
||||
}
|
||||
|
@ -3506,7 +3525,8 @@ bool PeerConnection::SetConfiguration(const RTCConfiguration& configuration,
|
|||
transport_controller_->SetIceConfig(ParseIceConfig(modified_config));
|
||||
transport_controller_->SetMediaTransportSettings(
|
||||
modified_config.use_media_transport,
|
||||
modified_config.use_media_transport_for_data_channels);
|
||||
modified_config.use_media_transport_for_data_channels,
|
||||
modified_config.use_datagram_transport);
|
||||
|
||||
if (configuration_.active_reset_srtp_params !=
|
||||
modified_config.active_reset_srtp_params) {
|
||||
|
@ -6317,15 +6337,13 @@ RTCError PeerConnection::CreateChannels(const SessionDescription& desc) {
|
|||
cricket::VoiceChannel* PeerConnection::CreateVoiceChannel(
|
||||
const std::string& mid) {
|
||||
RtpTransportInternal* rtp_transport = GetRtpTransport(mid);
|
||||
MediaTransportInterface* media_transport = nullptr;
|
||||
if (configuration_.use_media_transport) {
|
||||
media_transport = GetMediaTransport(mid);
|
||||
}
|
||||
MediaTransportConfig media_transport_config =
|
||||
transport_controller_->GetMediaTransportConfig(mid);
|
||||
|
||||
cricket::VoiceChannel* voice_channel = channel_manager()->CreateVoiceChannel(
|
||||
call_ptr_, configuration_.media_config, rtp_transport,
|
||||
MediaTransportConfig(media_transport), signaling_thread(), mid,
|
||||
SrtpRequired(), GetCryptoOptions(), &ssrc_generator_, audio_options_);
|
||||
media_transport_config, signaling_thread(), mid, SrtpRequired(),
|
||||
GetCryptoOptions(), &ssrc_generator_, audio_options_);
|
||||
if (!voice_channel) {
|
||||
return nullptr;
|
||||
}
|
||||
|
@ -6342,15 +6360,13 @@ cricket::VoiceChannel* PeerConnection::CreateVoiceChannel(
|
|||
cricket::VideoChannel* PeerConnection::CreateVideoChannel(
|
||||
const std::string& mid) {
|
||||
RtpTransportInternal* rtp_transport = GetRtpTransport(mid);
|
||||
MediaTransportInterface* media_transport = nullptr;
|
||||
if (configuration_.use_media_transport) {
|
||||
media_transport = GetMediaTransport(mid);
|
||||
}
|
||||
MediaTransportConfig media_transport_config =
|
||||
transport_controller_->GetMediaTransportConfig(mid);
|
||||
|
||||
cricket::VideoChannel* video_channel = channel_manager()->CreateVideoChannel(
|
||||
call_ptr_, configuration_.media_config, rtp_transport,
|
||||
MediaTransportConfig(media_transport), signaling_thread(), mid,
|
||||
SrtpRequired(), GetCryptoOptions(), &ssrc_generator_, video_options_,
|
||||
media_transport_config, signaling_thread(), mid, SrtpRequired(),
|
||||
GetCryptoOptions(), &ssrc_generator_, video_options_,
|
||||
video_bitrate_allocator_factory_.get());
|
||||
if (!video_channel) {
|
||||
return nullptr;
|
||||
|
@ -6529,7 +6545,8 @@ void PeerConnection::OnSctpClosingProcedureComplete_n(int sid) {
|
|||
|
||||
bool PeerConnection::SetupMediaTransportForDataChannels_n(
|
||||
const std::string& mid) {
|
||||
media_transport_ = transport_controller_->GetMediaTransport(mid);
|
||||
media_transport_ =
|
||||
transport_controller_->GetMediaTransportForDataChannel(mid);
|
||||
if (!media_transport_) {
|
||||
RTC_LOG(LS_ERROR)
|
||||
<< "Media transport is not available for data channels, mid=" << mid;
|
||||
|
@ -6886,8 +6903,9 @@ bool PeerConnection::ReadyToUseRemoteCandidate(
|
|||
}
|
||||
|
||||
bool PeerConnection::SrtpRequired() const {
|
||||
return dtls_enabled_ ||
|
||||
webrtc_session_desc_factory_->SdesPolicy() == cricket::SEC_REQUIRED;
|
||||
return !configuration_.use_datagram_transport &&
|
||||
(dtls_enabled_ ||
|
||||
webrtc_session_desc_factory_->SdesPolicy() == cricket::SEC_REQUIRED);
|
||||
}
|
||||
|
||||
void PeerConnection::OnTransportControllerGatheringState(
|
||||
|
|
|
@ -1079,22 +1079,6 @@ class PeerConnection : public PeerConnectionInternal,
|
|||
return rtp_transport;
|
||||
}
|
||||
|
||||
// Returns media transport, if PeerConnection was created with configuration
|
||||
// to use media transport. Otherwise returns nullptr.
|
||||
MediaTransportInterface* GetMediaTransport(const std::string& mid)
|
||||
RTC_RUN_ON(signaling_thread()) {
|
||||
auto media_transport = transport_controller_->GetMediaTransport(mid);
|
||||
RTC_DCHECK((configuration_.use_media_transport ||
|
||||
configuration_.use_media_transport_for_data_channels) ==
|
||||
(media_transport != nullptr))
|
||||
<< "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);
|
||||
return media_transport;
|
||||
}
|
||||
|
||||
void UpdateNegotiationNeeded();
|
||||
bool CheckIfNegotiationIsNeeded();
|
||||
|
||||
|
|
Loading…
Reference in a new issue