| /* | |
| * Copyright (c) 2026 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 "rtc_tools/rtc_event_log_visualizer/analyze_rtp_rtcp.h" | |
| #include <algorithm> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <deque> | |
| #include <map> | |
| #include <optional> | |
| #include <string> | |
| #include <tuple> | |
| #include <unordered_set> | |
| #include <utility> | |
| #include <vector> | |
| #include "api/function_view.h" | |
| #include "api/rtp_headers.h" | |
| #include "api/transport/ecn_marking.h" | |
| #include "api/units/data_rate.h" | |
| #include "api/units/time_delta.h" | |
| #include "api/units/timestamp.h" | |
| #include "logging/rtc_event_log/events/logged_rtp_rtcp.h" | |
| #include "logging/rtc_event_log/rtc_event_log_parser.h" | |
| #include "logging/rtc_event_log/rtc_event_processor.h" | |
| #include "modules/rtp_rtcp/include/rtp_rtcp_defines.h" | |
| #include "modules/rtp_rtcp/source/rtcp_packet/congestion_control_feedback.h" | |
| #include "modules/rtp_rtcp/source/rtcp_packet/receiver_report.h" | |
| #include "modules/rtp_rtcp/source/rtcp_packet/report_block.h" | |
| #include "modules/rtp_rtcp/source/rtcp_packet/sender_report.h" | |
| #include "modules/rtp_rtcp/source/rtcp_packet/target_bitrate.h" | |
| #include "modules/rtp_rtcp/source/rtcp_packet/transport_feedback.h" | |
| #include "rtc_base/checks.h" | |
| #include "rtc_base/logging.h" | |
| #include "rtc_base/numerics/sequence_number_unwrapper.h" | |
| #include "rtc_tools/rtc_event_log_visualizer/analyzer_common.h" | |
| #include "rtc_tools/rtc_event_log_visualizer/plot_base.h" | |
| namespace webrtc { | |
| namespace { | |
| template <typename T> | |
| TimeSeries CreateRtcpTypeTimeSeries(const std::vector<T>& rtcp_list, | |
| AnalyzerConfig config, | |
| std::string rtcp_name, | |
| int category_id) { | |
| TimeSeries time_series(rtcp_name, LineStyle::kNone, PointStyle::kHighlight); | |
| for (const auto& rtcp : rtcp_list) { | |
| float x = config.GetCallTimeSec(rtcp.timestamp); | |
| float y = category_id; | |
| time_series.points.emplace_back(x, y); | |
| } | |
| return time_series; | |
| } | |
| struct PacketLossSummary { | |
| size_t num_packets = 0; | |
| size_t num_lost_packets = 0; | |
| Timestamp base_time = Timestamp::MinusInfinity(); | |
| }; | |
| } // namespace | |
| float GetHighestSeqNumber(const rtcp::ReportBlock& block) { | |
| return block.extended_high_seq_num(); | |
| } | |
| float GetFractionLost(const rtcp::ReportBlock& block) { | |
| return static_cast<double>(block.fraction_lost()) / 256 * 100; | |
| } | |
| float GetCumulativeLost(const rtcp::ReportBlock& block) { | |
| return block.cumulative_lost(); | |
| } | |
| float DelaySinceLastSr(const rtcp::ReportBlock& block) { | |
| return static_cast<double>(block.delay_since_last_sr()) / 65536; | |
| } | |
| void CreatePacketGraph(PacketDirection direction, | |
| const ParsedRtcEventLog& parsed_log, | |
| const AnalyzerConfig& config, | |
| Plot* plot) { | |
| for (const auto& stream : parsed_log.rtp_packets_by_ssrc(direction)) { | |
| // Filter on SSRC. | |
| if (!MatchingSsrc(stream.ssrc, config.desired_ssrc_)) { | |
| continue; | |
| } | |
| TimeSeries time_series(GetStreamName(parsed_log, direction, stream.ssrc), | |
| LineStyle::kBar); | |
| auto GetPacketSize = [](const LoggedRtpPacket& packet) { | |
| return std::optional<float>(packet.total_length); | |
| }; | |
| auto ToCallTime = [&config](const LoggedRtpPacket& packet) { | |
| return config.GetCallTimeSec(packet.timestamp); | |
| }; | |
| ProcessPoints<LoggedRtpPacket>(ToCallTime, GetPacketSize, | |
| stream.packet_view, &time_series); | |
| plot->AppendTimeSeries(std::move(time_series)); | |
| } | |
| plot->SetXAxis(config.CallBeginTimeSec(), config.CallEndTimeSec(), "Time (s)", | |
| kLeftMargin, kRightMargin); | |
| plot->SetSuggestedYAxis(0, 1, "Packet size (bytes)", kBottomMargin, | |
| kTopMargin); | |
| plot->SetTitle(GetDirectionAsString(direction) + " RTP packets"); | |
| } | |
| void CreateRtcpTypeGraph(PacketDirection direction, | |
| const ParsedRtcEventLog& parsed_log, | |
| const AnalyzerConfig& config, | |
| Plot* plot) { | |
| plot->AppendTimeSeries(CreateRtcpTypeTimeSeries( | |
| parsed_log.transport_feedbacks(direction), config, "TWCC", 1)); | |
| plot->AppendTimeSeries(CreateRtcpTypeTimeSeries( | |
| parsed_log.congestion_feedback(direction), config, "CCFB", 2)); | |
| plot->AppendTimeSeries(CreateRtcpTypeTimeSeries( | |
| parsed_log.receiver_reports(direction), config, "RR", 3)); | |
| plot->AppendTimeSeries(CreateRtcpTypeTimeSeries( | |
| parsed_log.sender_reports(direction), config, "SR", 4)); | |
| plot->AppendTimeSeries(CreateRtcpTypeTimeSeries( | |
| parsed_log.extended_reports(direction), config, "XR", 5)); | |
| plot->AppendTimeSeries( | |
| CreateRtcpTypeTimeSeries(parsed_log.nacks(direction), config, "NACK", 6)); | |
| plot->AppendTimeSeries( | |
| CreateRtcpTypeTimeSeries(parsed_log.rembs(direction), config, "REMB", 7)); | |
| plot->AppendTimeSeries( | |
| CreateRtcpTypeTimeSeries(parsed_log.firs(direction), config, "FIR", 8)); | |
| plot->AppendTimeSeries( | |
| CreateRtcpTypeTimeSeries(parsed_log.plis(direction), config, "PLI", 9)); | |
| plot->AppendTimeSeries( | |
| CreateRtcpTypeTimeSeries(parsed_log.byes(direction), config, "BYE", 10)); | |
| plot->SetXAxis(config.CallBeginTimeSec(), config.CallEndTimeSec(), "Time (s)", | |
| kLeftMargin, kRightMargin); | |
| plot->SetSuggestedYAxis(0, 1, "RTCP type", kBottomMargin, kTopMargin); | |
| plot->SetTitle(GetDirectionAsString(direction) + " RTCP packets"); | |
| plot->SetYAxisTickLabels({{1, "TWCC"}, | |
| {2, "CCFB"}, | |
| {3, "RR"}, | |
| {4, "SR"}, | |
| {5, "XR"}, | |
| {6, "NACK"}, | |
| {7, "REMB"}, | |
| {8, "FIR"}, | |
| {9, "PLI"}, | |
| {10, "BYE"}}); | |
| } | |
| template <typename IterableType> | |
| void CreateAccumulatedPacketsTimeSeries(Plot* plot, | |
| const AnalyzerConfig& config, | |
| const IterableType& packets, | |
| const std::string& label) { | |
| TimeSeries time_series(label, LineStyle::kStep); | |
| for (size_t i = 0; i < packets.size(); i++) { | |
| float x = config.GetCallTimeSec(packets[i].log_time()); | |
| time_series.points.emplace_back(x, i + 1); | |
| } | |
| plot->AppendTimeSeries(std::move(time_series)); | |
| } | |
| void CreateAccumulatedPacketsGraph(PacketDirection direction, | |
| const ParsedRtcEventLog& parsed_log, | |
| const AnalyzerConfig& config, | |
| Plot* plot) { | |
| for (const auto& stream : parsed_log.rtp_packets_by_ssrc(direction)) { | |
| if (!MatchingSsrc(stream.ssrc, config.desired_ssrc_)) | |
| continue; | |
| std::string label = | |
| std::string("RTP ") + GetStreamName(parsed_log, direction, stream.ssrc); | |
| CreateAccumulatedPacketsTimeSeries(plot, config, stream.packet_view, label); | |
| } | |
| std::string label = | |
| std::string("RTCP ") + "(" + GetDirectionAsShortString(direction) + ")"; | |
| if (direction == kIncomingPacket) { | |
| CreateAccumulatedPacketsTimeSeries( | |
| plot, config, parsed_log.incoming_rtcp_packets(), label); | |
| } else { | |
| CreateAccumulatedPacketsTimeSeries( | |
| plot, config, parsed_log.outgoing_rtcp_packets(), label); | |
| } | |
| plot->SetXAxis(config.CallBeginTimeSec(), config.CallEndTimeSec(), "Time (s)", | |
| kLeftMargin, kRightMargin); | |
| plot->SetSuggestedYAxis(0, 1, "Received Packets", kBottomMargin, kTopMargin); | |
| plot->SetTitle(std::string("Accumulated ") + GetDirectionAsString(direction) + | |
| " RTP/RTCP packets"); | |
| } | |
| void CreatePacketRateGraph(PacketDirection direction, | |
| const ParsedRtcEventLog& parsed_log, | |
| const AnalyzerConfig& config, | |
| Plot* plot) { | |
| auto CountPackets = [](auto packet) { return 1.0; }; | |
| for (const auto& stream : parsed_log.rtp_packets_by_ssrc(direction)) { | |
| // Filter on SSRC. | |
| if (!MatchingSsrc(stream.ssrc, config.desired_ssrc_)) { | |
| continue; | |
| } | |
| TimeSeries time_series( | |
| std::string("RTP ") + GetStreamName(parsed_log, direction, stream.ssrc), | |
| LineStyle::kLine); | |
| MovingAverage<LoggedRtpPacket, double>(CountPackets, stream.packet_view, | |
| config, &time_series); | |
| plot->AppendTimeSeries(std::move(time_series)); | |
| } | |
| TimeSeries time_series( | |
| std::string("RTCP ") + "(" + GetDirectionAsShortString(direction) + ")", | |
| LineStyle::kLine); | |
| if (direction == kIncomingPacket) { | |
| MovingAverage<LoggedRtcpPacketIncoming, double>( | |
| CountPackets, parsed_log.incoming_rtcp_packets(), config, &time_series); | |
| } else { | |
| MovingAverage<LoggedRtcpPacketOutgoing, double>( | |
| CountPackets, parsed_log.outgoing_rtcp_packets(), config, &time_series); | |
| } | |
| plot->AppendTimeSeries(std::move(time_series)); | |
| plot->SetXAxis(config.CallBeginTimeSec(), config.CallEndTimeSec(), "Time (s)", | |
| kLeftMargin, kRightMargin); | |
| plot->SetSuggestedYAxis(0, 1, "Packet Rate (packets/s)", kBottomMargin, | |
| kTopMargin); | |
| plot->SetTitle("Rate of " + GetDirectionAsString(direction) + | |
| " RTP/RTCP packets"); | |
| } | |
| void CreateTotalPacketRateGraph(PacketDirection direction, | |
| const ParsedRtcEventLog& parsed_log, | |
| const AnalyzerConfig& config, | |
| Plot* plot) { | |
| // Contains a log timestamp to enable counting logged events of different | |
| // types using MovingAverage(). | |
| class LogTime { | |
| public: | |
| explicit LogTime(Timestamp log_time) : log_time_(log_time) {} | |
| Timestamp log_time() const { return log_time_; } | |
| private: | |
| Timestamp log_time_; | |
| }; | |
| std::vector<LogTime> packet_times; | |
| auto handle_rtp = [&packet_times](const LoggedRtpPacket& packet) { | |
| packet_times.emplace_back(packet.log_time()); | |
| }; | |
| RtcEventProcessor process; | |
| for (const auto& stream : parsed_log.rtp_packets_by_ssrc(direction)) { | |
| process.AddEvents(stream.packet_view, handle_rtp, direction); | |
| } | |
| if (direction == kIncomingPacket) { | |
| auto handle_incoming_rtcp = | |
| [&packet_times](const LoggedRtcpPacketIncoming& packet) { | |
| packet_times.emplace_back(packet.log_time()); | |
| }; | |
| process.AddEvents(parsed_log.incoming_rtcp_packets(), handle_incoming_rtcp); | |
| } else { | |
| auto handle_outgoing_rtcp = | |
| [&packet_times](const LoggedRtcpPacketOutgoing& packet) { | |
| packet_times.emplace_back(packet.log_time()); | |
| }; | |
| process.AddEvents(parsed_log.outgoing_rtcp_packets(), handle_outgoing_rtcp); | |
| } | |
| process.ProcessEventsInOrder(); | |
| TimeSeries time_series(std::string("Total ") + "(" + | |
| GetDirectionAsShortString(direction) + ") packets", | |
| LineStyle::kLine); | |
| MovingAverage<LogTime, uint64_t>([](auto packet) { return 1; }, packet_times, | |
| config, &time_series); | |
| plot->AppendTimeSeries(std::move(time_series)); | |
| plot->SetXAxis(config.CallBeginTimeSec(), config.CallEndTimeSec(), "Time (s)", | |
| kLeftMargin, kRightMargin); | |
| plot->SetSuggestedYAxis(0, 1, "Packet Rate (packets/s)", kBottomMargin, | |
| kTopMargin); | |
| plot->SetTitle("Rate of all " + GetDirectionAsString(direction) + | |
| " RTP/RTCP packets"); | |
| } | |
| // For each SSRC, plot the sequence number difference between consecutive | |
| // incoming packets. | |
| void CreateSequenceNumberGraph(const ParsedRtcEventLog& parsed_log, | |
| const AnalyzerConfig& config, | |
| Plot* plot) { | |
| for (const auto& stream : parsed_log.incoming_rtp_packets_by_ssrc()) { | |
| // Filter on SSRC. | |
| if (!MatchingSsrc(stream.ssrc, config.desired_ssrc_)) { | |
| continue; | |
| } | |
| TimeSeries time_series( | |
| GetStreamName(parsed_log, kIncomingPacket, stream.ssrc), | |
| LineStyle::kBar); | |
| auto GetSequenceNumberDiff = [](const LoggedRtpPacketIncoming& old_packet, | |
| const LoggedRtpPacketIncoming& new_packet) { | |
| int64_t diff = | |
| WrappingDifference(new_packet.rtp.header.sequenceNumber, | |
| old_packet.rtp.header.sequenceNumber, 1ul << 16); | |
| return diff; | |
| }; | |
| auto ToCallTime = [&config](const LoggedRtpPacketIncoming& packet) { | |
| return config.GetCallTimeSec(packet.log_time()); | |
| }; | |
| ProcessPairs<LoggedRtpPacketIncoming, float>( | |
| ToCallTime, GetSequenceNumberDiff, stream.incoming_packets, | |
| &time_series); | |
| plot->AppendTimeSeries(std::move(time_series)); | |
| } | |
| plot->SetXAxis(config.CallBeginTimeSec(), config.CallEndTimeSec(), "Time (s)", | |
| kLeftMargin, kRightMargin); | |
| plot->SetSuggestedYAxis(0, 1, "Difference since last packet", kBottomMargin, | |
| kTopMargin); | |
| plot->SetTitle("Incoming sequence number delta"); | |
| } | |
| void CreateIncomingPacketLossGraph(const ParsedRtcEventLog& parsed_log, | |
| const AnalyzerConfig& config, | |
| Plot* plot) { | |
| for (const auto& stream : parsed_log.incoming_rtp_packets_by_ssrc()) { | |
| const std::vector<LoggedRtpPacketIncoming>& packets = | |
| stream.incoming_packets; | |
| // Filter on SSRC. | |
| if (!MatchingSsrc(stream.ssrc, config.desired_ssrc_) || packets.empty()) { | |
| continue; | |
| } | |
| TimeSeries time_series( | |
| GetStreamName(parsed_log, kIncomingPacket, stream.ssrc), | |
| LineStyle::kLine, PointStyle::kHighlight); | |
| // TODO(terelius): Should the window and step size be read from the class | |
| // instead? | |
| const TimeDelta kWindow = TimeDelta::Millis(1000); | |
| const TimeDelta kStep = TimeDelta::Millis(1000); | |
| SeqNumUnwrapper<uint16_t> unwrapper_; | |
| SeqNumUnwrapper<uint16_t> prior_unwrapper_; | |
| size_t window_index_begin = 0; | |
| size_t window_index_end = 0; | |
| uint64_t highest_seq_number = | |
| unwrapper_.Unwrap(packets[0].rtp.header.sequenceNumber) - 1; | |
| uint64_t highest_prior_seq_number = | |
| prior_unwrapper_.Unwrap(packets[0].rtp.header.sequenceNumber) - 1; | |
| for (Timestamp t = config.begin_time_; t < config.end_time_ + kStep; | |
| t += kStep) { | |
| while (window_index_end < packets.size() && | |
| packets[window_index_end].rtp.log_time() < t) { | |
| uint64_t sequence_number = unwrapper_.Unwrap( | |
| packets[window_index_end].rtp.header.sequenceNumber); | |
| highest_seq_number = std::max(highest_seq_number, sequence_number); | |
| ++window_index_end; | |
| } | |
| while (window_index_begin < packets.size() && | |
| packets[window_index_begin].rtp.log_time() < t - kWindow) { | |
| uint64_t sequence_number = prior_unwrapper_.Unwrap( | |
| packets[window_index_begin].rtp.header.sequenceNumber); | |
| highest_prior_seq_number = | |
| std::max(highest_prior_seq_number, sequence_number); | |
| ++window_index_begin; | |
| } | |
| float x = config.GetCallTimeSec(t); | |
| uint64_t expected_packets = highest_seq_number - highest_prior_seq_number; | |
| if (expected_packets > 0) { | |
| int64_t received_packets = window_index_end - window_index_begin; | |
| int64_t lost_packets = expected_packets - received_packets; | |
| float y = static_cast<float>(lost_packets) / expected_packets * 100; | |
| time_series.points.emplace_back(x, y); | |
| } | |
| } | |
| plot->AppendTimeSeries(std::move(time_series)); | |
| } | |
| plot->SetXAxis(config.CallBeginTimeSec(), config.CallEndTimeSec(), "Time (s)", | |
| kLeftMargin, kRightMargin); | |
| plot->SetSuggestedYAxis(0, 1, "Loss rate (in %)", kBottomMargin, kTopMargin); | |
| plot->SetTitle("Incoming packet loss (derived from incoming packets)"); | |
| } | |
| // For each SSRC, plot the bandwidth used by that stream. | |
| void CreateStreamBitrateGraph(PacketDirection direction, | |
| const ParsedRtcEventLog& parsed_log, | |
| const AnalyzerConfig& config, | |
| Plot* plot) { | |
| for (const auto& stream : parsed_log.rtp_packets_by_ssrc(direction)) { | |
| // Filter on SSRC. | |
| if (!MatchingSsrc(stream.ssrc, config.desired_ssrc_)) { | |
| continue; | |
| } | |
| TimeSeries time_series(GetStreamName(parsed_log, direction, stream.ssrc), | |
| LineStyle::kLine); | |
| auto GetPacketSizeKilobits = [](const LoggedRtpPacket& packet) { | |
| return packet.total_length * 8.0 / 1000.0; | |
| }; | |
| MovingAverage<LoggedRtpPacket, double>( | |
| GetPacketSizeKilobits, stream.packet_view, config, &time_series); | |
| plot->AppendTimeSeries(std::move(time_series)); | |
| } | |
| plot->SetXAxis(config.CallBeginTimeSec(), config.CallEndTimeSec(), "Time (s)", | |
| kLeftMargin, kRightMargin); | |
| plot->SetSuggestedYAxis(0, 1, "Bitrate (kbps)", kBottomMargin, kTopMargin); | |
| plot->SetTitle(GetDirectionAsString(direction) + " bitrate per stream"); | |
| } | |
| // Plot the bitrate allocation for each temporal and spatial layer. | |
| // Computed from RTCP XR target bitrate block, so the graph is only populated if | |
| // those are sent. | |
| void CreateBitrateAllocationGraph(PacketDirection direction, | |
| const ParsedRtcEventLog& parsed_log, | |
| const AnalyzerConfig& config, | |
| Plot* plot) { | |
| std::map<LayerDescription, TimeSeries> time_series; | |
| const auto& xr_list = parsed_log.extended_reports(direction); | |
| for (const auto& rtcp : xr_list) { | |
| const std::optional<rtcp::TargetBitrate>& target_bitrate = | |
| rtcp.xr.target_bitrate(); | |
| if (!target_bitrate.has_value()) | |
| continue; | |
| for (const auto& bitrate_item : target_bitrate->GetTargetBitrates()) { | |
| LayerDescription layer(rtcp.xr.sender_ssrc(), bitrate_item.spatial_layer, | |
| bitrate_item.temporal_layer); | |
| auto time_series_it = time_series.find(layer); | |
| if (time_series_it == time_series.end()) { | |
| std::string layer_name = GetLayerName(layer); | |
| bool inserted; | |
| std::tie(time_series_it, inserted) = time_series.insert( | |
| std::make_pair(layer, TimeSeries(layer_name, LineStyle::kStep))); | |
| RTC_DCHECK(inserted); | |
| } | |
| float x = config.GetCallTimeSec(rtcp.log_time()); | |
| float y = bitrate_item.target_bitrate_kbps; | |
| time_series_it->second.points.emplace_back(x, y); | |
| } | |
| } | |
| for (auto& layer : time_series) { | |
| plot->AppendTimeSeries(std::move(layer.second)); | |
| } | |
| plot->SetXAxis(config.CallBeginTimeSec(), config.CallEndTimeSec(), "Time (s)", | |
| kLeftMargin, kRightMargin); | |
| plot->SetSuggestedYAxis(0, 1, "Bitrate (kbps)", kBottomMargin, kTopMargin); | |
| if (direction == kIncomingPacket) | |
| plot->SetTitle("Target bitrate per incoming layer"); | |
| else | |
| plot->SetTitle("Target bitrate per outgoing layer"); | |
| } | |
| void CreateEcnFeedbackGraph(Plot* plot, | |
| PacketDirection direction, | |
| const ParsedRtcEventLog& parsed_log, | |
| const AnalyzerConfig& config) { | |
| TimeSeries not_ect("Not ECN capable", LineStyle::kBar, | |
| PointStyle::kHighlight); | |
| TimeSeries ect_1("ECN capable", LineStyle::kBar, PointStyle::kHighlight); | |
| TimeSeries ce("Congestion experienced", LineStyle::kBar, | |
| PointStyle::kHighlight); | |
| for (const LoggedRtcpCongestionControlFeedback& feedback : | |
| parsed_log.congestion_feedback(direction)) { | |
| int ect_1_count = 0; | |
| int not_ect_count = 0; | |
| int ce_count = 0; | |
| for (const rtcp::CongestionControlFeedback::PacketInfo& info : | |
| feedback.congestion_feedback.packets()) { | |
| switch (info.ecn) { | |
| case EcnMarking::kNotEct: | |
| ++not_ect_count; | |
| break; | |
| case EcnMarking::kEct1: | |
| ++ect_1_count; | |
| break; | |
| case EcnMarking::kEct0: | |
| RTC_LOG(LS_ERROR) << "unexpected ect(0)"; | |
| break; | |
| case EcnMarking::kCe: | |
| ++ce_count; | |
| break; | |
| } | |
| } | |
| ect_1.points.emplace_back(config.GetCallTimeSec(feedback.timestamp), | |
| ect_1_count); | |
| not_ect.points.emplace_back(config.GetCallTimeSec(feedback.timestamp), | |
| not_ect_count); | |
| ce.points.emplace_back(config.GetCallTimeSec(feedback.timestamp), ce_count); | |
| } | |
| plot->AppendTimeSeriesIfNotEmpty(std::move(ect_1)); | |
| plot->AppendTimeSeriesIfNotEmpty(std::move(not_ect)); | |
| plot->AppendTimeSeriesIfNotEmpty(std::move(ce)); | |
| plot->SetXAxis(config.CallBeginTimeSec(), config.CallEndTimeSec(), "Time (s)", | |
| kLeftMargin, kRightMargin); | |
| plot->SetSuggestedYAxis(0, 10, "Count per feedback", kBottomMargin, | |
| kTopMargin); | |
| } | |
| void CreateOutgoingEcnFeedbackGraph(const ParsedRtcEventLog& parsed_log, | |
| const AnalyzerConfig& config, | |
| Plot* plot) { | |
| CreateEcnFeedbackGraph(plot, kOutgoingPacket, parsed_log, config); | |
| plot->SetTitle("Outgoing ECN count per feedback"); | |
| } | |
| void CreateIncomingEcnFeedbackGraph(const ParsedRtcEventLog& parsed_log, | |
| const AnalyzerConfig& config, | |
| Plot* plot) { | |
| CreateEcnFeedbackGraph(plot, kIncomingPacket, parsed_log, config); | |
| plot->SetTitle("Incoming ECN count per feedback"); | |
| } | |
| void CreateOutgoingLossRateGraph(const ParsedRtcEventLog& parsed_log, | |
| const AnalyzerConfig& config, | |
| Plot* plot) { | |
| struct PacketLossPerFeedback { | |
| Timestamp timestamp; // Time when this feedback was received. | |
| int num_packets_in_feedback = 0; // Includes lost packets. | |
| int num_lost_packets = 0; // In this specific feedback. | |
| int num_reordered_packets = 0; // Packets received in this feedback, but | |
| // was previously reported as lost. | |
| int num_missing_feedback = | |
| 0; // Packets missing feedback between this report and the previous. | |
| }; | |
| class LossFeedbackBuilder { | |
| public: | |
| void AddPacket(uint16_t sequence_number, TimeDelta arrival_time_delta) { | |
| last_unwrapped_sequence_number_ = | |
| sequence_number_unwrapper_.Unwrap(sequence_number); | |
| if (!first_sequence_number_.has_value()) { | |
| first_sequence_number_ = last_unwrapped_sequence_number_; | |
| } | |
| ++num_packets_; | |
| if (arrival_time_delta.IsInfinite()) { | |
| lost_sequence_numbers_.insert(last_unwrapped_sequence_number_); | |
| } else { | |
| num_reordered_packets_ += previous_lost_sequence_numbers_.count( | |
| last_unwrapped_sequence_number_); | |
| } | |
| } | |
| void Update(PacketLossPerFeedback& feedback) { | |
| feedback.num_packets_in_feedback += num_packets_; | |
| feedback.num_lost_packets += lost_sequence_numbers_.size(); | |
| feedback.num_reordered_packets += num_reordered_packets_; | |
| if (first_sequence_number_.has_value() && | |
| previous_feedback_highest_seq_number_.has_value()) { | |
| feedback.num_missing_feedback += | |
| *first_sequence_number_ - *previous_feedback_highest_seq_number_ - | |
| 1; | |
| } | |
| // Prepare for next feedback. | |
| first_sequence_number_ = std::nullopt; | |
| previous_lost_sequence_numbers_.insert(lost_sequence_numbers_.begin(), | |
| lost_sequence_numbers_.end()); | |
| previous_feedback_highest_seq_number_ = last_unwrapped_sequence_number_; | |
| lost_sequence_numbers_.clear(); | |
| num_reordered_packets_ = 0; | |
| num_packets_ = 0; | |
| } | |
| private: | |
| int64_t last_unwrapped_sequence_number_ = 0; | |
| int num_reordered_packets_ = 0; | |
| int num_packets_ = 0; | |
| std::optional<int64_t> first_sequence_number_; | |
| std::unordered_set<int64_t> lost_sequence_numbers_; | |
| std::unordered_set<int64_t> previous_lost_sequence_numbers_; | |
| std::optional<int64_t> previous_feedback_highest_seq_number_; | |
| RtpSequenceNumberUnwrapper sequence_number_unwrapper_; | |
| }; | |
| TimeSeries loss_rate_series("Loss rate (from packet feedback)", | |
| LineStyle::kLine, PointStyle::kHighlight); | |
| TimeSeries reordered_packets_between_feedback( | |
| "Ratio of reordered packets from last feedback", LineStyle::kLine, | |
| PointStyle::kHighlight); | |
| TimeSeries average_loss_rate_series("Average loss rate last 5s", | |
| LineStyle::kLine, PointStyle::kHighlight); | |
| TimeSeries missing_feedback_series("Missing feedback", LineStyle::kNone, | |
| PointStyle::kHighlight); | |
| std::vector<PacketLossPerFeedback> loss_per_feedback; | |
| if (!parsed_log.congestion_feedback(kIncomingPacket).empty()) { | |
| plot->SetTitle("Outgoing loss rate (from CCFB)"); | |
| std::map</*ssrc*/ uint32_t, LossFeedbackBuilder> per_ssrc_builder; | |
| for (const LoggedRtcpCongestionControlFeedback& feedback : | |
| parsed_log.congestion_feedback(kIncomingPacket)) { | |
| const rtcp::CongestionControlFeedback& transport_feedback = | |
| feedback.congestion_feedback; | |
| PacketLossPerFeedback packet_loss_per_feedback = { | |
| .timestamp = feedback.log_time()}; | |
| for (const rtcp::CongestionControlFeedback::PacketInfo& packet : | |
| transport_feedback.packets()) { | |
| per_ssrc_builder[packet.ssrc].AddPacket(packet.sequence_number, | |
| packet.arrival_time_offset); | |
| } | |
| for (auto& [ssrc, builder] : per_ssrc_builder) { | |
| builder.Update(packet_loss_per_feedback); | |
| } | |
| loss_per_feedback.push_back(packet_loss_per_feedback); | |
| } | |
| } else if (!parsed_log.transport_feedbacks(kIncomingPacket).empty()) { | |
| plot->SetTitle("Outgoing loss rate (from TWCC)"); | |
| LossFeedbackBuilder builder; | |
| for (const LoggedRtcpPacketTransportFeedback& feedback : | |
| parsed_log.transport_feedbacks(kIncomingPacket)) { | |
| feedback.transport_feedback.ForAllPackets( | |
| [&](uint16_t sequence_number, TimeDelta receive_time_delta) { | |
| builder.AddPacket(sequence_number, receive_time_delta); | |
| }); | |
| PacketLossPerFeedback packet_loss_per_feedback = { | |
| .timestamp = feedback.log_time()}; | |
| builder.Update(packet_loss_per_feedback); | |
| loss_per_feedback.push_back(packet_loss_per_feedback); | |
| } | |
| } | |
| PacketLossSummary window_summary; | |
| Timestamp last_observation_receive_time = Timestamp::Zero(); | |
| // Use loss based bwe 2 observation duration and observation window size. | |
| constexpr TimeDelta kObservationDuration = TimeDelta::Millis(250); | |
| constexpr uint32_t kObservationWindowSize = 20; | |
| std::deque<PacketLossSummary> observations; | |
| int previous_feedback_size = 0; | |
| for (const PacketLossPerFeedback& feedback : loss_per_feedback) { | |
| for (int64_t num = 0; num < feedback.num_missing_feedback; ++num) { | |
| missing_feedback_series.points.emplace_back( | |
| config.GetCallTimeSec(feedback.timestamp), 100 + num); | |
| } | |
| // Compute loss rate from the transport feedback. | |
| float loss_rate = static_cast<float>(feedback.num_lost_packets * 100.0 / | |
| feedback.num_packets_in_feedback); | |
| loss_rate_series.points.emplace_back( | |
| config.GetCallTimeSec(feedback.timestamp), loss_rate); | |
| float reordered_rate = | |
| previous_feedback_size == 0 | |
| ? 0 | |
| : static_cast<float>(feedback.num_reordered_packets * 100.0 / | |
| previous_feedback_size); | |
| previous_feedback_size = feedback.num_packets_in_feedback; | |
| reordered_packets_between_feedback.points.emplace_back( | |
| config.GetCallTimeSec(feedback.timestamp), reordered_rate); | |
| // Compute loss rate in a window of kObservationWindowSize. | |
| if (window_summary.num_packets == 0) { | |
| window_summary.base_time = feedback.timestamp; | |
| } | |
| window_summary.num_packets += feedback.num_packets_in_feedback; | |
| window_summary.num_lost_packets += | |
| feedback.num_lost_packets - feedback.num_reordered_packets; | |
| const Timestamp last_received_time = feedback.timestamp; | |
| const TimeDelta observation_duration = | |
| window_summary.base_time == Timestamp::Zero() | |
| ? TimeDelta::Zero() | |
| : last_received_time - window_summary.base_time; | |
| if (observation_duration > kObservationDuration) { | |
| last_observation_receive_time = last_received_time; | |
| observations.push_back(window_summary); | |
| if (observations.size() > kObservationWindowSize) { | |
| observations.pop_front(); | |
| } | |
| // Compute average loss rate in a number of windows. | |
| int total_packets = 0; | |
| int total_loss = 0; | |
| for (const auto& observation : observations) { | |
| total_loss += observation.num_lost_packets; | |
| total_packets += observation.num_packets; | |
| } | |
| if (total_packets > 0) { | |
| float average_loss_rate = total_loss * 100.0 / total_packets; | |
| average_loss_rate_series.points.emplace_back( | |
| config.GetCallTimeSec(feedback.timestamp), average_loss_rate); | |
| } else { | |
| average_loss_rate_series.points.emplace_back( | |
| config.GetCallTimeSec(feedback.timestamp), 0); | |
| } | |
| window_summary = PacketLossSummary(); | |
| } | |
| } | |
| // Add the data set to the plot. | |
| plot->AppendTimeSeriesIfNotEmpty(std::move(loss_rate_series)); | |
| plot->AppendTimeSeriesIfNotEmpty( | |
| std::move(reordered_packets_between_feedback)); | |
| plot->AppendTimeSeriesIfNotEmpty(std::move(average_loss_rate_series)); | |
| plot->AppendTimeSeriesIfNotEmpty(std::move(missing_feedback_series)); | |
| plot->SetXAxis(config.CallBeginTimeSec(), config.CallEndTimeSec(), "Time (s)", | |
| kLeftMargin, kRightMargin); | |
| plot->SetSuggestedYAxis(0, 100, "Loss rate (percent)", kBottomMargin, | |
| kTopMargin); | |
| } | |
| void CreateTimestampGraph(PacketDirection direction, | |
| const ParsedRtcEventLog& parsed_log, | |
| const AnalyzerConfig& config, | |
| Plot* plot) { | |
| for (const auto& stream : parsed_log.rtp_packets_by_ssrc(direction)) { | |
| TimeSeries rtp_timestamps( | |
| GetStreamName(parsed_log, direction, stream.ssrc) + " capture-time", | |
| LineStyle::kLine, PointStyle::kHighlight); | |
| for (const auto& packet : stream.packet_view) { | |
| float x = config.GetCallTimeSec(packet.log_time()); | |
| float y = packet.header.timestamp; | |
| rtp_timestamps.points.emplace_back(x, y); | |
| } | |
| plot->AppendTimeSeries(std::move(rtp_timestamps)); | |
| TimeSeries rtcp_timestamps( | |
| GetStreamName(parsed_log, direction, stream.ssrc) + | |
| " rtcp capture-time", | |
| LineStyle::kLine, PointStyle::kHighlight); | |
| // TODO(terelius): Why only sender reports? | |
| const auto& sender_reports = parsed_log.sender_reports(direction); | |
| for (const auto& rtcp : sender_reports) { | |
| if (rtcp.sr.sender_ssrc() != stream.ssrc) | |
| continue; | |
| float x = config.GetCallTimeSec(rtcp.log_time()); | |
| float y = rtcp.sr.rtp_timestamp(); | |
| rtcp_timestamps.points.emplace_back(x, y); | |
| } | |
| plot->AppendTimeSeriesIfNotEmpty(std::move(rtcp_timestamps)); | |
| } | |
| plot->SetXAxis(config.CallBeginTimeSec(), config.CallEndTimeSec(), "Time (s)", | |
| kLeftMargin, kRightMargin); | |
| plot->SetSuggestedYAxis(0, 1, "RTP timestamp", kBottomMargin, kTopMargin); | |
| plot->SetTitle(GetDirectionAsString(direction) + " timestamps"); | |
| } | |
| void CreateSenderAndReceiverReportPlot( | |
| PacketDirection direction, | |
| FunctionView<float(const rtcp::ReportBlock&)> fy, | |
| std::string title, | |
| std::string yaxis_label, | |
| const ParsedRtcEventLog& parsed_log, | |
| const AnalyzerConfig& config, | |
| Plot* plot) { | |
| std::map<uint32_t, TimeSeries> sr_reports_by_ssrc; | |
| const auto& sender_reports = parsed_log.sender_reports(direction); | |
| for (const auto& rtcp : sender_reports) { | |
| float x = config.GetCallTimeSec(rtcp.log_time()); | |
| uint32_t ssrc = rtcp.sr.sender_ssrc(); | |
| for (const auto& block : rtcp.sr.report_blocks()) { | |
| float y = fy(block); | |
| auto sr_report_it = sr_reports_by_ssrc.find(ssrc); | |
| bool inserted; | |
| if (sr_report_it == sr_reports_by_ssrc.end()) { | |
| std::tie(sr_report_it, inserted) = sr_reports_by_ssrc.emplace( | |
| ssrc, TimeSeries(GetStreamName(parsed_log, direction, ssrc) + | |
| " Sender Reports", | |
| LineStyle::kLine, PointStyle::kHighlight)); | |
| } | |
| sr_report_it->second.points.emplace_back(x, y); | |
| } | |
| } | |
| for (auto& kv : sr_reports_by_ssrc) { | |
| plot->AppendTimeSeries(std::move(kv.second)); | |
| } | |
| std::map<uint32_t, TimeSeries> rr_reports_by_ssrc; | |
| const auto& receiver_reports = parsed_log.receiver_reports(direction); | |
| for (const auto& rtcp : receiver_reports) { | |
| float x = config.GetCallTimeSec(rtcp.log_time()); | |
| uint32_t ssrc = rtcp.rr.sender_ssrc(); | |
| for (const auto& block : rtcp.rr.report_blocks()) { | |
| float y = fy(block); | |
| auto rr_report_it = rr_reports_by_ssrc.find(ssrc); | |
| bool inserted; | |
| if (rr_report_it == rr_reports_by_ssrc.end()) { | |
| std::tie(rr_report_it, inserted) = rr_reports_by_ssrc.emplace( | |
| ssrc, TimeSeries(GetStreamName(parsed_log, direction, ssrc) + | |
| " Receiver Reports", | |
| LineStyle::kLine, PointStyle::kHighlight)); | |
| } | |
| rr_report_it->second.points.emplace_back(x, y); | |
| } | |
| } | |
| for (auto& kv : rr_reports_by_ssrc) { | |
| plot->AppendTimeSeries(std::move(kv.second)); | |
| } | |
| plot->SetXAxis(config.CallBeginTimeSec(), config.CallEndTimeSec(), "Time (s)", | |
| kLeftMargin, kRightMargin); | |
| plot->SetSuggestedYAxis(0, 1, yaxis_label, kBottomMargin, kTopMargin); | |
| plot->SetTitle(title); | |
| } | |
| } // namespace webrtc |