// This function is only expected to be called on the signaling thread. // On the other hand, some test or even production setups may use // several signaling threads. int GenerateUniqueId() { static std::atomic<int> g_unique_id{0};
return ++g_unique_id;
}
// Returns true if a "per-sender" encoding parameter contains a value that isn't // its default. Currently max_bitrate_bps and bitrate_priority both are // implemented "per-sender," meaning that these encoding parameters // are used for the RtpSender as a whole, not for a specific encoding layer. // This is done by setting these encoding parameters at index 0 of // RtpParameters.encodings. This function can be used to check if these // parameters are set at any index other than 0 of RtpParameters.encodings, // because they are currently unimplemented to be used for a specific encoding // layer. bool PerSenderRtpEncodingParameterHasValue( const RtpEncodingParameters& encoding_params) { if (encoding_params.bitrate_priority != kDefaultBitratePriority ||
encoding_params.network_priority != Priority::kLow) { returntrue;
} returnfalse;
}
// Checks that the codec parameters are valid.
RTCError CheckCodecParameters(const RtpParameters& parameters, const std::vector<Codec>& send_codecs, const std::optional<Codec>& send_codec) { // Match the currently used codec against the codec preferences to gather // the SVC capabilities.
std::optional<Codec> send_codec_with_svc_info; if (send_codec && send_codec->type == Codec::Type::kVideo) { auto codec_match = absl::c_find_if(
send_codecs, [&](auto& codec) { return send_codec->Matches(codec); }); if (codec_match != send_codecs.end()) {
send_codec_with_svc_info = *codec_match;
}
}
// Logic that runs on the worker thread to set the parameters. // Returns an error if the parameters check failed or if the set failed. void SetRtpParametersOnWorkerThread(
MediaSendChannelInterface* media_channel, const std::vector<Codec>& send_codecs, const std::vector<std::string>& disabled_rids, const Environment& env,
uint32_t ssrc,
RtpParameters parameters,
SetParametersCallback callback) {
RTC_DCHECK(media_channel);
RtpParameters old_parameters = media_channel->GetRtpSendParameters(ssrc); // Add the inactive layers if disabled_rids isn't empty.
RtpParameters rtp_parameters =
disabled_rids.empty() ? parameters
: RestoreEncodingLayers(parameters, disabled_rids,
old_parameters.encodings);
RTCError result = CheckRtpParametersInvalidModificationAndValues(
old_parameters, rtp_parameters, env.field_trials()); if (!result.ok()) {
std::move(callback)(std::move(result)); return;
}
result = CheckCodecParameters(rtp_parameters, send_codecs,
media_channel->GetSendCodec()); if (!result.ok()) {
std::move(callback)(std::move(result)); return;
}
// Returns true if any RtpParameters member that isn't implemented contains a // value. bool UnimplementedRtpParameterHasValue(const RtpParameters& parameters) { if (!parameters.mid.empty()) { returntrue;
} for (size_t i = 0; i < parameters.encodings.size(); ++i) { // Encoding parameters that are per-sender should only contain value at // index 0. if (i != 0 &&
PerSenderRtpEncodingParameterHasValue(parameters.encodings[i])) { returntrue;
}
} returnfalse;
}
RtpSenderBase::RtpSenderBase( const Environment& env,
Thread* signaling_thread,
Thread* worker_thread,
absl::string_view id,
MediaType media_type,
SetStreamsObserver* set_streams_observer,
absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
MediaSendChannelInterface* media_channel)
: env_(env),
signaling_thread_(signaling_thread),
worker_thread_(worker_thread),
id_(id),
media_type_(media_type),
media_channel_(nullptr), // Will be set in SetMediaChannel().
set_streams_observer_(set_streams_observer),
worker_safety_(PendingTaskSafetyFlag::CreateAttachedToTaskQueue( /*alive=*/media_channel != nullptr,
worker_thread_)),
signaling_safety_(
PendingTaskSafetyFlag::CreateAttachedToTaskQueue(/*alive=*/true,
signaling_thread_)),
enable_sframe_at_owner_(std::move(enable_sframe_at_owner)) {
RTC_DCHECK(worker_thread_);
init_parameters_.encodings.emplace_back(); if (media_channel) { // When initialized with a valid media channel, we need to be running on the // worker thread in order to set things up properly.
RTC_DCHECK_RUN_ON(worker_thread_);
SetMediaChannel(media_channel);
} else { // Otherwise, we're less picky (but probably running on the signaling // thread).
}
}
RtpSenderBase::~RtpSenderBase() {
RTC_DCHECK(!media_channel_) << "Missing call to SetMediaChannel(nullptr)";
}
void RtpSenderBase::SetFrameEncryptor(
scoped_refptr<FrameEncryptorInterface> frame_encryptor) {
RTC_DCHECK_RUN_ON(signaling_thread_); if (stopped_) { return;
} // Special Case: Set the frame encryptor to any value on any existing channel.
worker_thread_->BlockingCall([&, ssrc = ssrc_] {
RTC_DCHECK_RUN_ON(worker_thread_);
frame_encryptor_ = std::move(frame_encryptor); if (media_channel_) {
media_channel_->SetFrameEncryptor(ssrc, frame_encryptor_);
}
});
}
if (media_channel_) {
media_channel_->SetParametersChangedCallback(nullptr);
}
// Note that setting the media_channel_ to nullptr and clearing the send state // via ClearSend_w, are separate operations. Stopping the actual send // operation, needs to be done via any of the paths that end up with a call to // ClearSend_w(), such as DetachTrackAndGetStopTask().
media_channel_ = media_channel; if (media_channel_) {
media_channel_->SetParametersChangedCallback(
[this] { OnParametersChanged(); });
}
media_channel_ ? worker_safety_->SetAlive() : worker_safety_->SetNotAlive();
}
RtpParameters RtpSenderBase::GetParameters() const {
RTC_DCHECK_RUN_ON(signaling_thread_); #if RTC_DCHECK_IS_ON // TODO(tommi): Here, we can use `last_transaction_id_` to allow for // multiple GetParameters() calls in a row return cached parameters // (we could still generate a new transaction_id every time). Since // `last_transaction_id_` will be reset whenever the parameters change, we // could reliably cache the currently active parameters and whenever // `last_transaction_id_` has been reset, only then take the penalty of // refreshing the cached value (or even rely on the `changed` callback to // refresh the cached parameters). Alternatively, we could maintain such a // cache only at the GetParametersInternal() level that's used internally in // webrtc, e.g. for stats purposes, and use the cache only when // GetParametersInternal() is called directly and not via GetParameters(). // // This `cached` variable and the `RTC_DCHECK` below are here temporarily // to verify the correctness of the cache as the first implementation of it // lands. Once we have confidence that the cache is reliably up to date, // we can update GetParameters() to use the cache without having to thread // hop. auto cached = cached_parameters_; #endif
RtpParameters result = GetParametersInternal(/*may_use_cache=*/false, /*with_all_layers=*/false); // Start a new transaction. `last_transaction_id_` will be reset whenever // the parameters change.
last_transaction_id_ = CreateRandomUuid();
result.transaction_id = last_transaction_id_.value();
#if RTC_DCHECK_IS_ON // The internal cache is only used when not stopped and ssrc_ is not 0. // `cached_parameters_` might get reset if the media channel is gone. if (cached && !stopped_ && ssrc_ != 0 && cached_parameters_) {
RtpParameters cached_filtered = *cached;
RemoveEncodingLayers(disabled_rids_, &cached_filtered.encodings); if (cached_filtered != result) {
RTC_LOG(LS_ERROR)
<< "Cached send params not equal to worker thread state.\n"
<< "Cached: " << cached_filtered << "\n"
<< "Result: " << result;
}
RTC_DCHECK(cached_filtered == result)
<< "The cached value should have been equal (filtered)";
} #endif return result;
}
std::optional<RTCError> RtpSenderBase::ValidateAndMaybeUpdateInitParameters( const RtpParameters& parameters) { if (UnimplementedRtpParameterHasValue(parameters)) { return LOG_ERROR(RTCError::UnsupportedParameter()
<< "Attempted to set an unimplemented parameter of " "RtpParameters.");
} if (ssrc_ == 0) { auto result = CheckRtpParametersInvalidModificationAndValues(
init_parameters_, parameters, send_codecs_, std::nullopt,
env_.field_trials()); if (result.ok()) {
init_parameters_ = parameters;
} return result;
} return std::nullopt;
}
RTCError RtpSenderBase::SetParametersInternal(const RtpParameters& parameters,
SetParametersCallback callback, bool blocking) {
RTC_DCHECK_RUN_ON(signaling_thread_);
RTC_DCHECK(!stopped_);
RTC_DCHECK(!blocking || !callback) << "Callback must be null if blocking";
if (auto error = ValidateAndMaybeUpdateInitParameters(parameters)) { if (callback) {
std::move(callback)(*error);
} return *error;
}
// Invalidate the cache to ensure that GetParameters() doesn't use a stale // cache while the worker thread is updating the parameters.
cached_parameters_.reset();
if (blocking && worker_thread_ == signaling_thread_) { return SetParametersInternalWorkaround(parameters);
}
// Specific handling for when a blocking operation is requested.
Event done_event;
RTCError blocking_error = RTCError::OK();
std::unique_ptr<RtpParameters> blocking_applied_parameters; if (blocking) {
callback = [&done_event, &blocking_error](RTCError error) {
blocking_error = std::move(error);
done_event.Set();
};
}
// A wrapper callback that fetches the parameters on the worker thread // immediately after they have been set, then posts a task to the signaling // thread to update the cache and invoke the original callback. // This ensures strict ordering: Set -> Fetch -> Update Cache -> Callback. // // Note: The callback might be invoked on a thread other than the worker // thread (e.g. the encoder queue). In that case, we must post a task back // to the worker thread to safely access `media_channel_`. auto callback_wrapper =
[this, blocking, &blocking_applied_parameters,
signaling_safety = signaling_safety_.flag(),
worker_safety_flag = worker_safety_, input_parameters = parameters,
callback = std::move(callback), ssrc = ssrc_](RTCError error) mutable { auto on_worker_thread = [this, blocking, &blocking_applied_parameters,
signaling_safety = std::move(signaling_safety),
input_parameters = std::move(input_parameters),
callback = std::move(callback), ssrc,
error = std::move(error)]() mutable {
RTC_DCHECK_RUN_ON(worker_thread_);
std::unique_ptr<RtpParameters> fetched_parameters; if (error.ok()) {
fetched_parameters = std::make_unique<RtpParameters>(
media_channel_->GetRtpSendParameters(ssrc));
}
if (error.ok()) {
cached_parameters_ = std::move(applied_parameters);
}
return error;
}
RTCError RtpSenderBase::CheckSetParameters(const RtpParameters& parameters) {
RTC_DCHECK_RUN_ON(signaling_thread_); if (stopped_) { return LOG_ERROR(RTCError::InvalidState()
<< "Cannot set parameters on a stopped sender.");
} if (!last_transaction_id_) { return LOG_ERROR(RTCError::InvalidState()
<< "Failed to set parameters since getParameters() has " "never been called" " on this sender");
} if (last_transaction_id_ != parameters.transaction_id) { return LOG_ERROR(RTCError::InvalidModification()
<< "Failed to set parameters since the transaction_id " "doesn't match" " the last value returned from getParameters()");
}
return RTCError::OK();
}
RTCError RtpSenderBase::SetParameters(const RtpParameters& parameters) {
RTC_DCHECK_RUN_ON(signaling_thread_);
TRACE_EVENT0("webrtc", "RtpSenderBase::SetParameters");
RTCError result = CheckSetParameters(parameters); if (!result.ok()) return result;
result = SetParametersInternal(parameters, nullptr, /*blocking=*/true);
last_transaction_id_.reset(); return result;
}
void RtpSenderBase::SetObserver(RtpSenderObserverInterface* observer) {
RTC_DCHECK_RUN_ON(signaling_thread_);
observer_ = observer; // Deliver any notifications the observer may have missed by being set late. if (sent_first_packet_ && observer_) {
observer_->OnFirstPacketSent(media_type());
}
}
if (stopped_) {
RTC_LOG(LS_ERROR) << "SetTrack can't be called on a stopped RtpSender."; returnfalse;
} if (track && track->kind() != track_kind()) {
RTC_LOG(LS_ERROR) << "SetTrack with " << track->kind()
<< " called on RtpSender with " << track_kind()
<< " track."; returnfalse;
}
// Detach from old track. if (track_) {
DetachTrack();
track_->UnregisterObserver(this);
RemoveTrackFromStats();
}
// Attach to new track. bool prev_can_send_track = can_send_track(); // Keep a reference to the old track to keep it alive until we call SetSend.
scoped_refptr<MediaStreamTrackInterface> old_track = track_;
track_ = track; if (track_) {
track_->RegisterObserver(this);
AttachTrack();
}
// If we are already sending with a particular SSRC, stop sending. if (can_send_track()) {
ClearSend();
RemoveTrackFromStats();
}
ssrc_ = ssrc; if (can_send_track()) {
SetSend();
AddTrackToStats();
}
RtpParameters current_parameters; bool params_modified = false;
worker_thread_->BlockingCall([&, ssrc = ssrc_] {
RTC_DCHECK_RUN_ON(worker_thread_); if (!init_parameters_.encodings.empty() ||
init_parameters_.degradation_preference.has_value()) { if (ssrc != 0) {
RTC_DCHECK(media_channel_); // Get the current parameters, which are constructed from the SDP. The // number of layers in the SDP is currently authoritative to support SDP // munging for Plan-B simulcast with "a=ssrc-group:SIM <ssrc-id>..." // lines as described in RFC 5576. All fields should be default // constructed and the SSRC field set, which we need to copy.
current_parameters = media_channel_->GetRtpSendParameters(ssrc); // SSRC 0 has special meaning as "no stream". In this case, // current_parameters may have size 0.
RTC_CHECK_GE(current_parameters.encodings.size(),
init_parameters_.encodings.size()); for (size_t i = 0; i < init_parameters_.encodings.size(); ++i) {
init_parameters_.encodings[i].ssrc =
current_parameters.encodings[i].ssrc;
init_parameters_.encodings[i].rid =
current_parameters.encodings[i].rid;
current_parameters.encodings[i] = init_parameters_.encodings[i];
}
current_parameters.degradation_preference =
init_parameters_.degradation_preference;
params_modified =
media_channel_
->SetRtpSendParameters(ssrc, current_parameters, nullptr)
.ok(); if (params_modified) { // The parameters may change as they're applied.
current_parameters = media_channel_->GetRtpSendParameters(ssrc);
}
} // Clear the `init_parameters_` after they have been applied to the // media channel. This prevents stale values from being used in // subsequent calls to `SetSsrc`, which could happen if `SetSsrc` is // called multiple times on the same sender. See // https://issues.webrtc.org/issues/500993975 for details.
init_parameters_.encodings.clear();
init_parameters_.degradation_preference = std::nullopt;
}
// While we're on the worker thread, attach the frame decryptor, transformer // and selector to the current media channel. if (frame_encryptor_) {
media_channel_->SetFrameEncryptor(ssrc, frame_encryptor_);
} if (frame_transformer_) {
media_channel_->SetEncoderToPacketizerFrameTransformer(
ssrc, frame_transformer_);
} if (encoder_selector_) {
media_channel_->SetEncoderSelector(ssrc, encoder_selector_);
}
}); if (params_modified) { // As a result of the `SetRtpSendParameters` call, an async task will be // queued to update `cached_parameters_` - unless the parameters didn't // really change. In any case, we might as well stash away the current // parameters right away.
cached_parameters_ = std::move(current_parameters);
}
}
// If we are already sending with a particular SSRC, stop sending. if (can_send_track()) {
ClearSend();
RemoveTrackFromStats();
}
ssrc_ = ssrc; if (can_send_track()) {
SetSend();
AddTrackToStats();
}
if (!init_parameters_.encodings.empty() ||
init_parameters_.degradation_preference.has_value()) { if (ssrc != 0) {
RTC_DCHECK(media_channel_); // Get the current parameters, which are constructed from the SDP. The // number of layers in the SDP is currently authoritative to support SDP // munging for Plan-B simulcast with "a=ssrc-group:SIM <ssrc-id>..." // lines as described in RFC 5576. All fields should be default // constructed and the SSRC field set, which we need to copy.
current_parameters = media_channel_->GetRtpSendParameters(ssrc); // SSRC 0 has special meaning as "no stream". In this case, // current_parameters may have size 0.
RTC_CHECK_GE(current_parameters.encodings.size(),
init_parameters_.encodings.size()); for (size_t i = 0; i < init_parameters_.encodings.size(); ++i) {
init_parameters_.encodings[i].ssrc =
current_parameters.encodings[i].ssrc;
init_parameters_.encodings[i].rid =
current_parameters.encodings[i].rid;
current_parameters.encodings[i] = init_parameters_.encodings[i];
}
current_parameters.degradation_preference =
init_parameters_.degradation_preference;
params_modified =
media_channel_
->SetRtpSendParameters(ssrc, current_parameters, nullptr)
.ok(); if (params_modified) { // The parameters may change as they're applied.
current_parameters = media_channel_->GetRtpSendParameters(ssrc);
}
} // Clear the `init_parameters_` after they have been applied to the // media channel. This prevents stale values from being used in // subsequent calls to `SetSsrc`, which could happen if `SetSsrc` is // called multiple times on the same sender. See // https://issues.webrtc.org/issues/500993975 for details.
init_parameters_.encodings.clear();
init_parameters_.degradation_preference = std::nullopt;
}
// While we're on the worker thread, attach the frame decryptor, transformer // and selector to the current media channel. if (frame_encryptor_ != nullptr) {
media_channel_->SetFrameEncryptor(ssrc, frame_encryptor_);
} if (frame_transformer_ != nullptr) {
media_channel_->SetEncoderToPacketizerFrameTransformer(
ssrc, frame_transformer_);
} if (encoder_selector_ != nullptr) {
media_channel_->SetEncoderSelector(ssrc, encoder_selector_);
}
void RtpSenderBase::Stop() {
RTC_DCHECK_RUN_ON(signaling_thread_);
TRACE_EVENT0("webrtc", "RtpSenderBase::Stop"); // TODO(deadbeef): Need to do more here to fully stop sending packets. if (stopped_) { return;
} if (track_) {
DetachTrack();
track_->UnregisterObserver(this);
}
bool clear_send = can_send_track(); if (clear_send) {
RemoveTrackFromStats();
}
RTCError RtpSenderBase::DisableEncodingLayers( const std::vector<std::string>& rids) {
RTC_DCHECK_RUN_ON(signaling_thread_); if (stopped_) { return LOG_ERROR(RTCError::InvalidState()
<< "Cannot disable encodings on a stopped sender.");
}
bool all_already_disabled = true; for (const std::string& rid : rids) { if (!absl::c_linear_search(disabled_rids_, rid)) {
all_already_disabled = false; break;
}
} if (all_already_disabled) { return RTCError::OK();
}
// Check that all the specified layers exist and disable them in the channel.
RtpParameters parameters = GetParametersInternalWithAllLayers(); for (const std::string& rid : rids) { if (absl::c_none_of(parameters.encodings,
[&rid](const RtpEncodingParameters& encoding) { return encoding.rid == rid;
})) { return LOG_ERROR(RTCError::InvalidParameter()
<< "RID: " << rid
<< " does not refer to a valid layer.");
}
}
if (ssrc_ == 0) {
RemoveEncodingLayers(rids, &init_parameters_.encodings); // Invalidate any transaction upon success.
last_transaction_id_.reset(); return RTCError::OK();
}
for (RtpEncodingParameters& encoding : parameters.encodings) { // Remain active if not in the disable list.
encoding.active &= absl::c_none_of(
rids,
[&encoding](const std::string& rid) { return encoding.rid == rid; });
}
RTCError result = SetParametersInternalWithAllLayers(parameters); if (result.ok()) { for (constauto& rid : rids) { // Avoid inserting duplicates. if (std::find(disabled_rids_.begin(), disabled_rids_.end(), rid) ==
disabled_rids_.end()) {
disabled_rids_.push_back(rid);
}
} // Invalidate any transaction upon success.
last_transaction_id_.reset();
} return result;
}
bool AudioRtpSender::CanInsertDtmf() {
RTC_DCHECK_RUN_ON(signaling_thread_); if (stopped_) { returnfalse;
} // Check that this RTP sender is active (description has been applied that // matches an SSRC to its ID). if (ssrc_ == 0) {
RTC_LOG(LS_ERROR) << "CanInsertDtmf: Sender does not have SSRC."; returnfalse;
} return worker_thread_->BlockingCall([&] {
RTC_DCHECK_RUN_ON(worker_thread_); return media_channel_ ? voice_media_channel()->CanInsertDtmf() : false;
});
}
bool AudioRtpSender::InsertDtmf(int code, int duration) {
RTC_DCHECK_RUN_ON(signaling_thread_); if (stopped_) { returnfalse;
} if (ssrc_ == 0) {
RTC_LOG(LS_ERROR) << "InsertDtmf: Sender does not have SSRC."; returnfalse;
} return worker_thread_->BlockingCall([&, ssrc = ssrc_] {
RTC_DCHECK_RUN_ON(worker_thread_); return media_channel_
? voice_media_channel()->InsertDtmf(ssrc, code, duration)
: false;
});
}
RTCError AudioRtpSender::GenerateKeyFrame( const std::vector<std::string>& rids) {
RTC_DCHECK_RUN_ON(signaling_thread_);
RTC_DLOG(LS_ERROR) << "Tried to get generate a key frame for audio."; return RTCError::UnsupportedOperation()
<< "Generating key frames for audio is not supported.";
}
void AudioRtpSender::SetSend() {
RTC_DCHECK_RUN_ON(signaling_thread_);
RTC_DCHECK(!stopped_);
RTC_DCHECK(can_send_track()); if (stopped_) { return;
}
AudioOptions options; #if !defined(WEBRTC_CHROMIUM_BUILD) && !defined(WEBRTC_WEBKIT_BUILD) // TODO(tommi): Remove this hack when we move CreateAudioSource out of // PeerConnection. This is a bit of a strange way to apply local audio // options since it is also applied to all streams/channels, local or remote. if (track_->enabled() && audio_track()->GetSource() &&
!audio_track()->GetSource()->remote()) {
options = audio_track()->GetSource()->options();
} #endif
// `track_->enabled()` hops to the signaling thread, so call it before we hop // to the worker thread or else it will deadlock. bool track_enabled = track_->enabled();
InvalidateCache(); bool success = worker_thread_->BlockingCall([&, ssrc = ssrc_] {
RTC_DCHECK_RUN_ON(worker_thread_); return media_channel_
? voice_media_channel()->SetAudioSend(
ssrc, track_enabled, &options, sink_adapter_.get())
: false;
}); if (!success) {
RTC_LOG(LS_ERROR) << "SetAudioSend: ssrc is incorrect: " << ssrc_;
}
}
scoped_refptr<DtmfSenderInterface> VideoRtpSender::GetDtmfSender() const {
RTC_DCHECK_RUN_ON(signaling_thread_);
RTC_DLOG(LS_ERROR) << "Tried to get DTMF sender from video sender."; return nullptr;
}
RTCError VideoRtpSender::GenerateKeyFrame( const std::vector<std::string>& rids) {
RTC_DCHECK_RUN_ON(signaling_thread_); if (stopped_ || ssrc_ == 0) {
RTC_LOG(LS_WARNING) << "Tried to generate key frame for sender that is " "stopped or has no media channel."; // Wouldn't it be more correct to return an error? return RTCError::OK();
}
constauto parameters = GetParametersInternal(); for (constauto& rid : rids) { if (rid.empty()) { return LOG_ERROR(RTCError::InvalidParameter()
<< "Attempted to specify an empty rid.");
} if (!absl::c_any_of(parameters.encodings,
[&rid](const RtpEncodingParameters& parameters) { return parameters.rid == rid;
})) { return LOG_ERROR(RTCError::InvalidParameter()
<< "Attempted to specify a rid not configured.");
}
}
worker_thread_->PostTask(SafeTask(worker_safety_, [this, rids, ssrc = ssrc_] {
RTC_DCHECK_RUN_ON(worker_thread_); if (video_media_channel()) {
video_media_channel()->GenerateSendKeyFrame(ssrc, rids);
}
}));
void VideoRtpSender::ClearSend() {
RTC_DCHECK_RUN_ON(signaling_thread_);
RTC_DCHECK(ssrc_ != 0);
RTC_DCHECK(!stopped_); // Allow SetVideoSend to fail since `enable` is false and `source` is null. // This the normal case when the underlying media channel has already been // deleted.
worker_thread_->BlockingCall([&, ssrc = ssrc_] {
RTC_DCHECK_RUN_ON(worker_thread_);
ClearSend_w(ssrc);
});
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.