Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(79)

Side by Side Diff: webrtc/tools/event_log_visualizer/analyzer.cc

Issue 2190013002: Revert of Add BWE plot to event log analyzer. (Closed) Base URL: https://chromium.googlesource.com/external/webrtc.git@master
Patch Set: Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 /* 1 /*
2 * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 2 * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
3 * 3 *
4 * Use of this source code is governed by a BSD-style license 4 * Use of this source code is governed by a BSD-style license
5 * that can be found in the LICENSE file in the root of the source 5 * that can be found in the LICENSE file in the root of the source
6 * tree. An additional intellectual property rights grant can be found 6 * tree. An additional intellectual property rights grant can be found
7 * in the file PATENTS. All contributing project authors may 7 * in the file PATENTS. All contributing project authors may
8 * be found in the AUTHORS file in the root of the source tree. 8 * be found in the AUTHORS file in the root of the source tree.
9 */ 9 */
10 10
11 #include "webrtc/tools/event_log_visualizer/analyzer.h" 11 #include "webrtc/tools/event_log_visualizer/analyzer.h"
12 12
13 #include <algorithm> 13 #include <algorithm>
14 #include <limits> 14 #include <limits>
15 #include <map> 15 #include <map>
16 #include <sstream> 16 #include <sstream>
17 #include <string> 17 #include <string>
18 #include <utility> 18 #include <utility>
19 19
20 #include "webrtc/audio_receive_stream.h" 20 #include "webrtc/audio_receive_stream.h"
21 #include "webrtc/audio_send_stream.h" 21 #include "webrtc/audio_send_stream.h"
22 #include "webrtc/base/checks.h" 22 #include "webrtc/base/checks.h"
23 #include "webrtc/call.h" 23 #include "webrtc/call.h"
24 #include "webrtc/common_types.h" 24 #include "webrtc/common_types.h"
25 #include "webrtc/modules/congestion_controller/include/congestion_controller.h"
26 #include "webrtc/modules/rtp_rtcp/include/rtp_rtcp.h" 25 #include "webrtc/modules/rtp_rtcp/include/rtp_rtcp.h"
27 #include "webrtc/modules/rtp_rtcp/include/rtp_rtcp_defines.h" 26 #include "webrtc/modules/rtp_rtcp/include/rtp_rtcp_defines.h"
28 #include "webrtc/modules/rtp_rtcp/source/rtp_utility.h" 27 #include "webrtc/modules/rtp_rtcp/source/rtp_utility.h"
29 #include "webrtc/modules/rtp_rtcp/source/rtcp_utility.h"
30 #include "webrtc/modules/rtp_rtcp/source/rtcp_packet/transport_feedback.h"
31 #include "webrtc/video_receive_stream.h" 28 #include "webrtc/video_receive_stream.h"
32 #include "webrtc/video_send_stream.h" 29 #include "webrtc/video_send_stream.h"
33 30
34 namespace { 31 namespace {
35 32
36 std::string SsrcToString(uint32_t ssrc) { 33 std::string SsrcToString(uint32_t ssrc) {
37 std::stringstream ss; 34 std::stringstream ss;
38 ss << "SSRC " << ssrc; 35 ss << "SSRC " << ssrc;
39 return ss.str(); 36 return ss.str();
40 } 37 }
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after
88 85
89 namespace webrtc { 86 namespace webrtc {
90 namespace plotting { 87 namespace plotting {
91 88
92 89
93 bool EventLogAnalyzer::StreamId::operator<(const StreamId& other) const { 90 bool EventLogAnalyzer::StreamId::operator<(const StreamId& other) const {
94 if (ssrc_ < other.ssrc_) { 91 if (ssrc_ < other.ssrc_) {
95 return true; 92 return true;
96 } 93 }
97 if (ssrc_ == other.ssrc_) { 94 if (ssrc_ == other.ssrc_) {
98 if (direction_ < other.direction_) { 95 if (media_type_ < other.media_type_) {
99 return true; 96 return true;
100 } 97 }
98 if (media_type_ == other.media_type_) {
99 if (direction_ < other.direction_) {
100 return true;
101 }
102 }
101 } 103 }
102 return false; 104 return false;
103 } 105 }
104 106
105 bool EventLogAnalyzer::StreamId::operator==(const StreamId& other) const { 107 bool EventLogAnalyzer::StreamId::operator==(const StreamId& other) const {
106 return ssrc_ == other.ssrc_ && direction_ == other.direction_; 108 return ssrc_ == other.ssrc_ && direction_ == other.direction_ &&
109 media_type_ == other.media_type_;
107 } 110 }
108 111
109 112
110 EventLogAnalyzer::EventLogAnalyzer(const ParsedRtcEventLog& log) 113 EventLogAnalyzer::EventLogAnalyzer(const ParsedRtcEventLog& log)
111 : parsed_log_(log), window_duration_(250000), step_(10000) { 114 : parsed_log_(log), window_duration_(250000), step_(10000) {
112 uint64_t first_timestamp = std::numeric_limits<uint64_t>::max(); 115 uint64_t first_timestamp = std::numeric_limits<uint64_t>::max();
113 uint64_t last_timestamp = std::numeric_limits<uint64_t>::min(); 116 uint64_t last_timestamp = std::numeric_limits<uint64_t>::min();
114 117
115 // Maps a stream identifier consisting of ssrc and direction 118 // Maps a stream identifier consisting of ssrc, direction and MediaType
116 // to the header extensions used by that stream, 119 // to the header extensions used by that stream,
117 std::map<StreamId, RtpHeaderExtensionMap> extension_maps; 120 std::map<StreamId, RtpHeaderExtensionMap> extension_maps;
118 121
119 PacketDirection direction; 122 PacketDirection direction;
123 MediaType media_type;
120 uint8_t header[IP_PACKET_SIZE]; 124 uint8_t header[IP_PACKET_SIZE];
121 size_t header_length; 125 size_t header_length;
122 size_t total_length; 126 size_t total_length;
123 127
124 for (size_t i = 0; i < parsed_log_.GetNumberOfEvents(); i++) { 128 for (size_t i = 0; i < parsed_log_.GetNumberOfEvents(); i++) {
125 ParsedRtcEventLog::EventType event_type = parsed_log_.GetEventType(i); 129 ParsedRtcEventLog::EventType event_type = parsed_log_.GetEventType(i);
126 if (event_type != ParsedRtcEventLog::VIDEO_RECEIVER_CONFIG_EVENT && 130 if (event_type != ParsedRtcEventLog::VIDEO_RECEIVER_CONFIG_EVENT &&
127 event_type != ParsedRtcEventLog::VIDEO_SENDER_CONFIG_EVENT && 131 event_type != ParsedRtcEventLog::VIDEO_SENDER_CONFIG_EVENT &&
128 event_type != ParsedRtcEventLog::AUDIO_RECEIVER_CONFIG_EVENT && 132 event_type != ParsedRtcEventLog::AUDIO_RECEIVER_CONFIG_EVENT &&
129 event_type != ParsedRtcEventLog::AUDIO_SENDER_CONFIG_EVENT) { 133 event_type != ParsedRtcEventLog::AUDIO_SENDER_CONFIG_EVENT) {
130 uint64_t timestamp = parsed_log_.GetTimestamp(i); 134 uint64_t timestamp = parsed_log_.GetTimestamp(i);
131 first_timestamp = std::min(first_timestamp, timestamp); 135 first_timestamp = std::min(first_timestamp, timestamp);
132 last_timestamp = std::max(last_timestamp, timestamp); 136 last_timestamp = std::max(last_timestamp, timestamp);
133 } 137 }
134 138
135 switch (parsed_log_.GetEventType(i)) { 139 switch (parsed_log_.GetEventType(i)) {
136 case ParsedRtcEventLog::VIDEO_RECEIVER_CONFIG_EVENT: { 140 case ParsedRtcEventLog::VIDEO_RECEIVER_CONFIG_EVENT: {
137 VideoReceiveStream::Config config(nullptr); 141 VideoReceiveStream::Config config(nullptr);
138 parsed_log_.GetVideoReceiveConfig(i, &config); 142 parsed_log_.GetVideoReceiveConfig(i, &config);
139 StreamId stream(config.rtp.remote_ssrc, kIncomingPacket); 143 StreamId stream(config.rtp.remote_ssrc, kIncomingPacket,
144 MediaType::VIDEO);
140 extension_maps[stream].Erase(); 145 extension_maps[stream].Erase();
141 for (size_t j = 0; j < config.rtp.extensions.size(); ++j) { 146 for (size_t j = 0; j < config.rtp.extensions.size(); ++j) {
142 const std::string& extension = config.rtp.extensions[j].uri; 147 const std::string& extension = config.rtp.extensions[j].uri;
143 int id = config.rtp.extensions[j].id; 148 int id = config.rtp.extensions[j].id;
144 extension_maps[stream].Register(StringToRtpExtensionType(extension), 149 extension_maps[stream].Register(StringToRtpExtensionType(extension),
145 id); 150 id);
146 } 151 }
147 break; 152 break;
148 } 153 }
149 case ParsedRtcEventLog::VIDEO_SENDER_CONFIG_EVENT: { 154 case ParsedRtcEventLog::VIDEO_SENDER_CONFIG_EVENT: {
150 VideoSendStream::Config config(nullptr); 155 VideoSendStream::Config config(nullptr);
151 parsed_log_.GetVideoSendConfig(i, &config); 156 parsed_log_.GetVideoSendConfig(i, &config);
152 for (auto ssrc : config.rtp.ssrcs) { 157 for (auto ssrc : config.rtp.ssrcs) {
153 StreamId stream(ssrc, kOutgoingPacket); 158 StreamId stream(ssrc, kOutgoingPacket, MediaType::VIDEO);
154 extension_maps[stream].Erase(); 159 extension_maps[stream].Erase();
155 for (size_t j = 0; j < config.rtp.extensions.size(); ++j) { 160 for (size_t j = 0; j < config.rtp.extensions.size(); ++j) {
156 const std::string& extension = config.rtp.extensions[j].uri; 161 const std::string& extension = config.rtp.extensions[j].uri;
157 int id = config.rtp.extensions[j].id; 162 int id = config.rtp.extensions[j].id;
158 extension_maps[stream].Register(StringToRtpExtensionType(extension), 163 extension_maps[stream].Register(StringToRtpExtensionType(extension),
159 id); 164 id);
160 } 165 }
161 } 166 }
162 break; 167 break;
163 } 168 }
164 case ParsedRtcEventLog::AUDIO_RECEIVER_CONFIG_EVENT: { 169 case ParsedRtcEventLog::AUDIO_RECEIVER_CONFIG_EVENT: {
165 AudioReceiveStream::Config config; 170 AudioReceiveStream::Config config;
166 // TODO(terelius): Parse the audio configs once we have them. 171 // TODO(terelius): Parse the audio configs once we have them.
167 break; 172 break;
168 } 173 }
169 case ParsedRtcEventLog::AUDIO_SENDER_CONFIG_EVENT: { 174 case ParsedRtcEventLog::AUDIO_SENDER_CONFIG_EVENT: {
170 AudioSendStream::Config config(nullptr); 175 AudioSendStream::Config config(nullptr);
171 // TODO(terelius): Parse the audio configs once we have them. 176 // TODO(terelius): Parse the audio configs once we have them.
172 break; 177 break;
173 } 178 }
174 case ParsedRtcEventLog::RTP_EVENT: { 179 case ParsedRtcEventLog::RTP_EVENT: {
175 MediaType media_type;
176 parsed_log_.GetRtpHeader(i, &direction, &media_type, header, 180 parsed_log_.GetRtpHeader(i, &direction, &media_type, header,
177 &header_length, &total_length); 181 &header_length, &total_length);
178 // Parse header to get SSRC. 182 // Parse header to get SSRC.
179 RtpUtility::RtpHeaderParser rtp_parser(header, header_length); 183 RtpUtility::RtpHeaderParser rtp_parser(header, header_length);
180 RTPHeader parsed_header; 184 RTPHeader parsed_header;
181 rtp_parser.Parse(&parsed_header); 185 rtp_parser.Parse(&parsed_header);
182 StreamId stream(parsed_header.ssrc, direction); 186 StreamId stream(parsed_header.ssrc, direction, media_type);
183 // Look up the extension_map and parse it again to get the extensions. 187 // Look up the extension_map and parse it again to get the extensions.
184 if (extension_maps.count(stream) == 1) { 188 if (extension_maps.count(stream) == 1) {
185 RtpHeaderExtensionMap* extension_map = &extension_maps[stream]; 189 RtpHeaderExtensionMap* extension_map = &extension_maps[stream];
186 rtp_parser.Parse(&parsed_header, extension_map); 190 rtp_parser.Parse(&parsed_header, extension_map);
187 } 191 }
188 uint64_t timestamp = parsed_log_.GetTimestamp(i); 192 uint64_t timestamp = parsed_log_.GetTimestamp(i);
189 rtp_packets_[stream].push_back( 193 rtp_packets_[stream].push_back(
190 LoggedRtpPacket(timestamp, parsed_header, total_length)); 194 LoggedRtpPacket(timestamp, parsed_header));
191 break; 195 break;
192 } 196 }
193 case ParsedRtcEventLog::RTCP_EVENT: { 197 case ParsedRtcEventLog::RTCP_EVENT: {
194 uint8_t packet[IP_PACKET_SIZE];
195 MediaType media_type;
196 parsed_log_.GetRtcpPacket(i, &direction, &media_type, packet,
197 &total_length);
198
199 RtpUtility::RtpHeaderParser rtp_parser(packet, total_length);
200 RTPHeader parsed_header;
201 RTC_CHECK(rtp_parser.ParseRtcp(&parsed_header));
202 uint32_t ssrc = parsed_header.ssrc;
203
204 RTCPUtility::RTCPParserV2 rtcp_parser(packet, total_length, true);
205 RTC_CHECK(rtcp_parser.IsValid());
206
207 RTCPUtility::RTCPPacketTypes packet_type = rtcp_parser.Begin();
208 while (packet_type != RTCPUtility::RTCPPacketTypes::kInvalid) {
209 switch (packet_type) {
210 case RTCPUtility::RTCPPacketTypes::kTransportFeedback: {
211 // Currently feedback is logged twice, both for audio and video.
212 // Only act on one of them.
213 if (media_type == MediaType::VIDEO) {
214 std::unique_ptr<rtcp::RtcpPacket> rtcp_packet(
215 rtcp_parser.ReleaseRtcpPacket());
216 StreamId stream(ssrc, direction);
217 uint64_t timestamp = parsed_log_.GetTimestamp(i);
218 rtcp_packets_[stream].push_back(LoggedRtcpPacket(
219 timestamp, kRtcpTransportFeedback, std::move(rtcp_packet)));
220 }
221 break;
222 }
223 default:
224 break;
225 }
226 rtcp_parser.Iterate();
227 packet_type = rtcp_parser.PacketType();
228 }
229 break; 198 break;
230 } 199 }
231 case ParsedRtcEventLog::LOG_START: { 200 case ParsedRtcEventLog::LOG_START: {
232 break; 201 break;
233 } 202 }
234 case ParsedRtcEventLog::LOG_END: { 203 case ParsedRtcEventLog::LOG_END: {
235 break; 204 break;
236 } 205 }
237 case ParsedRtcEventLog::BWE_PACKET_LOSS_EVENT: { 206 case ParsedRtcEventLog::BWE_PACKET_LOSS_EVENT: {
238 BwePacketLossEvent bwe_update; 207 BwePacketLossEvent bwe_update;
(...skipping 17 matching lines...) Expand all
256 } 225 }
257 226
258 if (last_timestamp < first_timestamp) { 227 if (last_timestamp < first_timestamp) {
259 // No useful events in the log. 228 // No useful events in the log.
260 first_timestamp = last_timestamp = 0; 229 first_timestamp = last_timestamp = 0;
261 } 230 }
262 begin_time_ = first_timestamp; 231 begin_time_ = first_timestamp;
263 end_time_ = last_timestamp; 232 end_time_ = last_timestamp;
264 } 233 }
265 234
266 class BitrateObserver : public CongestionController::Observer,
267 public RemoteBitrateObserver {
268 public:
269 BitrateObserver() : last_bitrate_bps_(0), bitrate_updated_(false) {}
270
271 void OnNetworkChanged(uint32_t bitrate_bps,
272 uint8_t fraction_loss,
273 int64_t rtt_ms) override {
274 last_bitrate_bps_ = bitrate_bps;
275 bitrate_updated_ = true;
276 }
277
278 void OnReceiveBitrateChanged(const std::vector<uint32_t>& ssrcs,
279 uint32_t bitrate) override {}
280
281 uint32_t last_bitrate_bps() const { return last_bitrate_bps_; }
282 bool GetAndResetBitrateUpdated() {
283 bool bitrate_updated = bitrate_updated_;
284 bitrate_updated_ = false;
285 return bitrate_updated;
286 }
287
288 private:
289 uint32_t last_bitrate_bps_;
290 bool bitrate_updated_;
291 };
292
293 void EventLogAnalyzer::CreatePacketGraph(PacketDirection desired_direction, 235 void EventLogAnalyzer::CreatePacketGraph(PacketDirection desired_direction,
294 Plot* plot) { 236 Plot* plot) {
295 std::map<uint32_t, TimeSeries> time_series; 237 std::map<uint32_t, TimeSeries> time_series;
296 238
297 PacketDirection direction; 239 PacketDirection direction;
298 MediaType media_type; 240 MediaType media_type;
299 uint8_t header[IP_PACKET_SIZE]; 241 uint8_t header[IP_PACKET_SIZE];
300 size_t header_length, total_length; 242 size_t header_length, total_length;
301 float max_y = 0; 243 float max_y = 0;
302 244
(...skipping 423 matching lines...) Expand 10 before | Expand all | Expand 10 after
726 plot->yaxis_min = kDefaultYMin; 668 plot->yaxis_min = kDefaultYMin;
727 plot->yaxis_max = max_y * kYMargin; 669 plot->yaxis_max = max_y * kYMargin;
728 plot->yaxis_label = "Bitrate (kbps)"; 670 plot->yaxis_label = "Bitrate (kbps)";
729 if (desired_direction == webrtc::PacketDirection::kIncomingPacket) { 671 if (desired_direction == webrtc::PacketDirection::kIncomingPacket) {
730 plot->title = "Incoming bitrate per stream"; 672 plot->title = "Incoming bitrate per stream";
731 } else if (desired_direction == webrtc::PacketDirection::kOutgoingPacket) { 673 } else if (desired_direction == webrtc::PacketDirection::kOutgoingPacket) {
732 plot->title = "Outgoing bitrate per stream"; 674 plot->title = "Outgoing bitrate per stream";
733 } 675 }
734 } 676 }
735 677
736 void EventLogAnalyzer::CreateBweGraph(Plot* plot) {
737 std::map<uint64_t, const LoggedRtpPacket*> outgoing_rtp;
738 std::map<uint64_t, const LoggedRtcpPacket*> incoming_rtcp;
739
740 for (const auto& kv : rtp_packets_) {
741 if (kv.first.GetDirection() == PacketDirection::kOutgoingPacket) {
742 for (const LoggedRtpPacket& rtp_packet : kv.second)
743 outgoing_rtp.insert(std::make_pair(rtp_packet.timestamp, &rtp_packet));
744 }
745 }
746
747 for (const auto& kv : rtcp_packets_) {
748 if (kv.first.GetDirection() == PacketDirection::kIncomingPacket) {
749 for (const LoggedRtcpPacket& rtcp_packet : kv.second)
750 incoming_rtcp.insert(
751 std::make_pair(rtcp_packet.timestamp, &rtcp_packet));
752 }
753 }
754
755 SimulatedClock clock(0);
756 BitrateObserver observer;
757 RtcEventLogNullImpl null_event_log;
758 CongestionController cc(&clock, &observer, &observer, &null_event_log);
759 // TODO(holmer): Log the call config and use that here instead.
760 static const uint32_t kDefaultStartBitrateBps = 300000;
761 cc.SetBweBitrates(0, kDefaultStartBitrateBps, -1);
762
763 TimeSeries time_series;
764 time_series.label = "BWE";
765 time_series.style = LINE_DOT_GRAPH;
766 uint32_t max_y = 10;
767 uint32_t min_y = 0;
768
769 auto rtp_iterator = outgoing_rtp.begin();
770 auto rtcp_iterator = incoming_rtcp.begin();
771
772 auto NextRtpTime = [&]() {
773 if (rtp_iterator != outgoing_rtp.end())
774 return static_cast<int64_t>(rtp_iterator->first);
775 return std::numeric_limits<int64_t>::max();
776 };
777
778 auto NextRtcpTime = [&]() {
779 if (rtcp_iterator != incoming_rtcp.end())
780 return static_cast<int64_t>(rtcp_iterator->first);
781 return std::numeric_limits<int64_t>::max();
782 };
783
784 auto NextProcessTime = [&]() {
785 if (rtcp_iterator != incoming_rtcp.end() ||
786 rtp_iterator != outgoing_rtp.end()) {
787 return clock.TimeInMicroseconds() +
788 std::max<int64_t>(cc.TimeUntilNextProcess() * 1000, 0);
789 }
790 return std::numeric_limits<int64_t>::max();
791 };
792
793 int64_t time_us = std::min(NextRtpTime(), NextRtcpTime());
794 while (time_us != std::numeric_limits<int64_t>::max()) {
795 clock.AdvanceTimeMicroseconds(time_us - clock.TimeInMicroseconds());
796 if (clock.TimeInMicroseconds() >= NextRtcpTime()) {
797 clock.AdvanceTimeMilliseconds(rtcp_iterator->first / 1000 -
798 clock.TimeInMilliseconds());
799 const LoggedRtcpPacket& rtcp = *rtcp_iterator->second;
800 if (rtcp.type == kRtcpTransportFeedback) {
801 cc.GetTransportFeedbackObserver()->OnTransportFeedback(
802 *static_cast<rtcp::TransportFeedback*>(rtcp.packet.get()));
803 }
804 ++rtcp_iterator;
805 }
806 if (clock.TimeInMicroseconds() >= NextRtpTime()) {
807 clock.AdvanceTimeMilliseconds(rtp_iterator->first / 1000 -
808 clock.TimeInMilliseconds());
809 const LoggedRtpPacket& rtp = *rtp_iterator->second;
810 if (rtp.header.extension.hasTransportSequenceNumber) {
811 RTC_DCHECK(rtp.header.extension.hasTransportSequenceNumber);
812 cc.GetTransportFeedbackObserver()->AddPacket(
813 rtp.header.extension.transportSequenceNumber, rtp.total_length, 0);
814 rtc::SentPacket sent_packet(
815 rtp.header.extension.transportSequenceNumber, rtp.timestamp / 1000);
816 cc.OnSentPacket(sent_packet);
817 }
818 ++rtp_iterator;
819 }
820 if (clock.TimeInMicroseconds() >= NextProcessTime())
821 cc.Process();
822 if (observer.GetAndResetBitrateUpdated()) {
823 uint32_t y = observer.last_bitrate_bps() / 1000;
824 max_y = std::max(max_y, y);
825 min_y = std::min(min_y, y);
826 float x = static_cast<float>(clock.TimeInMicroseconds() - begin_time_) /
827 1000000;
828 time_series.points.emplace_back(x, y);
829 }
830 time_us = std::min({NextRtpTime(), NextRtcpTime(), NextProcessTime()});
831 }
832 // Add the data set to the plot.
833 plot->series.push_back(std::move(time_series));
834
835 plot->xaxis_min = kDefaultXMin;
836 plot->xaxis_max = (end_time_ - begin_time_) / 1000000 * kXMargin;
837 plot->xaxis_label = "Time (s)";
838 plot->yaxis_min = min_y - (kYMargin - 1) / 2 * (max_y - min_y);
839 plot->yaxis_max = max_y + (kYMargin - 1) / 2 * (max_y - min_y);
840 plot->yaxis_label = "Bitrate (kbps)";
841 plot->title = "BWE";
842 }
843
844 } // namespace plotting 678 } // namespace plotting
845 } // namespace webrtc 679 } // namespace webrtc
OLDNEW
« no previous file with comments | « webrtc/tools/event_log_visualizer/analyzer.h ('k') | webrtc/tools/event_log_visualizer/generate_timeseries.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698