|  | /* | 
|  | *  Copyright (c) 2016 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/event_log_visualizer/analyzer.h" | 
|  |  | 
|  | #include <algorithm> | 
|  | #include <cmath> | 
|  | #include <limits> | 
|  | #include <map> | 
|  | #include <sstream> | 
|  | #include <string> | 
|  | #include <utility> | 
|  |  | 
|  | #include "call/audio_receive_stream.h" | 
|  | #include "call/audio_send_stream.h" | 
|  | #include "call/call.h" | 
|  | #include "call/video_receive_stream.h" | 
|  | #include "call/video_send_stream.h" | 
|  | #include "common_types.h"  // NOLINT(build/include) | 
|  | #include "logging/rtc_event_log/rtc_stream_config.h" | 
|  | #include "modules/audio_coding/neteq/tools/audio_sink.h" | 
|  | #include "modules/audio_coding/neteq/tools/fake_decode_from_file.h" | 
|  | #include "modules/audio_coding/neteq/tools/neteq_delay_analyzer.h" | 
|  | #include "modules/audio_coding/neteq/tools/neteq_replacement_input.h" | 
|  | #include "modules/audio_coding/neteq/tools/neteq_test.h" | 
|  | #include "modules/audio_coding/neteq/tools/resample_input_audio_file.h" | 
|  | #include "modules/congestion_controller/acknowledged_bitrate_estimator.h" | 
|  | #include "modules/congestion_controller/bitrate_estimator.h" | 
|  | #include "modules/congestion_controller/delay_based_bwe.h" | 
|  | #include "modules/congestion_controller/include/receive_side_congestion_controller.h" | 
|  | #include "modules/congestion_controller/include/send_side_congestion_controller.h" | 
|  | #include "modules/include/module_common_types.h" | 
|  | #include "modules/pacing/packet_router.h" | 
|  | #include "modules/rtp_rtcp/include/rtp_rtcp.h" | 
|  | #include "modules/rtp_rtcp/include/rtp_rtcp_defines.h" | 
|  | #include "modules/rtp_rtcp/source/rtcp_packet/common_header.h" | 
|  | #include "modules/rtp_rtcp/source/rtcp_packet/receiver_report.h" | 
|  | #include "modules/rtp_rtcp/source/rtcp_packet/remb.h" | 
|  | #include "modules/rtp_rtcp/source/rtcp_packet/sender_report.h" | 
|  | #include "modules/rtp_rtcp/source/rtcp_packet/transport_feedback.h" | 
|  | #include "modules/rtp_rtcp/source/rtp_header_extensions.h" | 
|  | #include "modules/rtp_rtcp/source/rtp_utility.h" | 
|  | #include "rtc_base/checks.h" | 
|  | #include "rtc_base/format_macros.h" | 
|  | #include "rtc_base/logging.h" | 
|  | #include "rtc_base/numerics/sequence_number_util.h" | 
|  | #include "rtc_base/ptr_util.h" | 
|  | #include "rtc_base/rate_statistics.h" | 
|  |  | 
|  | #ifndef BWE_TEST_LOGGING_COMPILE_TIME_ENABLE | 
|  | #define BWE_TEST_LOGGING_COMPILE_TIME_ENABLE 0 | 
|  | #endif  // BWE_TEST_LOGGING_COMPILE_TIME_ENABLE | 
|  |  | 
|  | namespace webrtc { | 
|  | namespace plotting { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | const int kNumMicrosecsPerSec = 1000000; | 
|  |  | 
|  | void SortPacketFeedbackVector(std::vector<PacketFeedback>* vec) { | 
|  | auto pred = [](const PacketFeedback& packet_feedback) { | 
|  | return packet_feedback.arrival_time_ms == PacketFeedback::kNotReceived; | 
|  | }; | 
|  | vec->erase(std::remove_if(vec->begin(), vec->end(), pred), vec->end()); | 
|  | std::sort(vec->begin(), vec->end(), PacketFeedbackComparator()); | 
|  | } | 
|  |  | 
|  | std::string SsrcToString(uint32_t ssrc) { | 
|  | std::stringstream ss; | 
|  | ss << "SSRC " << ssrc; | 
|  | return ss.str(); | 
|  | } | 
|  |  | 
|  | // Checks whether an SSRC is contained in the list of desired SSRCs. | 
|  | // Note that an empty SSRC list matches every SSRC. | 
|  | bool MatchingSsrc(uint32_t ssrc, const std::vector<uint32_t>& desired_ssrc) { | 
|  | if (desired_ssrc.size() == 0) | 
|  | return true; | 
|  | return std::find(desired_ssrc.begin(), desired_ssrc.end(), ssrc) != | 
|  | desired_ssrc.end(); | 
|  | } | 
|  |  | 
|  | double AbsSendTimeToMicroseconds(int64_t abs_send_time) { | 
|  | // The timestamp is a fixed point representation with 6 bits for seconds | 
|  | // and 18 bits for fractions of a second. Thus, we divide by 2^18 to get the | 
|  | // time in seconds and then multiply by kNumMicrosecsPerSec to convert to | 
|  | // microseconds. | 
|  | static constexpr double kTimestampToMicroSec = | 
|  | static_cast<double>(kNumMicrosecsPerSec) / static_cast<double>(1ul << 18); | 
|  | return abs_send_time * kTimestampToMicroSec; | 
|  | } | 
|  |  | 
|  | // Computes the difference |later| - |earlier| where |later| and |earlier| | 
|  | // are counters that wrap at |modulus|. The difference is chosen to have the | 
|  | // least absolute value. For example if |modulus| is 8, then the difference will | 
|  | // be chosen in the range [-3, 4]. If |modulus| is 9, then the difference will | 
|  | // be in [-4, 4]. | 
|  | int64_t WrappingDifference(uint32_t later, uint32_t earlier, int64_t modulus) { | 
|  | RTC_DCHECK_LE(1, modulus); | 
|  | RTC_DCHECK_LT(later, modulus); | 
|  | RTC_DCHECK_LT(earlier, modulus); | 
|  | int64_t difference = | 
|  | static_cast<int64_t>(later) - static_cast<int64_t>(earlier); | 
|  | int64_t max_difference = modulus / 2; | 
|  | int64_t min_difference = max_difference - modulus + 1; | 
|  | if (difference > max_difference) { | 
|  | difference -= modulus; | 
|  | } | 
|  | if (difference < min_difference) { | 
|  | difference += modulus; | 
|  | } | 
|  | if (difference > max_difference / 2 || difference < min_difference / 2) { | 
|  | RTC_LOG(LS_WARNING) << "Difference between" << later << " and " << earlier | 
|  | << " expected to be in the range (" | 
|  | << min_difference / 2 << "," << max_difference / 2 | 
|  | << ") but is " << difference | 
|  | << ". Correct unwrapping is uncertain."; | 
|  | } | 
|  | return difference; | 
|  | } | 
|  |  | 
|  | // Return default values for header extensions, to use on streams without stored | 
|  | // mapping data. Currently this only applies to audio streams, since the mapping | 
|  | // is not stored in the event log. | 
|  | // TODO(ivoc): Remove this once this mapping is stored in the event log for | 
|  | //             audio streams. Tracking bug: webrtc:6399 | 
|  | webrtc::RtpHeaderExtensionMap GetDefaultHeaderExtensionMap() { | 
|  | webrtc::RtpHeaderExtensionMap default_map; | 
|  | default_map.Register<AudioLevel>(webrtc::RtpExtension::kAudioLevelDefaultId); | 
|  | default_map.Register<TransmissionOffset>( | 
|  | webrtc::RtpExtension::kTimestampOffsetDefaultId); | 
|  | default_map.Register<AbsoluteSendTime>( | 
|  | webrtc::RtpExtension::kAbsSendTimeDefaultId); | 
|  | default_map.Register<VideoOrientation>( | 
|  | webrtc::RtpExtension::kVideoRotationDefaultId); | 
|  | default_map.Register<VideoContentTypeExtension>( | 
|  | webrtc::RtpExtension::kVideoContentTypeDefaultId); | 
|  | default_map.Register<VideoTimingExtension>( | 
|  | webrtc::RtpExtension::kVideoTimingDefaultId); | 
|  | default_map.Register<TransportSequenceNumber>( | 
|  | webrtc::RtpExtension::kTransportSequenceNumberDefaultId); | 
|  | default_map.Register<PlayoutDelayLimits>( | 
|  | webrtc::RtpExtension::kPlayoutDelayDefaultId); | 
|  | return default_map; | 
|  | } | 
|  |  | 
|  | constexpr float kLeftMargin = 0.01f; | 
|  | constexpr float kRightMargin = 0.02f; | 
|  | constexpr float kBottomMargin = 0.02f; | 
|  | constexpr float kTopMargin = 0.05f; | 
|  |  | 
|  | rtc::Optional<double> NetworkDelayDiff_AbsSendTime( | 
|  | const LoggedRtpPacket& old_packet, | 
|  | const LoggedRtpPacket& new_packet) { | 
|  | if (old_packet.header.extension.hasAbsoluteSendTime && | 
|  | new_packet.header.extension.hasAbsoluteSendTime) { | 
|  | int64_t send_time_diff = WrappingDifference( | 
|  | new_packet.header.extension.absoluteSendTime, | 
|  | old_packet.header.extension.absoluteSendTime, 1ul << 24); | 
|  | int64_t recv_time_diff = new_packet.timestamp - old_packet.timestamp; | 
|  | double delay_change_us = | 
|  | recv_time_diff - AbsSendTimeToMicroseconds(send_time_diff); | 
|  | return delay_change_us / 1000; | 
|  | } else { | 
|  | return rtc::nullopt; | 
|  | } | 
|  | } | 
|  |  | 
|  | rtc::Optional<double> NetworkDelayDiff_CaptureTime( | 
|  | const LoggedRtpPacket& old_packet, | 
|  | const LoggedRtpPacket& new_packet) { | 
|  | int64_t send_time_diff = WrappingDifference( | 
|  | new_packet.header.timestamp, old_packet.header.timestamp, 1ull << 32); | 
|  | int64_t recv_time_diff = new_packet.timestamp - old_packet.timestamp; | 
|  |  | 
|  | const double kVideoSampleRate = 90000; | 
|  | // TODO(terelius): We treat all streams as video for now, even though | 
|  | // audio might be sampled at e.g. 16kHz, because it is really difficult to | 
|  | // figure out the true sampling rate of a stream. The effect is that the | 
|  | // delay will be scaled incorrectly for non-video streams. | 
|  |  | 
|  | double delay_change = | 
|  | static_cast<double>(recv_time_diff) / 1000 - | 
|  | static_cast<double>(send_time_diff) / kVideoSampleRate * 1000; | 
|  | if (delay_change < -10000 || 10000 < delay_change) { | 
|  | RTC_LOG(LS_WARNING) << "Very large delay change. Timestamps correct?"; | 
|  | RTC_LOG(LS_WARNING) << "Old capture time " << old_packet.header.timestamp | 
|  | << ", received time " << old_packet.timestamp; | 
|  | RTC_LOG(LS_WARNING) << "New capture time " << new_packet.header.timestamp | 
|  | << ", received time " << new_packet.timestamp; | 
|  | RTC_LOG(LS_WARNING) << "Receive time difference " << recv_time_diff << " = " | 
|  | << static_cast<double>(recv_time_diff) / | 
|  | kNumMicrosecsPerSec | 
|  | << "s"; | 
|  | RTC_LOG(LS_WARNING) << "Send time difference " << send_time_diff << " = " | 
|  | << static_cast<double>(send_time_diff) / | 
|  | kVideoSampleRate | 
|  | << "s"; | 
|  | } | 
|  | return delay_change; | 
|  | } | 
|  |  | 
|  | // For each element in data, use |get_y()| to extract a y-coordinate and | 
|  | // store the result in a TimeSeries. | 
|  | template <typename DataType> | 
|  | void ProcessPoints( | 
|  | rtc::FunctionView<rtc::Optional<float>(const DataType&)> get_y, | 
|  | const std::vector<DataType>& data, | 
|  | uint64_t begin_time, | 
|  | TimeSeries* result) { | 
|  | for (size_t i = 0; i < data.size(); i++) { | 
|  | float x = static_cast<float>(data[i].timestamp - begin_time) / | 
|  | kNumMicrosecsPerSec; | 
|  | rtc::Optional<float> y = get_y(data[i]); | 
|  | if (y) | 
|  | result->points.emplace_back(x, *y); | 
|  | } | 
|  | } | 
|  |  | 
|  | // For each pair of adjacent elements in |data|, use |get_y| to extract a | 
|  | // y-coordinate and store the result in a TimeSeries. Note that the x-coordinate | 
|  | // will be the time of the second element in the pair. | 
|  | template <typename DataType, typename ResultType> | 
|  | void ProcessPairs( | 
|  | rtc::FunctionView<rtc::Optional<ResultType>(const DataType&, | 
|  | const DataType&)> get_y, | 
|  | const std::vector<DataType>& data, | 
|  | uint64_t begin_time, | 
|  | TimeSeries* result) { | 
|  | for (size_t i = 1; i < data.size(); i++) { | 
|  | float x = static_cast<float>(data[i].timestamp - begin_time) / | 
|  | kNumMicrosecsPerSec; | 
|  | rtc::Optional<ResultType> y = get_y(data[i - 1], data[i]); | 
|  | if (y) | 
|  | result->points.emplace_back(x, static_cast<float>(*y)); | 
|  | } | 
|  | } | 
|  |  | 
|  | // For each element in data, use |extract()| to extract a y-coordinate and | 
|  | // store the result in a TimeSeries. | 
|  | template <typename DataType, typename ResultType> | 
|  | void AccumulatePoints( | 
|  | rtc::FunctionView<rtc::Optional<ResultType>(const DataType&)> extract, | 
|  | const std::vector<DataType>& data, | 
|  | uint64_t begin_time, | 
|  | TimeSeries* result) { | 
|  | ResultType sum = 0; | 
|  | for (size_t i = 0; i < data.size(); i++) { | 
|  | float x = static_cast<float>(data[i].timestamp - begin_time) / | 
|  | kNumMicrosecsPerSec; | 
|  | rtc::Optional<ResultType> y = extract(data[i]); | 
|  | if (y) { | 
|  | sum += *y; | 
|  | result->points.emplace_back(x, static_cast<float>(sum)); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // For each pair of adjacent elements in |data|, use |extract()| to extract a | 
|  | // y-coordinate and store the result in a TimeSeries. Note that the x-coordinate | 
|  | // will be the time of the second element in the pair. | 
|  | template <typename DataType, typename ResultType> | 
|  | void AccumulatePairs( | 
|  | rtc::FunctionView<rtc::Optional<ResultType>(const DataType&, | 
|  | const DataType&)> extract, | 
|  | const std::vector<DataType>& data, | 
|  | uint64_t begin_time, | 
|  | TimeSeries* result) { | 
|  | ResultType sum = 0; | 
|  | for (size_t i = 1; i < data.size(); i++) { | 
|  | float x = static_cast<float>(data[i].timestamp - begin_time) / | 
|  | kNumMicrosecsPerSec; | 
|  | rtc::Optional<ResultType> y = extract(data[i - 1], data[i]); | 
|  | if (y) | 
|  | sum += *y; | 
|  | result->points.emplace_back(x, static_cast<float>(sum)); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Calculates a moving average of |data| and stores the result in a TimeSeries. | 
|  | // A data point is generated every |step| microseconds from |begin_time| | 
|  | // to |end_time|. The value of each data point is the average of the data | 
|  | // during the preceeding |window_duration_us| microseconds. | 
|  | template <typename DataType, typename ResultType> | 
|  | void MovingAverage( | 
|  | rtc::FunctionView<rtc::Optional<ResultType>(const DataType&)> extract, | 
|  | const std::vector<DataType>& data, | 
|  | uint64_t begin_time, | 
|  | uint64_t end_time, | 
|  | uint64_t window_duration_us, | 
|  | uint64_t step, | 
|  | webrtc::plotting::TimeSeries* result) { | 
|  | size_t window_index_begin = 0; | 
|  | size_t window_index_end = 0; | 
|  | ResultType sum_in_window = 0; | 
|  |  | 
|  | for (uint64_t t = begin_time; t < end_time + step; t += step) { | 
|  | while (window_index_end < data.size() && | 
|  | data[window_index_end].timestamp < t) { | 
|  | rtc::Optional<ResultType> value = extract(data[window_index_end]); | 
|  | if (value) | 
|  | sum_in_window += *value; | 
|  | ++window_index_end; | 
|  | } | 
|  | while (window_index_begin < data.size() && | 
|  | data[window_index_begin].timestamp < t - window_duration_us) { | 
|  | rtc::Optional<ResultType> value = extract(data[window_index_begin]); | 
|  | if (value) | 
|  | sum_in_window -= *value; | 
|  | ++window_index_begin; | 
|  | } | 
|  | float window_duration_s = | 
|  | static_cast<float>(window_duration_us) / kNumMicrosecsPerSec; | 
|  | float x = static_cast<float>(t - begin_time) / kNumMicrosecsPerSec; | 
|  | float y = sum_in_window / window_duration_s; | 
|  | result->points.emplace_back(x, y); | 
|  | } | 
|  | } | 
|  |  | 
|  | const char kUnknownEnumValue[] = "unknown"; | 
|  |  | 
|  | const char kIceCandidateTypeLocal[] = "local"; | 
|  | const char kIceCandidateTypeStun[] = "stun"; | 
|  | const char kIceCandidateTypePrflx[] = "prflx"; | 
|  | const char kIceCandidateTypeRelay[] = "relay"; | 
|  |  | 
|  | const char kProtocolUdp[] = "udp"; | 
|  | const char kProtocolTcp[] = "tcp"; | 
|  | const char kProtocolSsltcp[] = "ssltcp"; | 
|  | const char kProtocolTls[] = "tls"; | 
|  |  | 
|  | const char kAddressFamilyIpv4[] = "ipv4"; | 
|  | const char kAddressFamilyIpv6[] = "ipv6"; | 
|  |  | 
|  | const char kNetworkTypeEthernet[] = "ethernet"; | 
|  | const char kNetworkTypeLoopback[] = "loopback"; | 
|  | const char kNetworkTypeWifi[] = "wifi"; | 
|  | const char kNetworkTypeVpn[] = "vpn"; | 
|  | const char kNetworkTypeCellular[] = "cellular"; | 
|  |  | 
|  | std::string GetIceCandidateTypeAsString(webrtc::IceCandidateType type) { | 
|  | switch (type) { | 
|  | case webrtc::IceCandidateType::kLocal: | 
|  | return kIceCandidateTypeLocal; | 
|  | case webrtc::IceCandidateType::kStun: | 
|  | return kIceCandidateTypeStun; | 
|  | case webrtc::IceCandidateType::kPrflx: | 
|  | return kIceCandidateTypePrflx; | 
|  | case webrtc::IceCandidateType::kRelay: | 
|  | return kIceCandidateTypeRelay; | 
|  | default: | 
|  | return kUnknownEnumValue; | 
|  | } | 
|  | } | 
|  |  | 
|  | std::string GetProtocolAsString(webrtc::IceCandidatePairProtocol protocol) { | 
|  | switch (protocol) { | 
|  | case webrtc::IceCandidatePairProtocol::kUdp: | 
|  | return kProtocolUdp; | 
|  | case webrtc::IceCandidatePairProtocol::kTcp: | 
|  | return kProtocolTcp; | 
|  | case webrtc::IceCandidatePairProtocol::kSsltcp: | 
|  | return kProtocolSsltcp; | 
|  | case webrtc::IceCandidatePairProtocol::kTls: | 
|  | return kProtocolTls; | 
|  | default: | 
|  | return kUnknownEnumValue; | 
|  | } | 
|  | } | 
|  |  | 
|  | std::string GetAddressFamilyAsString( | 
|  | webrtc::IceCandidatePairAddressFamily family) { | 
|  | switch (family) { | 
|  | case webrtc::IceCandidatePairAddressFamily::kIpv4: | 
|  | return kAddressFamilyIpv4; | 
|  | case webrtc::IceCandidatePairAddressFamily::kIpv6: | 
|  | return kAddressFamilyIpv6; | 
|  | default: | 
|  | return kUnknownEnumValue; | 
|  | } | 
|  | } | 
|  |  | 
|  | std::string GetNetworkTypeAsString(webrtc::IceCandidateNetworkType type) { | 
|  | switch (type) { | 
|  | case webrtc::IceCandidateNetworkType::kEthernet: | 
|  | return kNetworkTypeEthernet; | 
|  | case webrtc::IceCandidateNetworkType::kLoopback: | 
|  | return kNetworkTypeLoopback; | 
|  | case webrtc::IceCandidateNetworkType::kWifi: | 
|  | return kNetworkTypeWifi; | 
|  | case webrtc::IceCandidateNetworkType::kVpn: | 
|  | return kNetworkTypeVpn; | 
|  | case webrtc::IceCandidateNetworkType::kCellular: | 
|  | return kNetworkTypeCellular; | 
|  | default: | 
|  | return kUnknownEnumValue; | 
|  | } | 
|  | } | 
|  |  | 
|  | std::string GetCandidatePairLogDescriptionAsString( | 
|  | const ParsedRtcEventLog::IceCandidatePairConfig& config) { | 
|  | // Example: stun:wifi->relay(tcp):cellular@udp:ipv4 | 
|  | // represents a pair of a local server-reflexive candidate on a WiFi network | 
|  | // and a remote relay candidate using TCP as the relay protocol on a cell | 
|  | // network, when the candidate pair communicates over UDP using IPv4. | 
|  | std::stringstream ss; | 
|  | std::string local_candidate_type = | 
|  | GetIceCandidateTypeAsString(config.local_candidate_type); | 
|  | std::string remote_candidate_type = | 
|  | GetIceCandidateTypeAsString(config.remote_candidate_type); | 
|  | if (config.local_candidate_type == webrtc::IceCandidateType::kRelay) { | 
|  | local_candidate_type += | 
|  | "(" + GetProtocolAsString(config.local_relay_protocol) + ")"; | 
|  | } | 
|  | ss << local_candidate_type << ":" | 
|  | << GetNetworkTypeAsString(config.local_network_type) << ":" | 
|  | << GetAddressFamilyAsString(config.local_address_family) << "->" | 
|  | << remote_candidate_type << ":" | 
|  | << GetAddressFamilyAsString(config.remote_address_family) << "@" | 
|  | << GetProtocolAsString(config.candidate_pair_protocol); | 
|  | return ss.str(); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | EventLogAnalyzer::EventLogAnalyzer(const ParsedRtcEventLog& log) | 
|  | : parsed_log_(log), window_duration_(250000), step_(10000) { | 
|  | uint64_t first_timestamp = std::numeric_limits<uint64_t>::max(); | 
|  | uint64_t last_timestamp = std::numeric_limits<uint64_t>::min(); | 
|  |  | 
|  | PacketDirection direction; | 
|  | uint8_t header[IP_PACKET_SIZE]; | 
|  | size_t header_length; | 
|  | size_t total_length; | 
|  |  | 
|  | uint8_t last_incoming_rtcp_packet[IP_PACKET_SIZE]; | 
|  | uint8_t last_incoming_rtcp_packet_length = 0; | 
|  |  | 
|  | // Make a default extension map for streams without configuration information. | 
|  | // TODO(ivoc): Once configuration of audio streams is stored in the event log, | 
|  | //             this can be removed. Tracking bug: webrtc:6399 | 
|  | RtpHeaderExtensionMap default_extension_map = GetDefaultHeaderExtensionMap(); | 
|  |  | 
|  | rtc::Optional<uint64_t> last_log_start; | 
|  |  | 
|  | for (size_t i = 0; i < parsed_log_.GetNumberOfEvents(); i++) { | 
|  | ParsedRtcEventLog::EventType event_type = parsed_log_.GetEventType(i); | 
|  | if (event_type != ParsedRtcEventLog::VIDEO_RECEIVER_CONFIG_EVENT && | 
|  | event_type != ParsedRtcEventLog::VIDEO_SENDER_CONFIG_EVENT && | 
|  | event_type != ParsedRtcEventLog::AUDIO_RECEIVER_CONFIG_EVENT && | 
|  | event_type != ParsedRtcEventLog::AUDIO_SENDER_CONFIG_EVENT && | 
|  | event_type != ParsedRtcEventLog::LOG_START && | 
|  | event_type != ParsedRtcEventLog::LOG_END) { | 
|  | uint64_t timestamp = parsed_log_.GetTimestamp(i); | 
|  | first_timestamp = std::min(first_timestamp, timestamp); | 
|  | last_timestamp = std::max(last_timestamp, timestamp); | 
|  | } | 
|  |  | 
|  | switch (parsed_log_.GetEventType(i)) { | 
|  | case ParsedRtcEventLog::VIDEO_RECEIVER_CONFIG_EVENT: { | 
|  | rtclog::StreamConfig config = parsed_log_.GetVideoReceiveConfig(i); | 
|  | StreamId stream(config.remote_ssrc, kIncomingPacket); | 
|  | video_ssrcs_.insert(stream); | 
|  | StreamId rtx_stream(config.rtx_ssrc, kIncomingPacket); | 
|  | video_ssrcs_.insert(rtx_stream); | 
|  | rtx_ssrcs_.insert(rtx_stream); | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::VIDEO_SENDER_CONFIG_EVENT: { | 
|  | std::vector<rtclog::StreamConfig> configs = | 
|  | parsed_log_.GetVideoSendConfig(i); | 
|  | for (const auto& config : configs) { | 
|  | StreamId stream(config.local_ssrc, kOutgoingPacket); | 
|  | video_ssrcs_.insert(stream); | 
|  | StreamId rtx_stream(config.rtx_ssrc, kOutgoingPacket); | 
|  | video_ssrcs_.insert(rtx_stream); | 
|  | rtx_ssrcs_.insert(rtx_stream); | 
|  | } | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::AUDIO_RECEIVER_CONFIG_EVENT: { | 
|  | rtclog::StreamConfig config = parsed_log_.GetAudioReceiveConfig(i); | 
|  | StreamId stream(config.remote_ssrc, kIncomingPacket); | 
|  | audio_ssrcs_.insert(stream); | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::AUDIO_SENDER_CONFIG_EVENT: { | 
|  | rtclog::StreamConfig config = parsed_log_.GetAudioSendConfig(i); | 
|  | StreamId stream(config.local_ssrc, kOutgoingPacket); | 
|  | audio_ssrcs_.insert(stream); | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::RTP_EVENT: { | 
|  | RtpHeaderExtensionMap* extension_map = parsed_log_.GetRtpHeader( | 
|  | i, &direction, header, &header_length, &total_length, nullptr); | 
|  | RtpUtility::RtpHeaderParser rtp_parser(header, header_length); | 
|  | RTPHeader parsed_header; | 
|  | if (extension_map != nullptr) { | 
|  | rtp_parser.Parse(&parsed_header, extension_map); | 
|  | } else { | 
|  | // Use the default extension map. | 
|  | // TODO(ivoc): Once configuration of audio streams is stored in the | 
|  | //             event log, this can be removed. | 
|  | //             Tracking bug: webrtc:6399 | 
|  | rtp_parser.Parse(&parsed_header, &default_extension_map); | 
|  | } | 
|  | uint64_t timestamp = parsed_log_.GetTimestamp(i); | 
|  | StreamId stream(parsed_header.ssrc, direction); | 
|  | rtp_packets_[stream].push_back( | 
|  | LoggedRtpPacket(timestamp, parsed_header, total_length)); | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::RTCP_EVENT: { | 
|  | uint8_t packet[IP_PACKET_SIZE]; | 
|  | parsed_log_.GetRtcpPacket(i, &direction, packet, &total_length); | 
|  | // Currently incoming RTCP packets are logged twice, both for audio and | 
|  | // video. Only act on one of them. Compare against the previous parsed | 
|  | // incoming RTCP packet. | 
|  | if (direction == webrtc::kIncomingPacket) { | 
|  | RTC_CHECK_LE(total_length, IP_PACKET_SIZE); | 
|  | if (total_length == last_incoming_rtcp_packet_length && | 
|  | memcmp(last_incoming_rtcp_packet, packet, total_length) == 0) { | 
|  | continue; | 
|  | } else { | 
|  | memcpy(last_incoming_rtcp_packet, packet, total_length); | 
|  | last_incoming_rtcp_packet_length = total_length; | 
|  | } | 
|  | } | 
|  | rtcp::CommonHeader header; | 
|  | const uint8_t* packet_end = packet + total_length; | 
|  | for (const uint8_t* block = packet; block < packet_end; | 
|  | block = header.NextPacket()) { | 
|  | RTC_CHECK(header.Parse(block, packet_end - block)); | 
|  | if (header.type() == rtcp::TransportFeedback::kPacketType && | 
|  | header.fmt() == rtcp::TransportFeedback::kFeedbackMessageType) { | 
|  | std::unique_ptr<rtcp::TransportFeedback> rtcp_packet( | 
|  | rtc::MakeUnique<rtcp::TransportFeedback>()); | 
|  | if (rtcp_packet->Parse(header)) { | 
|  | uint32_t ssrc = rtcp_packet->sender_ssrc(); | 
|  | StreamId stream(ssrc, direction); | 
|  | uint64_t timestamp = parsed_log_.GetTimestamp(i); | 
|  | rtcp_packets_[stream].push_back(LoggedRtcpPacket( | 
|  | timestamp, kRtcpTransportFeedback, std::move(rtcp_packet))); | 
|  | } | 
|  | } else if (header.type() == rtcp::SenderReport::kPacketType) { | 
|  | std::unique_ptr<rtcp::SenderReport> rtcp_packet( | 
|  | rtc::MakeUnique<rtcp::SenderReport>()); | 
|  | if (rtcp_packet->Parse(header)) { | 
|  | uint32_t ssrc = rtcp_packet->sender_ssrc(); | 
|  | StreamId stream(ssrc, direction); | 
|  | uint64_t timestamp = parsed_log_.GetTimestamp(i); | 
|  | rtcp_packets_[stream].push_back( | 
|  | LoggedRtcpPacket(timestamp, kRtcpSr, std::move(rtcp_packet))); | 
|  | } | 
|  | } else if (header.type() == rtcp::ReceiverReport::kPacketType) { | 
|  | std::unique_ptr<rtcp::ReceiverReport> rtcp_packet( | 
|  | rtc::MakeUnique<rtcp::ReceiverReport>()); | 
|  | if (rtcp_packet->Parse(header)) { | 
|  | uint32_t ssrc = rtcp_packet->sender_ssrc(); | 
|  | StreamId stream(ssrc, direction); | 
|  | uint64_t timestamp = parsed_log_.GetTimestamp(i); | 
|  | rtcp_packets_[stream].push_back( | 
|  | LoggedRtcpPacket(timestamp, kRtcpRr, std::move(rtcp_packet))); | 
|  | } | 
|  | } else if (header.type() == rtcp::Remb::kPacketType && | 
|  | header.fmt() == rtcp::Remb::kFeedbackMessageType) { | 
|  | std::unique_ptr<rtcp::Remb> rtcp_packet( | 
|  | rtc::MakeUnique<rtcp::Remb>()); | 
|  | if (rtcp_packet->Parse(header)) { | 
|  | uint32_t ssrc = rtcp_packet->sender_ssrc(); | 
|  | StreamId stream(ssrc, direction); | 
|  | uint64_t timestamp = parsed_log_.GetTimestamp(i); | 
|  | rtcp_packets_[stream].push_back(LoggedRtcpPacket( | 
|  | timestamp, kRtcpRemb, std::move(rtcp_packet))); | 
|  | } | 
|  | } | 
|  | } | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::LOG_START: { | 
|  | if (last_log_start) { | 
|  | // A LOG_END event was missing. Use last_timestamp. | 
|  | RTC_DCHECK_GE(last_timestamp, *last_log_start); | 
|  | log_segments_.push_back( | 
|  | std::make_pair(*last_log_start, last_timestamp)); | 
|  | } | 
|  | last_log_start = parsed_log_.GetTimestamp(i); | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::LOG_END: { | 
|  | RTC_DCHECK(last_log_start); | 
|  | log_segments_.push_back( | 
|  | std::make_pair(*last_log_start, parsed_log_.GetTimestamp(i))); | 
|  | last_log_start.reset(); | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::AUDIO_PLAYOUT_EVENT: { | 
|  | uint32_t this_ssrc; | 
|  | parsed_log_.GetAudioPlayout(i, &this_ssrc); | 
|  | audio_playout_events_[this_ssrc].push_back(parsed_log_.GetTimestamp(i)); | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::LOSS_BASED_BWE_UPDATE: { | 
|  | LossBasedBweUpdate bwe_update; | 
|  | bwe_update.timestamp = parsed_log_.GetTimestamp(i); | 
|  | parsed_log_.GetLossBasedBweUpdate(i, &bwe_update.new_bitrate, | 
|  | &bwe_update.fraction_loss, | 
|  | &bwe_update.expected_packets); | 
|  | bwe_loss_updates_.push_back(bwe_update); | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::DELAY_BASED_BWE_UPDATE: { | 
|  | bwe_delay_updates_.push_back(parsed_log_.GetDelayBasedBweUpdate(i)); | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::AUDIO_NETWORK_ADAPTATION_EVENT: { | 
|  | AudioNetworkAdaptationEvent ana_event; | 
|  | ana_event.timestamp = parsed_log_.GetTimestamp(i); | 
|  | parsed_log_.GetAudioNetworkAdaptation(i, &ana_event.config); | 
|  | audio_network_adaptation_events_.push_back(ana_event); | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::BWE_PROBE_CLUSTER_CREATED_EVENT: { | 
|  | bwe_probe_cluster_created_events_.push_back( | 
|  | parsed_log_.GetBweProbeClusterCreated(i)); | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::BWE_PROBE_RESULT_EVENT: { | 
|  | bwe_probe_result_events_.push_back(parsed_log_.GetBweProbeResult(i)); | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::ALR_STATE_EVENT: { | 
|  | alr_state_events_.push_back(parsed_log_.GetAlrState(i)); | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::ICE_CANDIDATE_PAIR_CONFIG: { | 
|  | ice_candidate_pair_configs_.push_back( | 
|  | parsed_log_.GetIceCandidatePairConfig(i)); | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::ICE_CANDIDATE_PAIR_EVENT: { | 
|  | ice_candidate_pair_events_.push_back( | 
|  | parsed_log_.GetIceCandidatePairEvent(i)); | 
|  | break; | 
|  | } | 
|  | case ParsedRtcEventLog::UNKNOWN_EVENT: { | 
|  | break; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | if (last_timestamp < first_timestamp) { | 
|  | // No useful events in the log. | 
|  | first_timestamp = last_timestamp = 0; | 
|  | } | 
|  | begin_time_ = first_timestamp; | 
|  | end_time_ = last_timestamp; | 
|  | call_duration_s_ = ToCallTime(end_time_); | 
|  | if (last_log_start) { | 
|  | // The log was missing the last LOG_END event. Fake it. | 
|  | log_segments_.push_back(std::make_pair(*last_log_start, end_time_)); | 
|  | } | 
|  | RTC_LOG(LS_INFO) << "Found " << log_segments_.size() | 
|  | << " (LOG_START, LOG_END) segments in log."; | 
|  | } | 
|  |  | 
|  | class BitrateObserver : public NetworkChangedObserver, | 
|  | public RemoteBitrateObserver { | 
|  | public: | 
|  | BitrateObserver() : last_bitrate_bps_(0), bitrate_updated_(false) {} | 
|  |  | 
|  | void OnNetworkChanged(uint32_t bitrate_bps, | 
|  | uint8_t fraction_loss, | 
|  | int64_t rtt_ms, | 
|  | int64_t probing_interval_ms) override { | 
|  | last_bitrate_bps_ = bitrate_bps; | 
|  | bitrate_updated_ = true; | 
|  | } | 
|  |  | 
|  | void OnReceiveBitrateChanged(const std::vector<uint32_t>& ssrcs, | 
|  | uint32_t bitrate) override {} | 
|  |  | 
|  | uint32_t last_bitrate_bps() const { return last_bitrate_bps_; } | 
|  | bool GetAndResetBitrateUpdated() { | 
|  | bool bitrate_updated = bitrate_updated_; | 
|  | bitrate_updated_ = false; | 
|  | return bitrate_updated; | 
|  | } | 
|  |  | 
|  | private: | 
|  | uint32_t last_bitrate_bps_; | 
|  | bool bitrate_updated_; | 
|  | }; | 
|  |  | 
|  | bool EventLogAnalyzer::IsRtxSsrc(StreamId stream_id) const { | 
|  | return rtx_ssrcs_.count(stream_id) == 1; | 
|  | } | 
|  |  | 
|  | bool EventLogAnalyzer::IsVideoSsrc(StreamId stream_id) const { | 
|  | return video_ssrcs_.count(stream_id) == 1; | 
|  | } | 
|  |  | 
|  | bool EventLogAnalyzer::IsAudioSsrc(StreamId stream_id) const { | 
|  | return audio_ssrcs_.count(stream_id) == 1; | 
|  | } | 
|  |  | 
|  | std::string EventLogAnalyzer::GetStreamName(StreamId stream_id) const { | 
|  | std::stringstream name; | 
|  | if (IsAudioSsrc(stream_id)) { | 
|  | name << "Audio "; | 
|  | } else if (IsVideoSsrc(stream_id)) { | 
|  | name << "Video "; | 
|  | } else { | 
|  | name << "Unknown "; | 
|  | } | 
|  | if (IsRtxSsrc(stream_id)) | 
|  | name << "RTX "; | 
|  | if (stream_id.GetDirection() == kIncomingPacket) { | 
|  | name << "(In) "; | 
|  | } else { | 
|  | name << "(Out) "; | 
|  | } | 
|  | name << SsrcToString(stream_id.GetSsrc()); | 
|  | return name.str(); | 
|  | } | 
|  |  | 
|  | // This is much more reliable for outgoing streams than for incoming streams. | 
|  | rtc::Optional<uint32_t> EventLogAnalyzer::EstimateRtpClockFrequency( | 
|  | const std::vector<LoggedRtpPacket>& packets) const { | 
|  | RTC_CHECK(packets.size() >= 2); | 
|  | uint64_t end_time_us = log_segments_.empty() | 
|  | ? std::numeric_limits<uint64_t>::max() | 
|  | : log_segments_.front().second; | 
|  | SeqNumUnwrapper<uint32_t> unwrapper; | 
|  | uint64_t first_rtp_timestamp = unwrapper.Unwrap(packets[0].header.timestamp); | 
|  | uint64_t first_log_timestamp = packets[0].timestamp; | 
|  | uint64_t last_rtp_timestamp = first_rtp_timestamp; | 
|  | uint64_t last_log_timestamp = first_log_timestamp; | 
|  | for (size_t i = 1; i < packets.size(); i++) { | 
|  | if (packets[i].timestamp > end_time_us) | 
|  | break; | 
|  | last_rtp_timestamp = unwrapper.Unwrap(packets[i].header.timestamp); | 
|  | last_log_timestamp = packets[i].timestamp; | 
|  | } | 
|  | if (last_log_timestamp - first_log_timestamp < kNumMicrosecsPerSec) { | 
|  | RTC_LOG(LS_WARNING) | 
|  | << "Failed to estimate RTP clock frequency: Stream too short. (" | 
|  | << packets.size() << " packets, " | 
|  | << last_log_timestamp - first_log_timestamp << " us)"; | 
|  | return rtc::nullopt; | 
|  | } | 
|  | double duration = | 
|  | static_cast<double>(last_log_timestamp - first_log_timestamp) / | 
|  | kNumMicrosecsPerSec; | 
|  | double estimated_frequency = | 
|  | (last_rtp_timestamp - first_rtp_timestamp) / duration; | 
|  | for (uint32_t f : {8000, 16000, 32000, 48000, 90000}) { | 
|  | if (std::fabs(estimated_frequency - f) < 0.05 * f) { | 
|  | return f; | 
|  | } | 
|  | } | 
|  | RTC_LOG(LS_WARNING) << "Failed to estimate RTP clock frequency: Estimate " | 
|  | << estimated_frequency | 
|  | << "not close to any stardard RTP frequency."; | 
|  | return rtc::nullopt; | 
|  | } | 
|  |  | 
|  | float EventLogAnalyzer::ToCallTime(int64_t timestamp) const { | 
|  | return static_cast<float>(timestamp - begin_time_) / kNumMicrosecsPerSec; | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreatePacketGraph(PacketDirection desired_direction, | 
|  | Plot* plot) { | 
|  | for (auto& kv : rtp_packets_) { | 
|  | StreamId stream_id = kv.first; | 
|  | const std::vector<LoggedRtpPacket>& packet_stream = kv.second; | 
|  | // Filter on direction and SSRC. | 
|  | if (stream_id.GetDirection() != desired_direction || | 
|  | !MatchingSsrc(stream_id.GetSsrc(), desired_ssrc_)) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | TimeSeries time_series(GetStreamName(stream_id), LineStyle::kBar); | 
|  | ProcessPoints<LoggedRtpPacket>( | 
|  | [](const LoggedRtpPacket& packet) { | 
|  | return rtc::Optional<float>(packet.total_length); | 
|  | }, | 
|  | packet_stream, begin_time_, &time_series); | 
|  | plot->AppendTimeSeries(std::move(time_series)); | 
|  | } | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 1, "Packet size (bytes)", kBottomMargin, | 
|  | kTopMargin); | 
|  | if (desired_direction == webrtc::PacketDirection::kIncomingPacket) { | 
|  | plot->SetTitle("Incoming RTP packets"); | 
|  | } else if (desired_direction == webrtc::PacketDirection::kOutgoingPacket) { | 
|  | plot->SetTitle("Outgoing RTP packets"); | 
|  | } | 
|  | } | 
|  |  | 
|  | template <typename T> | 
|  | void EventLogAnalyzer::CreateAccumulatedPacketsTimeSeries( | 
|  | PacketDirection desired_direction, | 
|  | Plot* plot, | 
|  | const std::map<StreamId, std::vector<T>>& packets, | 
|  | const std::string& label_prefix) { | 
|  | for (auto& kv : packets) { | 
|  | StreamId stream_id = kv.first; | 
|  | const std::vector<T>& packet_stream = kv.second; | 
|  | // Filter on direction and SSRC. | 
|  | if (stream_id.GetDirection() != desired_direction || | 
|  | !MatchingSsrc(stream_id.GetSsrc(), desired_ssrc_)) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | std::string label = label_prefix + " " + GetStreamName(stream_id); | 
|  | TimeSeries time_series(label, LineStyle::kStep); | 
|  | for (size_t i = 0; i < packet_stream.size(); i++) { | 
|  | float x = ToCallTime(packet_stream[i].timestamp); | 
|  | time_series.points.emplace_back(x, i + 1); | 
|  | } | 
|  |  | 
|  | plot->AppendTimeSeries(std::move(time_series)); | 
|  | } | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateAccumulatedPacketsGraph( | 
|  | PacketDirection desired_direction, | 
|  | Plot* plot) { | 
|  | CreateAccumulatedPacketsTimeSeries(desired_direction, plot, rtp_packets_, | 
|  | "RTP"); | 
|  | CreateAccumulatedPacketsTimeSeries(desired_direction, plot, rtcp_packets_, | 
|  | "RTCP"); | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 1, "Received Packets", kBottomMargin, kTopMargin); | 
|  | if (desired_direction == webrtc::PacketDirection::kIncomingPacket) { | 
|  | plot->SetTitle("Accumulated Incoming RTP/RTCP packets"); | 
|  | } else if (desired_direction == webrtc::PacketDirection::kOutgoingPacket) { | 
|  | plot->SetTitle("Accumulated Outgoing RTP/RTCP packets"); | 
|  | } | 
|  | } | 
|  |  | 
|  | // For each SSRC, plot the time between the consecutive playouts. | 
|  | void EventLogAnalyzer::CreatePlayoutGraph(Plot* plot) { | 
|  | std::map<uint32_t, TimeSeries> time_series; | 
|  | std::map<uint32_t, uint64_t> last_playout; | 
|  |  | 
|  | uint32_t ssrc; | 
|  |  | 
|  | for (size_t i = 0; i < parsed_log_.GetNumberOfEvents(); i++) { | 
|  | ParsedRtcEventLog::EventType event_type = parsed_log_.GetEventType(i); | 
|  | if (event_type == ParsedRtcEventLog::AUDIO_PLAYOUT_EVENT) { | 
|  | parsed_log_.GetAudioPlayout(i, &ssrc); | 
|  | uint64_t timestamp = parsed_log_.GetTimestamp(i); | 
|  | if (MatchingSsrc(ssrc, desired_ssrc_)) { | 
|  | float x = ToCallTime(timestamp); | 
|  | float y = static_cast<float>(timestamp - last_playout[ssrc]) / 1000; | 
|  | if (time_series[ssrc].points.size() == 0) { | 
|  | // There were no previusly logged playout for this SSRC. | 
|  | // Generate a point, but place it on the x-axis. | 
|  | y = 0; | 
|  | } | 
|  | time_series[ssrc].points.push_back(TimeSeriesPoint(x, y)); | 
|  | last_playout[ssrc] = timestamp; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // Set labels and put in graph. | 
|  | for (auto& kv : time_series) { | 
|  | kv.second.label = SsrcToString(kv.first); | 
|  | kv.second.line_style = LineStyle::kBar; | 
|  | plot->AppendTimeSeries(std::move(kv.second)); | 
|  | } | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 1, "Time since last playout (ms)", kBottomMargin, | 
|  | kTopMargin); | 
|  | plot->SetTitle("Audio playout"); | 
|  | } | 
|  |  | 
|  | // For audio SSRCs, plot the audio level. | 
|  | void EventLogAnalyzer::CreateAudioLevelGraph(Plot* plot) { | 
|  | std::map<StreamId, TimeSeries> time_series; | 
|  |  | 
|  | for (auto& kv : rtp_packets_) { | 
|  | StreamId stream_id = kv.first; | 
|  | const std::vector<LoggedRtpPacket>& packet_stream = kv.second; | 
|  | // TODO(ivoc): When audio send/receive configs are stored in the event | 
|  | //             log, a check should be added here to only process audio | 
|  | //             streams. Tracking bug: webrtc:6399 | 
|  | for (auto& packet : packet_stream) { | 
|  | if (packet.header.extension.hasAudioLevel) { | 
|  | float x = ToCallTime(packet.timestamp); | 
|  | // The audio level is stored in -dBov (so e.g. -10 dBov is stored as 10) | 
|  | // Here we convert it to dBov. | 
|  | float y = static_cast<float>(-packet.header.extension.audioLevel); | 
|  | time_series[stream_id].points.emplace_back(TimeSeriesPoint(x, y)); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | for (auto& series : time_series) { | 
|  | series.second.label = GetStreamName(series.first); | 
|  | series.second.line_style = LineStyle::kLine; | 
|  | plot->AppendTimeSeries(std::move(series.second)); | 
|  | } | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetYAxis(-127, 0, "Audio level (dBov)", kBottomMargin, | 
|  | kTopMargin); | 
|  | plot->SetTitle("Audio level"); | 
|  | } | 
|  |  | 
|  | // For each SSRC, plot the time between the consecutive playouts. | 
|  | void EventLogAnalyzer::CreateSequenceNumberGraph(Plot* plot) { | 
|  | for (auto& kv : rtp_packets_) { | 
|  | StreamId stream_id = kv.first; | 
|  | const std::vector<LoggedRtpPacket>& packet_stream = kv.second; | 
|  | // Filter on direction and SSRC. | 
|  | if (stream_id.GetDirection() != kIncomingPacket || | 
|  | !MatchingSsrc(stream_id.GetSsrc(), desired_ssrc_)) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | TimeSeries time_series(GetStreamName(stream_id), LineStyle::kBar); | 
|  | ProcessPairs<LoggedRtpPacket, float>( | 
|  | [](const LoggedRtpPacket& old_packet, | 
|  | const LoggedRtpPacket& new_packet) { | 
|  | int64_t diff = | 
|  | WrappingDifference(new_packet.header.sequenceNumber, | 
|  | old_packet.header.sequenceNumber, 1ul << 16); | 
|  | return diff; | 
|  | }, | 
|  | packet_stream, begin_time_, &time_series); | 
|  | plot->AppendTimeSeries(std::move(time_series)); | 
|  | } | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 1, "Difference since last packet", kBottomMargin, | 
|  | kTopMargin); | 
|  | plot->SetTitle("Sequence number"); | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateIncomingPacketLossGraph(Plot* plot) { | 
|  | for (auto& kv : rtp_packets_) { | 
|  | StreamId stream_id = kv.first; | 
|  | const std::vector<LoggedRtpPacket>& packet_stream = kv.second; | 
|  | // Filter on direction and SSRC. | 
|  | if (stream_id.GetDirection() != kIncomingPacket || | 
|  | !MatchingSsrc(stream_id.GetSsrc(), desired_ssrc_) || | 
|  | packet_stream.size() == 0) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | TimeSeries time_series(GetStreamName(stream_id), LineStyle::kLine, | 
|  | PointStyle::kHighlight); | 
|  | const uint64_t kWindowUs = 1000000; | 
|  | const uint64_t kStep = 1000000; | 
|  | SeqNumUnwrapper<uint16_t> unwrapper_; | 
|  | SeqNumUnwrapper<uint16_t> prior_unwrapper_; | 
|  | size_t window_index_begin = 0; | 
|  | size_t window_index_end = 0; | 
|  | int64_t highest_seq_number = | 
|  | unwrapper_.Unwrap(packet_stream[0].header.sequenceNumber) - 1; | 
|  | int64_t highest_prior_seq_number = | 
|  | prior_unwrapper_.Unwrap(packet_stream[0].header.sequenceNumber) - 1; | 
|  |  | 
|  | for (uint64_t t = begin_time_; t < end_time_ + kStep; t += kStep) { | 
|  | while (window_index_end < packet_stream.size() && | 
|  | packet_stream[window_index_end].timestamp < t) { | 
|  | int64_t sequence_number = unwrapper_.Unwrap( | 
|  | packet_stream[window_index_end].header.sequenceNumber); | 
|  | highest_seq_number = std::max(highest_seq_number, sequence_number); | 
|  | ++window_index_end; | 
|  | } | 
|  | while (window_index_begin < packet_stream.size() && | 
|  | packet_stream[window_index_begin].timestamp < t - kWindowUs) { | 
|  | int64_t sequence_number = prior_unwrapper_.Unwrap( | 
|  | packet_stream[window_index_begin].header.sequenceNumber); | 
|  | highest_prior_seq_number = | 
|  | std::max(highest_prior_seq_number, sequence_number); | 
|  | ++window_index_begin; | 
|  | } | 
|  | float x = ToCallTime(t); | 
|  | int64_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(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 1, "Estimated loss rate (%)", kBottomMargin, | 
|  | kTopMargin); | 
|  | plot->SetTitle("Estimated incoming loss rate"); | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateIncomingDelayDeltaGraph(Plot* plot) { | 
|  | for (auto& kv : rtp_packets_) { | 
|  | StreamId stream_id = kv.first; | 
|  | const std::vector<LoggedRtpPacket>& packet_stream = kv.second; | 
|  | // Filter on direction and SSRC. | 
|  | if (stream_id.GetDirection() != kIncomingPacket || | 
|  | !MatchingSsrc(stream_id.GetSsrc(), desired_ssrc_) || | 
|  | IsAudioSsrc(stream_id) || !IsVideoSsrc(stream_id) || | 
|  | IsRtxSsrc(stream_id)) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | TimeSeries capture_time_data(GetStreamName(stream_id) + " capture-time", | 
|  | LineStyle::kBar); | 
|  | ProcessPairs<LoggedRtpPacket, double>(NetworkDelayDiff_CaptureTime, | 
|  | packet_stream, begin_time_, | 
|  | &capture_time_data); | 
|  | plot->AppendTimeSeries(std::move(capture_time_data)); | 
|  |  | 
|  | TimeSeries send_time_data(GetStreamName(stream_id) + " abs-send-time", | 
|  | LineStyle::kBar); | 
|  | ProcessPairs<LoggedRtpPacket, double>(NetworkDelayDiff_AbsSendTime, | 
|  | packet_stream, begin_time_, | 
|  | &send_time_data); | 
|  | plot->AppendTimeSeries(std::move(send_time_data)); | 
|  | } | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 1, "Latency change (ms)", kBottomMargin, | 
|  | kTopMargin); | 
|  | plot->SetTitle("Network latency difference between consecutive packets"); | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateIncomingDelayGraph(Plot* plot) { | 
|  | for (auto& kv : rtp_packets_) { | 
|  | StreamId stream_id = kv.first; | 
|  | const std::vector<LoggedRtpPacket>& packet_stream = kv.second; | 
|  | // Filter on direction and SSRC. | 
|  | if (stream_id.GetDirection() != kIncomingPacket || | 
|  | !MatchingSsrc(stream_id.GetSsrc(), desired_ssrc_) || | 
|  | IsAudioSsrc(stream_id) || !IsVideoSsrc(stream_id) || | 
|  | IsRtxSsrc(stream_id)) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | TimeSeries capture_time_data(GetStreamName(stream_id) + " capture-time", | 
|  | LineStyle::kLine); | 
|  | AccumulatePairs<LoggedRtpPacket, double>(NetworkDelayDiff_CaptureTime, | 
|  | packet_stream, begin_time_, | 
|  | &capture_time_data); | 
|  | plot->AppendTimeSeries(std::move(capture_time_data)); | 
|  |  | 
|  | TimeSeries send_time_data(GetStreamName(stream_id) + " abs-send-time", | 
|  | LineStyle::kLine); | 
|  | AccumulatePairs<LoggedRtpPacket, double>(NetworkDelayDiff_AbsSendTime, | 
|  | packet_stream, begin_time_, | 
|  | &send_time_data); | 
|  | plot->AppendTimeSeries(std::move(send_time_data)); | 
|  | } | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 1, "Latency change (ms)", kBottomMargin, | 
|  | kTopMargin); | 
|  | plot->SetTitle("Network latency (relative to first packet)"); | 
|  | } | 
|  |  | 
|  | // Plot the fraction of packets lost (as perceived by the loss-based BWE). | 
|  | void EventLogAnalyzer::CreateFractionLossGraph(Plot* plot) { | 
|  | TimeSeries time_series("Fraction lost", LineStyle::kLine, | 
|  | PointStyle::kHighlight); | 
|  | for (auto& bwe_update : bwe_loss_updates_) { | 
|  | float x = ToCallTime(bwe_update.timestamp); | 
|  | float y = static_cast<float>(bwe_update.fraction_loss) / 255 * 100; | 
|  | time_series.points.emplace_back(x, y); | 
|  | } | 
|  |  | 
|  | plot->AppendTimeSeries(std::move(time_series)); | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 10, "Percent lost packets", kBottomMargin, | 
|  | kTopMargin); | 
|  | plot->SetTitle("Reported packet loss"); | 
|  | } | 
|  |  | 
|  | // Plot the total bandwidth used by all RTP streams. | 
|  | void EventLogAnalyzer::CreateTotalBitrateGraph( | 
|  | PacketDirection desired_direction, | 
|  | Plot* plot, | 
|  | bool show_detector_state, | 
|  | bool show_alr_state) { | 
|  | struct TimestampSize { | 
|  | TimestampSize(uint64_t t, size_t s) : timestamp(t), size(s) {} | 
|  | uint64_t timestamp; | 
|  | size_t size; | 
|  | }; | 
|  | std::vector<TimestampSize> packets; | 
|  |  | 
|  | PacketDirection direction; | 
|  | size_t total_length; | 
|  |  | 
|  | // Extract timestamps and sizes for the relevant packets. | 
|  | for (size_t i = 0; i < parsed_log_.GetNumberOfEvents(); i++) { | 
|  | ParsedRtcEventLog::EventType event_type = parsed_log_.GetEventType(i); | 
|  | if (event_type == ParsedRtcEventLog::RTP_EVENT) { | 
|  | parsed_log_.GetRtpHeader(i, &direction, nullptr, nullptr, &total_length, | 
|  | nullptr); | 
|  | if (direction == desired_direction) { | 
|  | uint64_t timestamp = parsed_log_.GetTimestamp(i); | 
|  | packets.push_back(TimestampSize(timestamp, total_length)); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | size_t window_index_begin = 0; | 
|  | size_t window_index_end = 0; | 
|  | size_t bytes_in_window = 0; | 
|  |  | 
|  | // Calculate a moving average of the bitrate and store in a TimeSeries. | 
|  | TimeSeries bitrate_series("Bitrate", LineStyle::kLine); | 
|  | for (uint64_t time = begin_time_; time < end_time_ + step_; time += step_) { | 
|  | while (window_index_end < packets.size() && | 
|  | packets[window_index_end].timestamp < time) { | 
|  | bytes_in_window += packets[window_index_end].size; | 
|  | ++window_index_end; | 
|  | } | 
|  | while (window_index_begin < packets.size() && | 
|  | packets[window_index_begin].timestamp < time - window_duration_) { | 
|  | RTC_DCHECK_LE(packets[window_index_begin].size, bytes_in_window); | 
|  | bytes_in_window -= packets[window_index_begin].size; | 
|  | ++window_index_begin; | 
|  | } | 
|  | float window_duration_in_seconds = | 
|  | static_cast<float>(window_duration_) / kNumMicrosecsPerSec; | 
|  | float x = ToCallTime(time); | 
|  | float y = bytes_in_window * 8 / window_duration_in_seconds / 1000; | 
|  | bitrate_series.points.emplace_back(x, y); | 
|  | } | 
|  | plot->AppendTimeSeries(std::move(bitrate_series)); | 
|  |  | 
|  | // Overlay the send-side bandwidth estimate over the outgoing bitrate. | 
|  | if (desired_direction == kOutgoingPacket) { | 
|  | TimeSeries loss_series("Loss-based estimate", LineStyle::kStep); | 
|  | for (auto& loss_update : bwe_loss_updates_) { | 
|  | float x = ToCallTime(loss_update.timestamp); | 
|  | float y = static_cast<float>(loss_update.new_bitrate) / 1000; | 
|  | loss_series.points.emplace_back(x, y); | 
|  | } | 
|  |  | 
|  | TimeSeries delay_series("Delay-based estimate", LineStyle::kStep); | 
|  | IntervalSeries overusing_series("Overusing", "#ff8e82", | 
|  | IntervalSeries::kHorizontal); | 
|  | IntervalSeries underusing_series("Underusing", "#5092fc", | 
|  | IntervalSeries::kHorizontal); | 
|  | IntervalSeries normal_series("Normal", "#c4ffc4", | 
|  | IntervalSeries::kHorizontal); | 
|  | IntervalSeries* last_series = &normal_series; | 
|  | double last_detector_switch = 0.0; | 
|  |  | 
|  | BandwidthUsage last_detector_state = BandwidthUsage::kBwNormal; | 
|  |  | 
|  | for (auto& delay_update : bwe_delay_updates_) { | 
|  | float x = ToCallTime(delay_update.timestamp); | 
|  | float y = static_cast<float>(delay_update.bitrate_bps) / 1000; | 
|  |  | 
|  | if (last_detector_state != delay_update.detector_state) { | 
|  | last_series->intervals.emplace_back(last_detector_switch, x); | 
|  | last_detector_state = delay_update.detector_state; | 
|  | last_detector_switch = x; | 
|  |  | 
|  | switch (delay_update.detector_state) { | 
|  | case BandwidthUsage::kBwNormal: | 
|  | last_series = &normal_series; | 
|  | break; | 
|  | case BandwidthUsage::kBwUnderusing: | 
|  | last_series = &underusing_series; | 
|  | break; | 
|  | case BandwidthUsage::kBwOverusing: | 
|  | last_series = &overusing_series; | 
|  | break; | 
|  | case BandwidthUsage::kLast: | 
|  | RTC_NOTREACHED(); | 
|  | } | 
|  | } | 
|  |  | 
|  | delay_series.points.emplace_back(x, y); | 
|  | } | 
|  |  | 
|  | RTC_CHECK(last_series); | 
|  | last_series->intervals.emplace_back(last_detector_switch, end_time_); | 
|  |  | 
|  | TimeSeries created_series("Probe cluster created.", LineStyle::kNone, | 
|  | PointStyle::kHighlight); | 
|  | for (auto& cluster : bwe_probe_cluster_created_events_) { | 
|  | float x = ToCallTime(cluster.timestamp); | 
|  | float y = static_cast<float>(cluster.bitrate_bps) / 1000; | 
|  | created_series.points.emplace_back(x, y); | 
|  | } | 
|  |  | 
|  | TimeSeries result_series("Probing results.", LineStyle::kNone, | 
|  | PointStyle::kHighlight); | 
|  | for (auto& result : bwe_probe_result_events_) { | 
|  | if (result.bitrate_bps) { | 
|  | float x = ToCallTime(result.timestamp); | 
|  | float y = static_cast<float>(*result.bitrate_bps) / 1000; | 
|  | result_series.points.emplace_back(x, y); | 
|  | } | 
|  | } | 
|  |  | 
|  | IntervalSeries alr_state("ALR", "#555555", IntervalSeries::kHorizontal); | 
|  | bool previously_in_alr = false; | 
|  | int64_t alr_start = 0; | 
|  | for (auto& alr : alr_state_events_) { | 
|  | float y = ToCallTime(alr.timestamp); | 
|  | if (!previously_in_alr && alr.in_alr) { | 
|  | alr_start = alr.timestamp; | 
|  | previously_in_alr = true; | 
|  | } else if (previously_in_alr && !alr.in_alr) { | 
|  | float x = ToCallTime(alr_start); | 
|  | alr_state.intervals.emplace_back(x, y); | 
|  | previously_in_alr = false; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (previously_in_alr) { | 
|  | float x = ToCallTime(alr_start); | 
|  | float y = ToCallTime(end_time_); | 
|  | alr_state.intervals.emplace_back(x, y); | 
|  | } | 
|  |  | 
|  | if (show_detector_state) { | 
|  | plot->AppendIntervalSeries(std::move(overusing_series)); | 
|  | plot->AppendIntervalSeries(std::move(underusing_series)); | 
|  | plot->AppendIntervalSeries(std::move(normal_series)); | 
|  | } | 
|  |  | 
|  | if (show_alr_state) { | 
|  | plot->AppendIntervalSeries(std::move(alr_state)); | 
|  | } | 
|  | plot->AppendTimeSeries(std::move(loss_series)); | 
|  | plot->AppendTimeSeries(std::move(delay_series)); | 
|  | plot->AppendTimeSeries(std::move(created_series)); | 
|  | plot->AppendTimeSeries(std::move(result_series)); | 
|  | } | 
|  |  | 
|  | // Overlay the incoming REMB over the outgoing bitrate | 
|  | // and outgoing REMB over incoming bitrate. | 
|  | PacketDirection remb_direction = | 
|  | desired_direction == kOutgoingPacket ? kIncomingPacket : kOutgoingPacket; | 
|  | TimeSeries remb_series("Remb", LineStyle::kStep); | 
|  | std::multimap<uint64_t, const LoggedRtcpPacket*> remb_packets; | 
|  | for (const auto& kv : rtcp_packets_) { | 
|  | if (kv.first.GetDirection() == remb_direction) { | 
|  | for (const LoggedRtcpPacket& rtcp_packet : kv.second) { | 
|  | if (rtcp_packet.type == kRtcpRemb) { | 
|  | remb_packets.insert( | 
|  | std::make_pair(rtcp_packet.timestamp, &rtcp_packet)); | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | for (const auto& kv : remb_packets) { | 
|  | const LoggedRtcpPacket* const rtcp = kv.second; | 
|  | const rtcp::Remb* const remb = static_cast<rtcp::Remb*>(rtcp->packet.get()); | 
|  | float x = ToCallTime(rtcp->timestamp); | 
|  | float y = static_cast<float>(remb->bitrate_bps()) / 1000; | 
|  | remb_series.points.emplace_back(x, y); | 
|  | } | 
|  | plot->AppendTimeSeriesIfNotEmpty(std::move(remb_series)); | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 1, "Bitrate (kbps)", kBottomMargin, kTopMargin); | 
|  | if (desired_direction == webrtc::PacketDirection::kIncomingPacket) { | 
|  | plot->SetTitle("Incoming RTP bitrate"); | 
|  | } else if (desired_direction == webrtc::PacketDirection::kOutgoingPacket) { | 
|  | plot->SetTitle("Outgoing RTP bitrate"); | 
|  | } | 
|  | } | 
|  |  | 
|  | // For each SSRC, plot the bandwidth used by that stream. | 
|  | void EventLogAnalyzer::CreateStreamBitrateGraph( | 
|  | PacketDirection desired_direction, | 
|  | Plot* plot) { | 
|  | for (auto& kv : rtp_packets_) { | 
|  | StreamId stream_id = kv.first; | 
|  | const std::vector<LoggedRtpPacket>& packet_stream = kv.second; | 
|  | // Filter on direction and SSRC. | 
|  | if (stream_id.GetDirection() != desired_direction || | 
|  | !MatchingSsrc(stream_id.GetSsrc(), desired_ssrc_)) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | TimeSeries time_series(GetStreamName(stream_id), LineStyle::kLine); | 
|  | MovingAverage<LoggedRtpPacket, double>( | 
|  | [](const LoggedRtpPacket& packet) { | 
|  | return packet.total_length * 8.0 / 1000.0; | 
|  | }, | 
|  | packet_stream, begin_time_, end_time_, window_duration_, step_, | 
|  | &time_series); | 
|  | plot->AppendTimeSeries(std::move(time_series)); | 
|  | } | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 1, "Bitrate (kbps)", kBottomMargin, kTopMargin); | 
|  | if (desired_direction == webrtc::PacketDirection::kIncomingPacket) { | 
|  | plot->SetTitle("Incoming bitrate per stream"); | 
|  | } else if (desired_direction == webrtc::PacketDirection::kOutgoingPacket) { | 
|  | plot->SetTitle("Outgoing bitrate per stream"); | 
|  | } | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateSendSideBweSimulationGraph(Plot* plot) { | 
|  | std::multimap<uint64_t, const LoggedRtpPacket*> outgoing_rtp; | 
|  | std::multimap<uint64_t, const LoggedRtcpPacket*> incoming_rtcp; | 
|  |  | 
|  | for (const auto& kv : rtp_packets_) { | 
|  | if (kv.first.GetDirection() == PacketDirection::kOutgoingPacket) { | 
|  | for (const LoggedRtpPacket& rtp_packet : kv.second) | 
|  | outgoing_rtp.insert(std::make_pair(rtp_packet.timestamp, &rtp_packet)); | 
|  | } | 
|  | } | 
|  |  | 
|  | for (const auto& kv : rtcp_packets_) { | 
|  | if (kv.first.GetDirection() == PacketDirection::kIncomingPacket) { | 
|  | for (const LoggedRtcpPacket& rtcp_packet : kv.second) | 
|  | incoming_rtcp.insert( | 
|  | std::make_pair(rtcp_packet.timestamp, &rtcp_packet)); | 
|  | } | 
|  | } | 
|  |  | 
|  | SimulatedClock clock(0); | 
|  | BitrateObserver observer; | 
|  | RtcEventLogNullImpl null_event_log; | 
|  | PacketRouter packet_router; | 
|  | PacedSender pacer(&clock, &packet_router, &null_event_log); | 
|  | SendSideCongestionController cc(&clock, &observer, &null_event_log, &pacer); | 
|  | // TODO(holmer): Log the call config and use that here instead. | 
|  | static const uint32_t kDefaultStartBitrateBps = 300000; | 
|  | cc.SetBweBitrates(0, kDefaultStartBitrateBps, -1); | 
|  |  | 
|  | TimeSeries time_series("Delay-based estimate", LineStyle::kStep, | 
|  | PointStyle::kHighlight); | 
|  | TimeSeries acked_time_series("Acked bitrate", LineStyle::kLine, | 
|  | PointStyle::kHighlight); | 
|  | TimeSeries acked_estimate_time_series( | 
|  | "Acked bitrate estimate", LineStyle::kLine, PointStyle::kHighlight); | 
|  |  | 
|  | auto rtp_iterator = outgoing_rtp.begin(); | 
|  | auto rtcp_iterator = incoming_rtcp.begin(); | 
|  |  | 
|  | auto NextRtpTime = [&]() { | 
|  | if (rtp_iterator != outgoing_rtp.end()) | 
|  | return static_cast<int64_t>(rtp_iterator->first); | 
|  | return std::numeric_limits<int64_t>::max(); | 
|  | }; | 
|  |  | 
|  | auto NextRtcpTime = [&]() { | 
|  | if (rtcp_iterator != incoming_rtcp.end()) | 
|  | return static_cast<int64_t>(rtcp_iterator->first); | 
|  | return std::numeric_limits<int64_t>::max(); | 
|  | }; | 
|  |  | 
|  | auto NextProcessTime = [&]() { | 
|  | if (rtcp_iterator != incoming_rtcp.end() || | 
|  | rtp_iterator != outgoing_rtp.end()) { | 
|  | return clock.TimeInMicroseconds() + | 
|  | std::max<int64_t>(cc.TimeUntilNextProcess() * 1000, 0); | 
|  | } | 
|  | return std::numeric_limits<int64_t>::max(); | 
|  | }; | 
|  |  | 
|  | RateStatistics acked_bitrate(250, 8000); | 
|  | #if !(BWE_TEST_LOGGING_COMPILE_TIME_ENABLE) | 
|  | // The event_log_visualizer should normally not be compiled with | 
|  | // BWE_TEST_LOGGING_COMPILE_TIME_ENABLE since the normal plots won't work. | 
|  | // However, compiling with BWE_TEST_LOGGING, runnning with --plot_sendside_bwe | 
|  | // and piping the output to plot_dynamics.py can be used as a hack to get the | 
|  | // internal state of various BWE components. In this case, it is important | 
|  | // we don't instantiate the AcknowledgedBitrateEstimator both here and in | 
|  | // SendSideCongestionController since that would lead to duplicate outputs. | 
|  | AcknowledgedBitrateEstimator acknowledged_bitrate_estimator( | 
|  | rtc::MakeUnique<BitrateEstimator>()); | 
|  | #endif  // !(BWE_TEST_LOGGING_COMPILE_TIME_ENABLE) | 
|  | int64_t time_us = std::min(NextRtpTime(), NextRtcpTime()); | 
|  | int64_t last_update_us = 0; | 
|  | while (time_us != std::numeric_limits<int64_t>::max()) { | 
|  | clock.AdvanceTimeMicroseconds(time_us - clock.TimeInMicroseconds()); | 
|  | if (clock.TimeInMicroseconds() >= NextRtcpTime()) { | 
|  | RTC_DCHECK_EQ(clock.TimeInMicroseconds(), NextRtcpTime()); | 
|  | const LoggedRtcpPacket& rtcp = *rtcp_iterator->second; | 
|  | if (rtcp.type == kRtcpTransportFeedback) { | 
|  | cc.OnTransportFeedback( | 
|  | *static_cast<rtcp::TransportFeedback*>(rtcp.packet.get())); | 
|  | std::vector<PacketFeedback> feedback = cc.GetTransportFeedbackVector(); | 
|  | SortPacketFeedbackVector(&feedback); | 
|  | rtc::Optional<uint32_t> bitrate_bps; | 
|  | if (!feedback.empty()) { | 
|  | #if !(BWE_TEST_LOGGING_COMPILE_TIME_ENABLE) | 
|  | acknowledged_bitrate_estimator.IncomingPacketFeedbackVector(feedback); | 
|  | #endif  // !(BWE_TEST_LOGGING_COMPILE_TIME_ENABLE) | 
|  | for (const PacketFeedback& packet : feedback) | 
|  | acked_bitrate.Update(packet.payload_size, packet.arrival_time_ms); | 
|  | bitrate_bps = acked_bitrate.Rate(feedback.back().arrival_time_ms); | 
|  | } | 
|  | float x = ToCallTime(clock.TimeInMicroseconds()); | 
|  | float y = bitrate_bps.value_or(0) / 1000; | 
|  | acked_time_series.points.emplace_back(x, y); | 
|  | #if !(BWE_TEST_LOGGING_COMPILE_TIME_ENABLE) | 
|  | y = acknowledged_bitrate_estimator.bitrate_bps().value_or(0) / 1000; | 
|  | acked_estimate_time_series.points.emplace_back(x, y); | 
|  | #endif  // !(BWE_TEST_LOGGING_COMPILE_TIME_ENABLE) | 
|  | } | 
|  | ++rtcp_iterator; | 
|  | } | 
|  | if (clock.TimeInMicroseconds() >= NextRtpTime()) { | 
|  | RTC_DCHECK_EQ(clock.TimeInMicroseconds(), NextRtpTime()); | 
|  | const LoggedRtpPacket& rtp = *rtp_iterator->second; | 
|  | if (rtp.header.extension.hasTransportSequenceNumber) { | 
|  | RTC_DCHECK(rtp.header.extension.hasTransportSequenceNumber); | 
|  | cc.AddPacket(rtp.header.ssrc, | 
|  | rtp.header.extension.transportSequenceNumber, | 
|  | rtp.total_length, PacedPacketInfo()); | 
|  | rtc::SentPacket sent_packet( | 
|  | rtp.header.extension.transportSequenceNumber, rtp.timestamp / 1000); | 
|  | cc.OnSentPacket(sent_packet); | 
|  | } | 
|  | ++rtp_iterator; | 
|  | } | 
|  | if (clock.TimeInMicroseconds() >= NextProcessTime()) { | 
|  | RTC_DCHECK_EQ(clock.TimeInMicroseconds(), NextProcessTime()); | 
|  | cc.Process(); | 
|  | } | 
|  | if (observer.GetAndResetBitrateUpdated() || | 
|  | time_us - last_update_us >= 1e6) { | 
|  | uint32_t y = observer.last_bitrate_bps() / 1000; | 
|  | float x = ToCallTime(clock.TimeInMicroseconds()); | 
|  | time_series.points.emplace_back(x, y); | 
|  | last_update_us = time_us; | 
|  | } | 
|  | time_us = std::min({NextRtpTime(), NextRtcpTime(), NextProcessTime()}); | 
|  | } | 
|  | // Add the data set to the plot. | 
|  | plot->AppendTimeSeries(std::move(time_series)); | 
|  | plot->AppendTimeSeries(std::move(acked_time_series)); | 
|  | plot->AppendTimeSeriesIfNotEmpty(std::move(acked_estimate_time_series)); | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 10, "Bitrate (kbps)", kBottomMargin, kTopMargin); | 
|  | plot->SetTitle("Simulated send-side BWE behavior"); | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateReceiveSideBweSimulationGraph(Plot* plot) { | 
|  | class RembInterceptingPacketRouter : public PacketRouter { | 
|  | public: | 
|  | void OnReceiveBitrateChanged(const std::vector<uint32_t>& ssrcs, | 
|  | uint32_t bitrate_bps) override { | 
|  | last_bitrate_bps_ = bitrate_bps; | 
|  | bitrate_updated_ = true; | 
|  | PacketRouter::OnReceiveBitrateChanged(ssrcs, bitrate_bps); | 
|  | } | 
|  | uint32_t last_bitrate_bps() const { return last_bitrate_bps_; } | 
|  | bool GetAndResetBitrateUpdated() { | 
|  | bool bitrate_updated = bitrate_updated_; | 
|  | bitrate_updated_ = false; | 
|  | return bitrate_updated; | 
|  | } | 
|  |  | 
|  | private: | 
|  | uint32_t last_bitrate_bps_; | 
|  | bool bitrate_updated_; | 
|  | }; | 
|  |  | 
|  | std::multimap<uint64_t, const LoggedRtpPacket*> incoming_rtp; | 
|  |  | 
|  | for (const auto& kv : rtp_packets_) { | 
|  | if (kv.first.GetDirection() == PacketDirection::kIncomingPacket && | 
|  | IsVideoSsrc(kv.first)) { | 
|  | for (const LoggedRtpPacket& rtp_packet : kv.second) | 
|  | incoming_rtp.insert(std::make_pair(rtp_packet.timestamp, &rtp_packet)); | 
|  | } | 
|  | } | 
|  |  | 
|  | SimulatedClock clock(0); | 
|  | RembInterceptingPacketRouter packet_router; | 
|  | // TODO(terelius): The PacketRrouter is the used as the RemoteBitrateObserver. | 
|  | // Is this intentional? | 
|  | ReceiveSideCongestionController rscc(&clock, &packet_router); | 
|  | // TODO(holmer): Log the call config and use that here instead. | 
|  | // static const uint32_t kDefaultStartBitrateBps = 300000; | 
|  | // rscc.SetBweBitrates(0, kDefaultStartBitrateBps, -1); | 
|  |  | 
|  | TimeSeries time_series("Receive side estimate", LineStyle::kLine, | 
|  | PointStyle::kHighlight); | 
|  | TimeSeries acked_time_series("Received bitrate", LineStyle::kLine); | 
|  |  | 
|  | RateStatistics acked_bitrate(250, 8000); | 
|  | int64_t last_update_us = 0; | 
|  | for (const auto& kv : incoming_rtp) { | 
|  | const LoggedRtpPacket& packet = *kv.second; | 
|  | int64_t arrival_time_ms = packet.timestamp / 1000; | 
|  | size_t payload = packet.total_length; /*Should subtract header?*/ | 
|  | clock.AdvanceTimeMicroseconds(packet.timestamp - | 
|  | clock.TimeInMicroseconds()); | 
|  | rscc.OnReceivedPacket(arrival_time_ms, payload, packet.header); | 
|  | acked_bitrate.Update(payload, arrival_time_ms); | 
|  | rtc::Optional<uint32_t> bitrate_bps = acked_bitrate.Rate(arrival_time_ms); | 
|  | if (bitrate_bps) { | 
|  | uint32_t y = *bitrate_bps / 1000; | 
|  | float x = ToCallTime(clock.TimeInMicroseconds()); | 
|  | acked_time_series.points.emplace_back(x, y); | 
|  | } | 
|  | if (packet_router.GetAndResetBitrateUpdated() || | 
|  | clock.TimeInMicroseconds() - last_update_us >= 1e6) { | 
|  | uint32_t y = packet_router.last_bitrate_bps() / 1000; | 
|  | float x = ToCallTime(clock.TimeInMicroseconds()); | 
|  | time_series.points.emplace_back(x, y); | 
|  | last_update_us = clock.TimeInMicroseconds(); | 
|  | } | 
|  | } | 
|  | // Add the data set to the plot. | 
|  | plot->AppendTimeSeries(std::move(time_series)); | 
|  | plot->AppendTimeSeries(std::move(acked_time_series)); | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 10, "Bitrate (kbps)", kBottomMargin, kTopMargin); | 
|  | plot->SetTitle("Simulated receive-side BWE behavior"); | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateNetworkDelayFeedbackGraph(Plot* plot) { | 
|  | std::multimap<uint64_t, const LoggedRtpPacket*> outgoing_rtp; | 
|  | std::multimap<uint64_t, const LoggedRtcpPacket*> incoming_rtcp; | 
|  |  | 
|  | for (const auto& kv : rtp_packets_) { | 
|  | if (kv.first.GetDirection() == PacketDirection::kOutgoingPacket) { | 
|  | for (const LoggedRtpPacket& rtp_packet : kv.second) | 
|  | outgoing_rtp.insert(std::make_pair(rtp_packet.timestamp, &rtp_packet)); | 
|  | } | 
|  | } | 
|  |  | 
|  | for (const auto& kv : rtcp_packets_) { | 
|  | if (kv.first.GetDirection() == PacketDirection::kIncomingPacket) { | 
|  | for (const LoggedRtcpPacket& rtcp_packet : kv.second) | 
|  | incoming_rtcp.insert( | 
|  | std::make_pair(rtcp_packet.timestamp, &rtcp_packet)); | 
|  | } | 
|  | } | 
|  |  | 
|  | SimulatedClock clock(0); | 
|  | TransportFeedbackAdapter feedback_adapter(&clock); | 
|  |  | 
|  | TimeSeries late_feedback_series("Late feedback results.", LineStyle::kNone, | 
|  | PointStyle::kHighlight); | 
|  | TimeSeries time_series("Network Delay Change", LineStyle::kLine, | 
|  | PointStyle::kHighlight); | 
|  | int64_t estimated_base_delay_ms = std::numeric_limits<int64_t>::max(); | 
|  |  | 
|  | auto rtp_iterator = outgoing_rtp.begin(); | 
|  | auto rtcp_iterator = incoming_rtcp.begin(); | 
|  |  | 
|  | auto NextRtpTime = [&]() { | 
|  | if (rtp_iterator != outgoing_rtp.end()) | 
|  | return static_cast<int64_t>(rtp_iterator->first); | 
|  | return std::numeric_limits<int64_t>::max(); | 
|  | }; | 
|  |  | 
|  | auto NextRtcpTime = [&]() { | 
|  | if (rtcp_iterator != incoming_rtcp.end()) | 
|  | return static_cast<int64_t>(rtcp_iterator->first); | 
|  | return std::numeric_limits<int64_t>::max(); | 
|  | }; | 
|  |  | 
|  | int64_t time_us = std::min(NextRtpTime(), NextRtcpTime()); | 
|  | int64_t prev_y = 0; | 
|  | while (time_us != std::numeric_limits<int64_t>::max()) { | 
|  | clock.AdvanceTimeMicroseconds(time_us - clock.TimeInMicroseconds()); | 
|  | if (clock.TimeInMicroseconds() >= NextRtcpTime()) { | 
|  | RTC_DCHECK_EQ(clock.TimeInMicroseconds(), NextRtcpTime()); | 
|  | const LoggedRtcpPacket& rtcp = *rtcp_iterator->second; | 
|  | if (rtcp.type == kRtcpTransportFeedback) { | 
|  | feedback_adapter.OnTransportFeedback( | 
|  | *static_cast<rtcp::TransportFeedback*>(rtcp.packet.get())); | 
|  | std::vector<PacketFeedback> feedback = | 
|  | feedback_adapter.GetTransportFeedbackVector(); | 
|  | SortPacketFeedbackVector(&feedback); | 
|  | for (const PacketFeedback& packet : feedback) { | 
|  | float x = ToCallTime(clock.TimeInMicroseconds()); | 
|  | if (packet.send_time_ms == PacketFeedback::kNoSendTime) { | 
|  | late_feedback_series.points.emplace_back(x, prev_y); | 
|  | continue; | 
|  | } | 
|  | int64_t y = packet.arrival_time_ms - packet.send_time_ms; | 
|  | prev_y = y; | 
|  | estimated_base_delay_ms = std::min(y, estimated_base_delay_ms); | 
|  | time_series.points.emplace_back(x, y); | 
|  | } | 
|  | } | 
|  | ++rtcp_iterator; | 
|  | } | 
|  | if (clock.TimeInMicroseconds() >= NextRtpTime()) { | 
|  | RTC_DCHECK_EQ(clock.TimeInMicroseconds(), NextRtpTime()); | 
|  | const LoggedRtpPacket& rtp = *rtp_iterator->second; | 
|  | if (rtp.header.extension.hasTransportSequenceNumber) { | 
|  | RTC_DCHECK(rtp.header.extension.hasTransportSequenceNumber); | 
|  | feedback_adapter.AddPacket(rtp.header.ssrc, | 
|  | rtp.header.extension.transportSequenceNumber, | 
|  | rtp.total_length, PacedPacketInfo()); | 
|  | feedback_adapter.OnSentPacket( | 
|  | rtp.header.extension.transportSequenceNumber, rtp.timestamp / 1000); | 
|  | } | 
|  | ++rtp_iterator; | 
|  | } | 
|  | time_us = std::min(NextRtpTime(), NextRtcpTime()); | 
|  | } | 
|  | // We assume that the base network delay (w/o queues) is the min delay | 
|  | // observed during the call. | 
|  | for (TimeSeriesPoint& point : time_series.points) | 
|  | point.y -= estimated_base_delay_ms; | 
|  | for (TimeSeriesPoint& point : late_feedback_series.points) | 
|  | point.y -= estimated_base_delay_ms; | 
|  | // Add the data set to the plot. | 
|  | plot->AppendTimeSeriesIfNotEmpty(std::move(time_series)); | 
|  | plot->AppendTimeSeriesIfNotEmpty(std::move(late_feedback_series)); | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 10, "Delay (ms)", kBottomMargin, kTopMargin); | 
|  | plot->SetTitle("Network Delay Change."); | 
|  | } | 
|  |  | 
|  | std::vector<std::pair<int64_t, int64_t>> EventLogAnalyzer::GetFrameTimestamps() | 
|  | const { | 
|  | std::vector<std::pair<int64_t, int64_t>> timestamps; | 
|  | size_t largest_stream_size = 0; | 
|  | const std::vector<LoggedRtpPacket>* largest_video_stream = nullptr; | 
|  | // Find the incoming video stream with the most number of packets that is | 
|  | // not rtx. | 
|  | for (const auto& kv : rtp_packets_) { | 
|  | if (kv.first.GetDirection() == kIncomingPacket && | 
|  | video_ssrcs_.find(kv.first) != video_ssrcs_.end() && | 
|  | rtx_ssrcs_.find(kv.first) == rtx_ssrcs_.end() && | 
|  | kv.second.size() > largest_stream_size) { | 
|  | largest_stream_size = kv.second.size(); | 
|  | largest_video_stream = &kv.second; | 
|  | } | 
|  | } | 
|  | if (largest_video_stream == nullptr) { | 
|  | for (auto& packet : *largest_video_stream) { | 
|  | if (packet.header.markerBit) { | 
|  | int64_t capture_ms = packet.header.timestamp / 90.0; | 
|  | int64_t arrival_ms = packet.timestamp / 1000.0; | 
|  | timestamps.push_back(std::make_pair(capture_ms, arrival_ms)); | 
|  | } | 
|  | } | 
|  | } | 
|  | return timestamps; | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreatePacerDelayGraph(Plot* plot) { | 
|  | for (const auto& kv : rtp_packets_) { | 
|  | const std::vector<LoggedRtpPacket>& packets = kv.second; | 
|  | StreamId stream_id = kv.first; | 
|  | if (stream_id.GetDirection() == kIncomingPacket) | 
|  | continue; | 
|  |  | 
|  | if (packets.size() < 2) { | 
|  | RTC_LOG(LS_WARNING) | 
|  | << "Can't estimate a the RTP clock frequency or the " | 
|  | "pacer delay with less than 2 packets in the stream"; | 
|  | continue; | 
|  | } | 
|  | rtc::Optional<uint32_t> estimated_frequency = | 
|  | EstimateRtpClockFrequency(packets); | 
|  | if (!estimated_frequency) | 
|  | continue; | 
|  | if (IsVideoSsrc(stream_id) && *estimated_frequency != 90000) { | 
|  | RTC_LOG(LS_WARNING) | 
|  | << "Video stream should use a 90 kHz clock but appears to use " | 
|  | << *estimated_frequency / 1000 << ". Discarding."; | 
|  | continue; | 
|  | } | 
|  |  | 
|  | TimeSeries pacer_delay_series( | 
|  | GetStreamName(stream_id) + "(" + | 
|  | std::to_string(*estimated_frequency / 1000) + " kHz)", | 
|  | LineStyle::kLine, PointStyle::kHighlight); | 
|  | SeqNumUnwrapper<uint32_t> timestamp_unwrapper; | 
|  | uint64_t first_capture_timestamp = | 
|  | timestamp_unwrapper.Unwrap(packets.front().header.timestamp); | 
|  | uint64_t first_send_timestamp = packets.front().timestamp; | 
|  | for (LoggedRtpPacket packet : packets) { | 
|  | double capture_time_ms = (static_cast<double>(timestamp_unwrapper.Unwrap( | 
|  | packet.header.timestamp)) - | 
|  | first_capture_timestamp) / | 
|  | *estimated_frequency * 1000; | 
|  | double send_time_ms = | 
|  | static_cast<double>(packet.timestamp - first_send_timestamp) / 1000; | 
|  | float x = ToCallTime(packet.timestamp); | 
|  | float y = send_time_ms - capture_time_ms; | 
|  | pacer_delay_series.points.emplace_back(x, y); | 
|  | } | 
|  | plot->AppendTimeSeries(std::move(pacer_delay_series)); | 
|  | } | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 10, "Pacer delay (ms)", kBottomMargin, kTopMargin); | 
|  | plot->SetTitle( | 
|  | "Delay from capture to send time. (First packet normalized to 0.)"); | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateTimestampGraph(Plot* plot) { | 
|  | for (const auto& kv : rtp_packets_) { | 
|  | const std::vector<LoggedRtpPacket>& rtp_packets = kv.second; | 
|  | StreamId stream_id = kv.first; | 
|  |  | 
|  | { | 
|  | TimeSeries timestamp_data(GetStreamName(stream_id) + " capture-time", | 
|  | LineStyle::kLine, PointStyle::kHighlight); | 
|  | for (LoggedRtpPacket packet : rtp_packets) { | 
|  | float x = ToCallTime(packet.timestamp); | 
|  | float y = packet.header.timestamp; | 
|  | timestamp_data.points.emplace_back(x, y); | 
|  | } | 
|  | plot->AppendTimeSeries(std::move(timestamp_data)); | 
|  | } | 
|  |  | 
|  | { | 
|  | auto kv = rtcp_packets_.find(stream_id); | 
|  | if (kv != rtcp_packets_.end()) { | 
|  | const auto& packets = kv->second; | 
|  | TimeSeries timestamp_data( | 
|  | GetStreamName(stream_id) + " rtcp capture-time", LineStyle::kLine, | 
|  | PointStyle::kHighlight); | 
|  | for (const LoggedRtcpPacket& rtcp : packets) { | 
|  | if (rtcp.type != kRtcpSr) | 
|  | continue; | 
|  | rtcp::SenderReport* sr; | 
|  | sr = static_cast<rtcp::SenderReport*>(rtcp.packet.get()); | 
|  | float x = ToCallTime(rtcp.timestamp); | 
|  | float y = sr->rtp_timestamp(); | 
|  | timestamp_data.points.emplace_back(x, y); | 
|  | } | 
|  | plot->AppendTimeSeries(std::move(timestamp_data)); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 1, "Timestamp (90khz)", kBottomMargin, kTopMargin); | 
|  | plot->SetTitle("Timestamps"); | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateAudioEncoderTargetBitrateGraph(Plot* plot) { | 
|  | TimeSeries time_series("Audio encoder target bitrate", LineStyle::kLine, | 
|  | PointStyle::kHighlight); | 
|  | ProcessPoints<AudioNetworkAdaptationEvent>( | 
|  | [](const AudioNetworkAdaptationEvent& ana_event) -> rtc::Optional<float> { | 
|  | if (ana_event.config.bitrate_bps) | 
|  | return static_cast<float>(*ana_event.config.bitrate_bps); | 
|  | return rtc::nullopt; | 
|  | }, | 
|  | audio_network_adaptation_events_, begin_time_, &time_series); | 
|  | plot->AppendTimeSeries(std::move(time_series)); | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 1, "Bitrate (bps)", kBottomMargin, kTopMargin); | 
|  | plot->SetTitle("Reported audio encoder target bitrate"); | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateAudioEncoderFrameLengthGraph(Plot* plot) { | 
|  | TimeSeries time_series("Audio encoder frame length", LineStyle::kLine, | 
|  | PointStyle::kHighlight); | 
|  | ProcessPoints<AudioNetworkAdaptationEvent>( | 
|  | [](const AudioNetworkAdaptationEvent& ana_event) { | 
|  | if (ana_event.config.frame_length_ms) | 
|  | return rtc::Optional<float>( | 
|  | static_cast<float>(*ana_event.config.frame_length_ms)); | 
|  | return rtc::Optional<float>(); | 
|  | }, | 
|  | audio_network_adaptation_events_, begin_time_, &time_series); | 
|  | plot->AppendTimeSeries(std::move(time_series)); | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 1, "Frame length (ms)", kBottomMargin, kTopMargin); | 
|  | plot->SetTitle("Reported audio encoder frame length"); | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateAudioEncoderPacketLossGraph(Plot* plot) { | 
|  | TimeSeries time_series("Audio encoder uplink packet loss fraction", | 
|  | LineStyle::kLine, PointStyle::kHighlight); | 
|  | ProcessPoints<AudioNetworkAdaptationEvent>( | 
|  | [](const AudioNetworkAdaptationEvent& ana_event) { | 
|  | if (ana_event.config.uplink_packet_loss_fraction) | 
|  | return rtc::Optional<float>(static_cast<float>( | 
|  | *ana_event.config.uplink_packet_loss_fraction)); | 
|  | return rtc::Optional<float>(); | 
|  | }, | 
|  | audio_network_adaptation_events_, begin_time_, &time_series); | 
|  | plot->AppendTimeSeries(std::move(time_series)); | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 10, "Percent lost packets", kBottomMargin, | 
|  | kTopMargin); | 
|  | plot->SetTitle("Reported audio encoder lost packets"); | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateAudioEncoderEnableFecGraph(Plot* plot) { | 
|  | TimeSeries time_series("Audio encoder FEC", LineStyle::kLine, | 
|  | PointStyle::kHighlight); | 
|  | ProcessPoints<AudioNetworkAdaptationEvent>( | 
|  | [](const AudioNetworkAdaptationEvent& ana_event) { | 
|  | if (ana_event.config.enable_fec) | 
|  | return rtc::Optional<float>( | 
|  | static_cast<float>(*ana_event.config.enable_fec)); | 
|  | return rtc::Optional<float>(); | 
|  | }, | 
|  | audio_network_adaptation_events_, begin_time_, &time_series); | 
|  | plot->AppendTimeSeries(std::move(time_series)); | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 1, "FEC (false/true)", kBottomMargin, kTopMargin); | 
|  | plot->SetTitle("Reported audio encoder FEC"); | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateAudioEncoderEnableDtxGraph(Plot* plot) { | 
|  | TimeSeries time_series("Audio encoder DTX", LineStyle::kLine, | 
|  | PointStyle::kHighlight); | 
|  | ProcessPoints<AudioNetworkAdaptationEvent>( | 
|  | [](const AudioNetworkAdaptationEvent& ana_event) { | 
|  | if (ana_event.config.enable_dtx) | 
|  | return rtc::Optional<float>( | 
|  | static_cast<float>(*ana_event.config.enable_dtx)); | 
|  | return rtc::Optional<float>(); | 
|  | }, | 
|  | audio_network_adaptation_events_, begin_time_, &time_series); | 
|  | plot->AppendTimeSeries(std::move(time_series)); | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 1, "DTX (false/true)", kBottomMargin, kTopMargin); | 
|  | plot->SetTitle("Reported audio encoder DTX"); | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateAudioEncoderNumChannelsGraph(Plot* plot) { | 
|  | TimeSeries time_series("Audio encoder number of channels", LineStyle::kLine, | 
|  | PointStyle::kHighlight); | 
|  | ProcessPoints<AudioNetworkAdaptationEvent>( | 
|  | [](const AudioNetworkAdaptationEvent& ana_event) { | 
|  | if (ana_event.config.num_channels) | 
|  | return rtc::Optional<float>( | 
|  | static_cast<float>(*ana_event.config.num_channels)); | 
|  | return rtc::Optional<float>(); | 
|  | }, | 
|  | audio_network_adaptation_events_, begin_time_, &time_series); | 
|  | plot->AppendTimeSeries(std::move(time_series)); | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 1, "Number of channels (1 (mono)/2 (stereo))", | 
|  | kBottomMargin, kTopMargin); | 
|  | plot->SetTitle("Reported audio encoder number of channels"); | 
|  | } | 
|  |  | 
|  | class NetEqStreamInput : public test::NetEqInput { | 
|  | public: | 
|  | // Does not take any ownership, and all pointers must refer to valid objects | 
|  | // that outlive the one constructed. | 
|  | NetEqStreamInput(const std::vector<LoggedRtpPacket>* packet_stream, | 
|  | const std::vector<uint64_t>* output_events_us, | 
|  | rtc::Optional<uint64_t> end_time_us) | 
|  | : packet_stream_(*packet_stream), | 
|  | packet_stream_it_(packet_stream_.begin()), | 
|  | output_events_us_it_(output_events_us->begin()), | 
|  | output_events_us_end_(output_events_us->end()), | 
|  | end_time_us_(end_time_us) { | 
|  | RTC_DCHECK(packet_stream); | 
|  | RTC_DCHECK(output_events_us); | 
|  | } | 
|  |  | 
|  | rtc::Optional<int64_t> NextPacketTime() const override { | 
|  | if (packet_stream_it_ == packet_stream_.end()) { | 
|  | return rtc::nullopt; | 
|  | } | 
|  | if (end_time_us_ && packet_stream_it_->timestamp > *end_time_us_) { | 
|  | return rtc::nullopt; | 
|  | } | 
|  | // Convert from us to ms. | 
|  | return packet_stream_it_->timestamp / 1000; | 
|  | } | 
|  |  | 
|  | rtc::Optional<int64_t> NextOutputEventTime() const override { | 
|  | if (output_events_us_it_ == output_events_us_end_) { | 
|  | return rtc::nullopt; | 
|  | } | 
|  | if (end_time_us_ && *output_events_us_it_ > *end_time_us_) { | 
|  | return rtc::nullopt; | 
|  | } | 
|  | // Convert from us to ms. | 
|  | return rtc::checked_cast<int64_t>(*output_events_us_it_ / 1000); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<PacketData> PopPacket() override { | 
|  | if (packet_stream_it_ == packet_stream_.end()) { | 
|  | return std::unique_ptr<PacketData>(); | 
|  | } | 
|  | std::unique_ptr<PacketData> packet_data(new PacketData()); | 
|  | packet_data->header = packet_stream_it_->header; | 
|  | // Convert from us to ms. | 
|  | packet_data->time_ms = packet_stream_it_->timestamp / 1000.0; | 
|  |  | 
|  | // This is a header-only "dummy" packet. Set the payload to all zeros, with | 
|  | // length according to the virtual length. | 
|  | packet_data->payload.SetSize(packet_stream_it_->total_length); | 
|  | std::fill_n(packet_data->payload.data(), packet_data->payload.size(), 0); | 
|  |  | 
|  | ++packet_stream_it_; | 
|  | return packet_data; | 
|  | } | 
|  |  | 
|  | void AdvanceOutputEvent() override { | 
|  | if (output_events_us_it_ != output_events_us_end_) { | 
|  | ++output_events_us_it_; | 
|  | } | 
|  | } | 
|  |  | 
|  | bool ended() const override { return !NextEventTime(); } | 
|  |  | 
|  | rtc::Optional<RTPHeader> NextHeader() const override { | 
|  | if (packet_stream_it_ == packet_stream_.end()) { | 
|  | return rtc::nullopt; | 
|  | } | 
|  | return packet_stream_it_->header; | 
|  | } | 
|  |  | 
|  | private: | 
|  | const std::vector<LoggedRtpPacket>& packet_stream_; | 
|  | std::vector<LoggedRtpPacket>::const_iterator packet_stream_it_; | 
|  | std::vector<uint64_t>::const_iterator output_events_us_it_; | 
|  | const std::vector<uint64_t>::const_iterator output_events_us_end_; | 
|  | const rtc::Optional<uint64_t> end_time_us_; | 
|  | }; | 
|  |  | 
|  | namespace { | 
|  | // Creates a NetEq test object and all necessary input and output helpers. Runs | 
|  | // the test and returns the NetEqDelayAnalyzer object that was used to | 
|  | // instrument the test. | 
|  | std::unique_ptr<test::NetEqDelayAnalyzer> CreateNetEqTestAndRun( | 
|  | const std::vector<LoggedRtpPacket>* packet_stream, | 
|  | const std::vector<uint64_t>* output_events_us, | 
|  | rtc::Optional<uint64_t> end_time_us, | 
|  | const std::string& replacement_file_name, | 
|  | int file_sample_rate_hz) { | 
|  | std::unique_ptr<test::NetEqInput> input( | 
|  | new NetEqStreamInput(packet_stream, output_events_us, end_time_us)); | 
|  |  | 
|  | constexpr int kReplacementPt = 127; | 
|  | std::set<uint8_t> cn_types; | 
|  | std::set<uint8_t> forbidden_types; | 
|  | input.reset(new test::NetEqReplacementInput(std::move(input), kReplacementPt, | 
|  | cn_types, forbidden_types)); | 
|  |  | 
|  | NetEq::Config config; | 
|  | config.max_packets_in_buffer = 200; | 
|  | config.enable_fast_accelerate = true; | 
|  |  | 
|  | std::unique_ptr<test::VoidAudioSink> output(new test::VoidAudioSink()); | 
|  |  | 
|  | test::NetEqTest::DecoderMap codecs; | 
|  |  | 
|  | // Create a "replacement decoder" that produces the decoded audio by reading | 
|  | // from a file rather than from the encoded payloads. | 
|  | std::unique_ptr<test::ResampleInputAudioFile> replacement_file( | 
|  | new test::ResampleInputAudioFile(replacement_file_name, | 
|  | file_sample_rate_hz)); | 
|  | replacement_file->set_output_rate_hz(48000); | 
|  | std::unique_ptr<AudioDecoder> replacement_decoder( | 
|  | new test::FakeDecodeFromFile(std::move(replacement_file), 48000, false)); | 
|  | test::NetEqTest::ExtDecoderMap ext_codecs; | 
|  | ext_codecs[kReplacementPt] = {replacement_decoder.get(), | 
|  | NetEqDecoder::kDecoderArbitrary, | 
|  | "replacement codec"}; | 
|  |  | 
|  | std::unique_ptr<test::NetEqDelayAnalyzer> delay_cb( | 
|  | new test::NetEqDelayAnalyzer); | 
|  | test::DefaultNetEqTestErrorCallback error_cb; | 
|  | test::NetEqTest::Callbacks callbacks; | 
|  | callbacks.error_callback = &error_cb; | 
|  | callbacks.post_insert_packet = delay_cb.get(); | 
|  | callbacks.get_audio_callback = delay_cb.get(); | 
|  |  | 
|  | test::NetEqTest test(config, codecs, ext_codecs, std::move(input), | 
|  | std::move(output), callbacks); | 
|  | test.Run(); | 
|  | return delay_cb; | 
|  | } | 
|  | }  // namespace | 
|  |  | 
|  | // Plots the jitter buffer delay profile. This will plot only for the first | 
|  | // incoming audio SSRC. If the stream contains more than one incoming audio | 
|  | // SSRC, all but the first will be ignored. | 
|  | void EventLogAnalyzer::CreateAudioJitterBufferGraph( | 
|  | const std::string& replacement_file_name, | 
|  | int file_sample_rate_hz, | 
|  | Plot* plot) { | 
|  | const auto& incoming_audio_kv = std::find_if( | 
|  | rtp_packets_.begin(), rtp_packets_.end(), | 
|  | [this](std::pair<StreamId, std::vector<LoggedRtpPacket>> kv) { | 
|  | return kv.first.GetDirection() == kIncomingPacket && | 
|  | this->IsAudioSsrc(kv.first); | 
|  | }); | 
|  | if (incoming_audio_kv == rtp_packets_.end()) { | 
|  | // No incoming audio stream found. | 
|  | return; | 
|  | } | 
|  |  | 
|  | const uint32_t ssrc = incoming_audio_kv->first.GetSsrc(); | 
|  |  | 
|  | std::map<uint32_t, std::vector<uint64_t>>::const_iterator output_events_it = | 
|  | audio_playout_events_.find(ssrc); | 
|  | if (output_events_it == audio_playout_events_.end()) { | 
|  | // Could not find output events with SSRC matching the input audio stream. | 
|  | // Using the first available stream of output events. | 
|  | output_events_it = audio_playout_events_.cbegin(); | 
|  | } | 
|  |  | 
|  | rtc::Optional<uint64_t> end_time_us = | 
|  | log_segments_.empty() | 
|  | ? rtc::nullopt | 
|  | : rtc::Optional<uint64_t>(log_segments_.front().second); | 
|  |  | 
|  | auto delay_cb = CreateNetEqTestAndRun( | 
|  | &incoming_audio_kv->second, &output_events_it->second, end_time_us, | 
|  | replacement_file_name, file_sample_rate_hz); | 
|  |  | 
|  | std::vector<float> send_times_s; | 
|  | std::vector<float> arrival_delay_ms; | 
|  | std::vector<float> corrected_arrival_delay_ms; | 
|  | std::vector<rtc::Optional<float>> playout_delay_ms; | 
|  | std::vector<rtc::Optional<float>> target_delay_ms; | 
|  | delay_cb->CreateGraphs(&send_times_s, &arrival_delay_ms, | 
|  | &corrected_arrival_delay_ms, &playout_delay_ms, | 
|  | &target_delay_ms); | 
|  | RTC_DCHECK_EQ(send_times_s.size(), arrival_delay_ms.size()); | 
|  | RTC_DCHECK_EQ(send_times_s.size(), corrected_arrival_delay_ms.size()); | 
|  | RTC_DCHECK_EQ(send_times_s.size(), playout_delay_ms.size()); | 
|  | RTC_DCHECK_EQ(send_times_s.size(), target_delay_ms.size()); | 
|  |  | 
|  | std::map<StreamId, TimeSeries> time_series_packet_arrival; | 
|  | std::map<StreamId, TimeSeries> time_series_relative_packet_arrival; | 
|  | std::map<StreamId, TimeSeries> time_series_play_time; | 
|  | std::map<StreamId, TimeSeries> time_series_target_time; | 
|  | float min_y_axis = 0.f; | 
|  | float max_y_axis = 0.f; | 
|  | const StreamId stream_id = incoming_audio_kv->first; | 
|  | for (size_t i = 0; i < send_times_s.size(); ++i) { | 
|  | time_series_packet_arrival[stream_id].points.emplace_back( | 
|  | TimeSeriesPoint(send_times_s[i], arrival_delay_ms[i])); | 
|  | time_series_relative_packet_arrival[stream_id].points.emplace_back( | 
|  | TimeSeriesPoint(send_times_s[i], corrected_arrival_delay_ms[i])); | 
|  | min_y_axis = std::min(min_y_axis, corrected_arrival_delay_ms[i]); | 
|  | max_y_axis = std::max(max_y_axis, corrected_arrival_delay_ms[i]); | 
|  | if (playout_delay_ms[i]) { | 
|  | time_series_play_time[stream_id].points.emplace_back( | 
|  | TimeSeriesPoint(send_times_s[i], *playout_delay_ms[i])); | 
|  | min_y_axis = std::min(min_y_axis, *playout_delay_ms[i]); | 
|  | max_y_axis = std::max(max_y_axis, *playout_delay_ms[i]); | 
|  | } | 
|  | if (target_delay_ms[i]) { | 
|  | time_series_target_time[stream_id].points.emplace_back( | 
|  | TimeSeriesPoint(send_times_s[i], *target_delay_ms[i])); | 
|  | min_y_axis = std::min(min_y_axis, *target_delay_ms[i]); | 
|  | max_y_axis = std::max(max_y_axis, *target_delay_ms[i]); | 
|  | } | 
|  | } | 
|  |  | 
|  | // This code is adapted for a single stream. The creation of the streams above | 
|  | // guarantee that no more than one steam is included. If multiple streams are | 
|  | // to be plotted, they should likely be given distinct labels below. | 
|  | RTC_DCHECK_EQ(time_series_relative_packet_arrival.size(), 1); | 
|  | for (auto& series : time_series_relative_packet_arrival) { | 
|  | series.second.label = "Relative packet arrival delay"; | 
|  | series.second.line_style = LineStyle::kLine; | 
|  | plot->AppendTimeSeries(std::move(series.second)); | 
|  | } | 
|  | RTC_DCHECK_EQ(time_series_play_time.size(), 1); | 
|  | for (auto& series : time_series_play_time) { | 
|  | series.second.label = "Playout delay"; | 
|  | series.second.line_style = LineStyle::kLine; | 
|  | plot->AppendTimeSeries(std::move(series.second)); | 
|  | } | 
|  | RTC_DCHECK_EQ(time_series_target_time.size(), 1); | 
|  | for (auto& series : time_series_target_time) { | 
|  | series.second.label = "Target delay"; | 
|  | series.second.line_style = LineStyle::kLine; | 
|  | series.second.point_style = PointStyle::kHighlight; | 
|  | plot->AppendTimeSeries(std::move(series.second)); | 
|  | } | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetYAxis(min_y_axis, max_y_axis, "Relative delay (ms)", kBottomMargin, | 
|  | kTopMargin); | 
|  | plot->SetTitle("NetEq timing"); | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateIceCandidatePairConfigGraph(Plot* plot) { | 
|  | std::map<uint32_t, TimeSeries> configs_by_cp_id; | 
|  | for (const auto& config : ice_candidate_pair_configs_) { | 
|  | if (configs_by_cp_id.find(config.candidate_pair_id) == | 
|  | configs_by_cp_id.end()) { | 
|  | const std::string candidate_pair_desc = | 
|  | GetCandidatePairLogDescriptionAsString(config); | 
|  | configs_by_cp_id[config.candidate_pair_id] = | 
|  | TimeSeries("[" + std::to_string(config.candidate_pair_id) + "]" + | 
|  | candidate_pair_desc, | 
|  | LineStyle::kNone, PointStyle::kHighlight); | 
|  | candidate_pair_desc_by_id_[config.candidate_pair_id] = | 
|  | candidate_pair_desc; | 
|  | } | 
|  | float x = ToCallTime(config.timestamp); | 
|  | float y = static_cast<float>(config.type); | 
|  | configs_by_cp_id[config.candidate_pair_id].points.emplace_back(x, y); | 
|  | } | 
|  |  | 
|  | // TODO(qingsi): There can be a large number of candidate pairs generated by | 
|  | // certain calls and the frontend cannot render the chart in this case due to | 
|  | // the failure of generating a palette with the same number of colors. | 
|  | for (auto& kv : configs_by_cp_id) { | 
|  | plot->AppendTimeSeries(std::move(kv.second)); | 
|  | } | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 3, "Numeric Config Type", kBottomMargin, | 
|  | kTopMargin); | 
|  | plot->SetTitle("[IceEventLog] ICE candidate pair configs"); | 
|  | } | 
|  |  | 
|  | std::string EventLogAnalyzer::GetCandidatePairLogDescriptionFromId( | 
|  | uint32_t candidate_pair_id) { | 
|  | if (candidate_pair_desc_by_id_.find(candidate_pair_id) != | 
|  | candidate_pair_desc_by_id_.end()) { | 
|  | return candidate_pair_desc_by_id_[candidate_pair_id]; | 
|  | } | 
|  | for (const auto& config : ice_candidate_pair_configs_) { | 
|  | // TODO(qingsi): Add the handling of the "Updated" config event after the | 
|  | // visualization of property change for candidate pairs is introduced. | 
|  | if (candidate_pair_desc_by_id_.find(config.candidate_pair_id) == | 
|  | candidate_pair_desc_by_id_.end()) { | 
|  | const std::string candidate_pair_desc = | 
|  | GetCandidatePairLogDescriptionAsString(config); | 
|  | candidate_pair_desc_by_id_[config.candidate_pair_id] = | 
|  | candidate_pair_desc; | 
|  | } | 
|  | } | 
|  | return candidate_pair_desc_by_id_[candidate_pair_id]; | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::CreateIceConnectivityCheckGraph(Plot* plot) { | 
|  | std::map<uint32_t, TimeSeries> checks_by_cp_id; | 
|  | for (const auto& event : ice_candidate_pair_events_) { | 
|  | if (checks_by_cp_id.find(event.candidate_pair_id) == | 
|  | checks_by_cp_id.end()) { | 
|  | checks_by_cp_id[event.candidate_pair_id] = TimeSeries( | 
|  | "[" + std::to_string(event.candidate_pair_id) + "]" + | 
|  | GetCandidatePairLogDescriptionFromId(event.candidate_pair_id), | 
|  | LineStyle::kNone, PointStyle::kHighlight); | 
|  | } | 
|  | float x = ToCallTime(event.timestamp); | 
|  | float y = static_cast<float>(event.type); | 
|  | checks_by_cp_id[event.candidate_pair_id].points.emplace_back(x, y); | 
|  | } | 
|  |  | 
|  | // TODO(qingsi): The same issue as in CreateIceCandidatePairConfigGraph. | 
|  | for (auto& kv : checks_by_cp_id) { | 
|  | plot->AppendTimeSeries(std::move(kv.second)); | 
|  | } | 
|  |  | 
|  | plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin); | 
|  | plot->SetSuggestedYAxis(0, 4, "Numeric Connectivity State", kBottomMargin, | 
|  | kTopMargin); | 
|  | plot->SetTitle("[IceEventLog] ICE connectivity checks"); | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::Notification( | 
|  | std::unique_ptr<TriageNotification> notification) { | 
|  | notifications_.push_back(std::move(notification)); | 
|  | } | 
|  |  | 
|  | void EventLogAnalyzer::PrintNotifications(FILE* file) { | 
|  | if (notifications_.size() == 0) | 
|  | return; | 
|  | fprintf(file, "========== TRIAGE NOTIFICATIONS ==========\n"); | 
|  | for (const auto& notification : notifications_) { | 
|  | rtc::Optional<float> call_timestamp = notification->Time(); | 
|  | if (call_timestamp.has_value()) { | 
|  | fprintf(file, "%3.3lf s : %s\n", call_timestamp.value(), | 
|  | notification->ToString().c_str()); | 
|  | } else { | 
|  | fprintf(file, "          : %s\n", notification->ToString().c_str()); | 
|  | } | 
|  | } | 
|  | fprintf(file, "========== END TRIAGE NOTIFICATIONS ==========\n"); | 
|  | } | 
|  |  | 
|  | // TODO(terelius): Notifications could possibly be generated by the same code | 
|  | // that produces the graphs. There is some code duplication that could be | 
|  | // avoided, but that might be solved anyway when we move functionality from the | 
|  | // analyzer to the parser. | 
|  | void EventLogAnalyzer::CreateTriageNotifications() { | 
|  | uint64_t end_time_us = log_segments_.empty() | 
|  | ? std::numeric_limits<uint64_t>::max() | 
|  | : log_segments_.front().second; | 
|  | // Check for gaps in sequence numbers and capture timestamps. | 
|  | for (auto& kv : rtp_packets_) { | 
|  | StreamId stream_id = kv.first; | 
|  | const std::vector<LoggedRtpPacket>& packet_stream = kv.second; | 
|  |  | 
|  | SeqNumUnwrapper<uint16_t> seq_no_unwrapper; | 
|  | rtc::Optional<int64_t> last_seq_no; | 
|  | SeqNumUnwrapper<uint32_t> timestamp_unwrapper; | 
|  | rtc::Optional<int64_t> last_timestamp; | 
|  | for (const auto& packet : packet_stream) { | 
|  | if (packet.timestamp > end_time_us) { | 
|  | // Only process the first (LOG_START, LOG_END) segment. | 
|  | break; | 
|  | } | 
|  | int64_t seq_no = seq_no_unwrapper.Unwrap(packet.header.sequenceNumber); | 
|  | if (last_seq_no.has_value() && | 
|  | std::abs(seq_no - last_seq_no.value()) > 1000) { | 
|  | // With roughly 100 packets per second (~800kbps), this would require 10 | 
|  | // seconds without data to trigger incorrectly. | 
|  | if (stream_id.GetDirection() == kIncomingPacket) { | 
|  | Notification(rtc::MakeUnique<IncomingSeqNoJump>( | 
|  | ToCallTime(packet.timestamp), packet.header.ssrc)); | 
|  | } else { | 
|  | Notification(rtc::MakeUnique<OutgoingSeqNoJump>( | 
|  | ToCallTime(packet.timestamp), packet.header.ssrc)); | 
|  | } | 
|  | } | 
|  | last_seq_no.emplace(seq_no); | 
|  | int64_t timestamp = timestamp_unwrapper.Unwrap(packet.header.timestamp); | 
|  | if (last_timestamp.has_value() && | 
|  | std::abs(timestamp - last_timestamp.value()) > 900000) { | 
|  | // With a 90 kHz clock, this would require 10 seconds without data to | 
|  | // trigger incorrectly. | 
|  | if (stream_id.GetDirection() == kIncomingPacket) { | 
|  | Notification(rtc::MakeUnique<IncomingCaptureTimeJump>( | 
|  | ToCallTime(packet.timestamp), packet.header.ssrc)); | 
|  | } else { | 
|  | Notification(rtc::MakeUnique<OutgoingCaptureTimeJump>( | 
|  | ToCallTime(packet.timestamp), packet.header.ssrc)); | 
|  | } | 
|  | } | 
|  | last_timestamp.emplace(timestamp); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Check for gaps in RTP and RTCP streams | 
|  | for (const auto direction : | 
|  | {PacketDirection::kIncomingPacket, PacketDirection::kOutgoingPacket}) { | 
|  | // TODO(terelius): The parser could provide a list of all packets, ordered | 
|  | // by time, for each direction. | 
|  | std::multimap<uint64_t, const LoggedRtpPacket*> rtp_in_direction; | 
|  | for (const auto& kv : rtp_packets_) { | 
|  | if (kv.first.GetDirection() == direction) { | 
|  | for (const LoggedRtpPacket& rtp_packet : kv.second) | 
|  | rtp_in_direction.emplace(rtp_packet.timestamp, &rtp_packet); | 
|  | } | 
|  | } | 
|  | rtc::Optional<uint64_t> last_rtp_packet; | 
|  | for (const auto& kv : rtp_in_direction) { | 
|  | uint64_t timestamp = kv.first; | 
|  | if (timestamp > end_time_us) { | 
|  | // Only process the first (LOG_START, LOG_END) segment. | 
|  | break; | 
|  | } | 
|  | int64_t duration = timestamp - last_rtp_packet.value_or(0); | 
|  | if (last_rtp_packet.has_value() && duration > 500000) { | 
|  | // No incoming packet for more than 500 ms. | 
|  | if (direction == kIncomingPacket) { | 
|  | Notification(rtc::MakeUnique<IncomingRtpReceiveTimeGap>( | 
|  | ToCallTime(timestamp), duration / 1000)); | 
|  | } else { | 
|  | Notification(rtc::MakeUnique<OutgoingRtpSendTimeGap>( | 
|  | ToCallTime(timestamp), duration / 1000)); | 
|  | } | 
|  | } | 
|  | last_rtp_packet.emplace(timestamp); | 
|  | } | 
|  |  | 
|  | // TODO(terelius): The parser could provide a list of all packets, ordered | 
|  | // by time, for each direction. | 
|  | std::multimap<uint64_t, const LoggedRtcpPacket*> rtcp_in_direction; | 
|  | for (const auto& kv : rtcp_packets_) { | 
|  | if (kv.first.GetDirection() == direction) { | 
|  | for (const LoggedRtcpPacket& rtcp_packet : kv.second) | 
|  | rtcp_in_direction.emplace(rtcp_packet.timestamp, &rtcp_packet); | 
|  | } | 
|  | } | 
|  | rtc::Optional<uint64_t> last_incoming_rtcp_packet; | 
|  | for (const auto& kv : rtcp_in_direction) { | 
|  | uint64_t timestamp = kv.first; | 
|  | if (timestamp > end_time_us) { | 
|  | // Only process the first (LOG_START, LOG_END) segment. | 
|  | break; | 
|  | } | 
|  | int64_t duration = timestamp - last_incoming_rtcp_packet.value_or(0); | 
|  | if (last_incoming_rtcp_packet.has_value() && duration > 2000000) { | 
|  | // No incoming feedback for more than 2000 ms. | 
|  | if (direction == kIncomingPacket) { | 
|  | Notification(rtc::MakeUnique<IncomingRtcpReceiveTimeGap>( | 
|  | ToCallTime(timestamp), duration / 1000)); | 
|  | } else { | 
|  | Notification(rtc::MakeUnique<OutgoingRtcpSendTimeGap>( | 
|  | ToCallTime(timestamp), duration / 1000)); | 
|  | } | 
|  | } | 
|  | last_incoming_rtcp_packet.emplace(timestamp); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Loss feedback | 
|  | int64_t total_lost_packets = 0; | 
|  | int64_t total_expected_packets = 0; | 
|  | for (auto& bwe_update : bwe_loss_updates_) { | 
|  | if (bwe_update.timestamp > end_time_us) { | 
|  | // Only process the first (LOG_START, LOG_END) segment. | 
|  | break; | 
|  | } | 
|  | int64_t lost_packets = static_cast<double>(bwe_update.fraction_loss) / 255 * | 
|  | bwe_update.expected_packets; | 
|  | total_lost_packets += lost_packets; | 
|  | total_expected_packets += bwe_update.expected_packets; | 
|  | } | 
|  | double avg_outgoing_loss = | 
|  | static_cast<double>(total_lost_packets) / total_expected_packets; | 
|  | if (avg_outgoing_loss > 0.05) { | 
|  | Notification(rtc::MakeUnique<OutgoingHighLoss>(avg_outgoing_loss)); | 
|  | } | 
|  | } | 
|  |  | 
|  | }  // namespace plotting | 
|  | }  // namespace webrtc |