OLD | NEW |
| (Empty) |
1 /* | |
2 * libjingle | |
3 * Copyright 2012 Google Inc. | |
4 * | |
5 * Redistribution and use in source and binary forms, with or without | |
6 * modification, are permitted provided that the following conditions are met: | |
7 * | |
8 * 1. Redistributions of source code must retain the above copyright notice, | |
9 * this list of conditions and the following disclaimer. | |
10 * 2. Redistributions in binary form must reproduce the above copyright notice, | |
11 * this list of conditions and the following disclaimer in the documentation | |
12 * and/or other materials provided with the distribution. | |
13 * 3. The name of the author may not be used to endorse or promote products | |
14 * derived from this software without specific prior written permission. | |
15 * | |
16 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED | |
17 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF | |
18 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO | |
19 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
20 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
21 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; | |
22 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, | |
23 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR | |
24 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF | |
25 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
26 */ | |
27 | |
28 #include "talk/app/webrtc/statscollector.h" | |
29 | |
30 #include <utility> | |
31 #include <vector> | |
32 | |
33 #include "talk/app/webrtc/peerconnection.h" | |
34 #include "talk/session/media/channel.h" | |
35 #include "webrtc/base/base64.h" | |
36 #include "webrtc/base/checks.h" | |
37 #include "webrtc/base/scoped_ptr.h" | |
38 #include "webrtc/base/timing.h" | |
39 | |
40 using rtc::scoped_ptr; | |
41 | |
42 namespace webrtc { | |
43 namespace { | |
44 | |
45 // The following is the enum RTCStatsIceCandidateType from | |
46 // http://w3c.github.io/webrtc-stats/#rtcstatsicecandidatetype-enum such that | |
47 // our stats report for ice candidate type could conform to that. | |
48 const char STATSREPORT_LOCAL_PORT_TYPE[] = "host"; | |
49 const char STATSREPORT_STUN_PORT_TYPE[] = "serverreflexive"; | |
50 const char STATSREPORT_PRFLX_PORT_TYPE[] = "peerreflexive"; | |
51 const char STATSREPORT_RELAY_PORT_TYPE[] = "relayed"; | |
52 | |
53 // Strings used by the stats collector to report adapter types. This fits the | |
54 // general stype of http://w3c.github.io/webrtc-stats than what | |
55 // AdapterTypeToString does. | |
56 const char* STATSREPORT_ADAPTER_TYPE_ETHERNET = "lan"; | |
57 const char* STATSREPORT_ADAPTER_TYPE_WIFI = "wlan"; | |
58 const char* STATSREPORT_ADAPTER_TYPE_WWAN = "wwan"; | |
59 const char* STATSREPORT_ADAPTER_TYPE_VPN = "vpn"; | |
60 const char* STATSREPORT_ADAPTER_TYPE_LOOPBACK = "loopback"; | |
61 | |
62 template<typename ValueType> | |
63 struct TypeForAdd { | |
64 const StatsReport::StatsValueName name; | |
65 const ValueType& value; | |
66 }; | |
67 | |
68 typedef TypeForAdd<bool> BoolForAdd; | |
69 typedef TypeForAdd<float> FloatForAdd; | |
70 typedef TypeForAdd<int64_t> Int64ForAdd; | |
71 typedef TypeForAdd<int> IntForAdd; | |
72 | |
73 StatsReport::Id GetTransportIdFromProxy(const ProxyTransportMap& map, | |
74 const std::string& proxy) { | |
75 RTC_DCHECK(!proxy.empty()); | |
76 auto found = map.find(proxy); | |
77 if (found == map.end()) { | |
78 return StatsReport::Id(); | |
79 } | |
80 | |
81 return StatsReport::NewComponentId( | |
82 found->second, cricket::ICE_CANDIDATE_COMPONENT_RTP); | |
83 } | |
84 | |
85 StatsReport* AddTrackReport(StatsCollection* reports, | |
86 const std::string& track_id) { | |
87 // Adds an empty track report. | |
88 StatsReport::Id id( | |
89 StatsReport::NewTypedId(StatsReport::kStatsReportTypeTrack, track_id)); | |
90 StatsReport* report = reports->ReplaceOrAddNew(id); | |
91 report->AddString(StatsReport::kStatsValueNameTrackId, track_id); | |
92 return report; | |
93 } | |
94 | |
95 template <class TrackVector> | |
96 void CreateTrackReports(const TrackVector& tracks, StatsCollection* reports, | |
97 TrackIdMap& track_ids) { | |
98 for (const auto& track : tracks) { | |
99 const std::string& track_id = track->id(); | |
100 StatsReport* report = AddTrackReport(reports, track_id); | |
101 RTC_DCHECK(report != nullptr); | |
102 track_ids[track_id] = report; | |
103 } | |
104 } | |
105 | |
106 void ExtractCommonSendProperties(const cricket::MediaSenderInfo& info, | |
107 StatsReport* report) { | |
108 report->AddString(StatsReport::kStatsValueNameCodecName, info.codec_name); | |
109 report->AddInt64(StatsReport::kStatsValueNameBytesSent, info.bytes_sent); | |
110 report->AddInt64(StatsReport::kStatsValueNameRtt, info.rtt_ms); | |
111 } | |
112 | |
113 void ExtractCommonReceiveProperties(const cricket::MediaReceiverInfo& info, | |
114 StatsReport* report) { | |
115 report->AddString(StatsReport::kStatsValueNameCodecName, info.codec_name); | |
116 } | |
117 | |
118 void SetAudioProcessingStats(StatsReport* report, | |
119 bool typing_noise_detected, | |
120 int echo_return_loss, | |
121 int echo_return_loss_enhancement, | |
122 int echo_delay_median_ms, | |
123 float aec_quality_min, | |
124 int echo_delay_std_ms) { | |
125 report->AddBoolean(StatsReport::kStatsValueNameTypingNoiseState, | |
126 typing_noise_detected); | |
127 report->AddFloat(StatsReport::kStatsValueNameEchoCancellationQualityMin, | |
128 aec_quality_min); | |
129 const IntForAdd ints[] = { | |
130 { StatsReport::kStatsValueNameEchoReturnLoss, echo_return_loss }, | |
131 { StatsReport::kStatsValueNameEchoReturnLossEnhancement, | |
132 echo_return_loss_enhancement }, | |
133 { StatsReport::kStatsValueNameEchoDelayMedian, echo_delay_median_ms }, | |
134 { StatsReport::kStatsValueNameEchoDelayStdDev, echo_delay_std_ms }, | |
135 }; | |
136 for (const auto& i : ints) | |
137 report->AddInt(i.name, i.value); | |
138 } | |
139 | |
140 void ExtractStats(const cricket::VoiceReceiverInfo& info, StatsReport* report) { | |
141 ExtractCommonReceiveProperties(info, report); | |
142 const FloatForAdd floats[] = { | |
143 { StatsReport::kStatsValueNameExpandRate, info.expand_rate }, | |
144 { StatsReport::kStatsValueNameSecondaryDecodedRate, | |
145 info.secondary_decoded_rate }, | |
146 { StatsReport::kStatsValueNameSpeechExpandRate, info.speech_expand_rate }, | |
147 { StatsReport::kStatsValueNameAccelerateRate, info.accelerate_rate }, | |
148 { StatsReport::kStatsValueNamePreemptiveExpandRate, | |
149 info.preemptive_expand_rate }, | |
150 }; | |
151 | |
152 const IntForAdd ints[] = { | |
153 { StatsReport::kStatsValueNameAudioOutputLevel, info.audio_level }, | |
154 { StatsReport::kStatsValueNameCurrentDelayMs, info.delay_estimate_ms }, | |
155 { StatsReport::kStatsValueNameDecodingCNG, info.decoding_cng }, | |
156 { StatsReport::kStatsValueNameDecodingCTN, info.decoding_calls_to_neteq }, | |
157 { StatsReport::kStatsValueNameDecodingCTSG, | |
158 info.decoding_calls_to_silence_generator }, | |
159 { StatsReport::kStatsValueNameDecodingNormal, info.decoding_normal }, | |
160 { StatsReport::kStatsValueNameDecodingPLC, info.decoding_plc }, | |
161 { StatsReport::kStatsValueNameDecodingPLCCNG, info.decoding_plc_cng }, | |
162 { StatsReport::kStatsValueNameJitterBufferMs, info.jitter_buffer_ms }, | |
163 { StatsReport::kStatsValueNameJitterReceived, info.jitter_ms }, | |
164 { StatsReport::kStatsValueNamePacketsLost, info.packets_lost }, | |
165 { StatsReport::kStatsValueNamePacketsReceived, info.packets_rcvd }, | |
166 { StatsReport::kStatsValueNamePreferredJitterBufferMs, | |
167 info.jitter_buffer_preferred_ms }, | |
168 }; | |
169 | |
170 for (const auto& f : floats) | |
171 report->AddFloat(f.name, f.value); | |
172 | |
173 for (const auto& i : ints) | |
174 report->AddInt(i.name, i.value); | |
175 | |
176 report->AddInt64(StatsReport::kStatsValueNameBytesReceived, | |
177 info.bytes_rcvd); | |
178 report->AddInt64(StatsReport::kStatsValueNameCaptureStartNtpTimeMs, | |
179 info.capture_start_ntp_time_ms); | |
180 } | |
181 | |
182 void ExtractStats(const cricket::VoiceSenderInfo& info, StatsReport* report) { | |
183 ExtractCommonSendProperties(info, report); | |
184 | |
185 SetAudioProcessingStats( | |
186 report, info.typing_noise_detected, info.echo_return_loss, | |
187 info.echo_return_loss_enhancement, info.echo_delay_median_ms, | |
188 info.aec_quality_min, info.echo_delay_std_ms); | |
189 | |
190 RTC_DCHECK_GE(info.audio_level, 0); | |
191 const IntForAdd ints[] = { | |
192 { StatsReport::kStatsValueNameAudioInputLevel, info.audio_level}, | |
193 { StatsReport::kStatsValueNameJitterReceived, info.jitter_ms }, | |
194 { StatsReport::kStatsValueNamePacketsLost, info.packets_lost }, | |
195 { StatsReport::kStatsValueNamePacketsSent, info.packets_sent }, | |
196 }; | |
197 | |
198 for (const auto& i : ints) | |
199 report->AddInt(i.name, i.value); | |
200 } | |
201 | |
202 void ExtractStats(const cricket::VideoReceiverInfo& info, StatsReport* report) { | |
203 ExtractCommonReceiveProperties(info, report); | |
204 report->AddString(StatsReport::kStatsValueNameCodecImplementationName, | |
205 info.decoder_implementation_name); | |
206 report->AddInt64(StatsReport::kStatsValueNameBytesReceived, | |
207 info.bytes_rcvd); | |
208 report->AddInt64(StatsReport::kStatsValueNameCaptureStartNtpTimeMs, | |
209 info.capture_start_ntp_time_ms); | |
210 const IntForAdd ints[] = { | |
211 { StatsReport::kStatsValueNameCurrentDelayMs, info.current_delay_ms }, | |
212 { StatsReport::kStatsValueNameDecodeMs, info.decode_ms }, | |
213 { StatsReport::kStatsValueNameFirsSent, info.firs_sent }, | |
214 { StatsReport::kStatsValueNameFrameHeightReceived, info.frame_height }, | |
215 { StatsReport::kStatsValueNameFrameRateDecoded, info.framerate_decoded }, | |
216 { StatsReport::kStatsValueNameFrameRateOutput, info.framerate_output }, | |
217 { StatsReport::kStatsValueNameFrameRateReceived, info.framerate_rcvd }, | |
218 { StatsReport::kStatsValueNameFrameWidthReceived, info.frame_width }, | |
219 { StatsReport::kStatsValueNameJitterBufferMs, info.jitter_buffer_ms }, | |
220 { StatsReport::kStatsValueNameMaxDecodeMs, info.max_decode_ms }, | |
221 { StatsReport::kStatsValueNameMinPlayoutDelayMs, | |
222 info.min_playout_delay_ms }, | |
223 { StatsReport::kStatsValueNameNacksSent, info.nacks_sent }, | |
224 { StatsReport::kStatsValueNamePacketsLost, info.packets_lost }, | |
225 { StatsReport::kStatsValueNamePacketsReceived, info.packets_rcvd }, | |
226 { StatsReport::kStatsValueNamePlisSent, info.plis_sent }, | |
227 { StatsReport::kStatsValueNameRenderDelayMs, info.render_delay_ms }, | |
228 { StatsReport::kStatsValueNameTargetDelayMs, info.target_delay_ms }, | |
229 }; | |
230 | |
231 for (const auto& i : ints) | |
232 report->AddInt(i.name, i.value); | |
233 } | |
234 | |
235 void ExtractStats(const cricket::VideoSenderInfo& info, StatsReport* report) { | |
236 ExtractCommonSendProperties(info, report); | |
237 | |
238 report->AddString(StatsReport::kStatsValueNameCodecImplementationName, | |
239 info.encoder_implementation_name); | |
240 report->AddBoolean(StatsReport::kStatsValueNameBandwidthLimitedResolution, | |
241 (info.adapt_reason & 0x2) > 0); | |
242 report->AddBoolean(StatsReport::kStatsValueNameCpuLimitedResolution, | |
243 (info.adapt_reason & 0x1) > 0); | |
244 report->AddBoolean(StatsReport::kStatsValueNameViewLimitedResolution, | |
245 (info.adapt_reason & 0x4) > 0); | |
246 | |
247 const IntForAdd ints[] = { | |
248 { StatsReport::kStatsValueNameAdaptationChanges, info.adapt_changes }, | |
249 { StatsReport::kStatsValueNameAvgEncodeMs, info.avg_encode_ms }, | |
250 { StatsReport::kStatsValueNameEncodeUsagePercent, | |
251 info.encode_usage_percent }, | |
252 { StatsReport::kStatsValueNameFirsReceived, info.firs_rcvd }, | |
253 { StatsReport::kStatsValueNameFrameHeightInput, info.input_frame_height }, | |
254 { StatsReport::kStatsValueNameFrameHeightSent, info.send_frame_height }, | |
255 { StatsReport::kStatsValueNameFrameRateInput, info.framerate_input }, | |
256 { StatsReport::kStatsValueNameFrameRateSent, info.framerate_sent }, | |
257 { StatsReport::kStatsValueNameFrameWidthInput, info.input_frame_width }, | |
258 { StatsReport::kStatsValueNameFrameWidthSent, info.send_frame_width }, | |
259 { StatsReport::kStatsValueNameNacksReceived, info.nacks_rcvd }, | |
260 { StatsReport::kStatsValueNamePacketsLost, info.packets_lost }, | |
261 { StatsReport::kStatsValueNamePacketsSent, info.packets_sent }, | |
262 { StatsReport::kStatsValueNamePlisReceived, info.plis_rcvd }, | |
263 }; | |
264 | |
265 for (const auto& i : ints) | |
266 report->AddInt(i.name, i.value); | |
267 } | |
268 | |
269 void ExtractStats(const cricket::BandwidthEstimationInfo& info, | |
270 double stats_gathering_started, | |
271 PeerConnectionInterface::StatsOutputLevel level, | |
272 StatsReport* report) { | |
273 RTC_DCHECK(report->type() == StatsReport::kStatsReportTypeBwe); | |
274 | |
275 report->set_timestamp(stats_gathering_started); | |
276 const IntForAdd ints[] = { | |
277 { StatsReport::kStatsValueNameAvailableSendBandwidth, | |
278 info.available_send_bandwidth }, | |
279 { StatsReport::kStatsValueNameAvailableReceiveBandwidth, | |
280 info.available_recv_bandwidth }, | |
281 { StatsReport::kStatsValueNameTargetEncBitrate, info.target_enc_bitrate }, | |
282 { StatsReport::kStatsValueNameActualEncBitrate, info.actual_enc_bitrate }, | |
283 { StatsReport::kStatsValueNameRetransmitBitrate, info.retransmit_bitrate }, | |
284 { StatsReport::kStatsValueNameTransmitBitrate, info.transmit_bitrate }, | |
285 }; | |
286 for (const auto& i : ints) | |
287 report->AddInt(i.name, i.value); | |
288 report->AddInt64(StatsReport::kStatsValueNameBucketDelay, info.bucket_delay); | |
289 } | |
290 | |
291 void ExtractRemoteStats(const cricket::MediaSenderInfo& info, | |
292 StatsReport* report) { | |
293 report->set_timestamp(info.remote_stats[0].timestamp); | |
294 // TODO(hta): Extract some stats here. | |
295 } | |
296 | |
297 void ExtractRemoteStats(const cricket::MediaReceiverInfo& info, | |
298 StatsReport* report) { | |
299 report->set_timestamp(info.remote_stats[0].timestamp); | |
300 // TODO(hta): Extract some stats here. | |
301 } | |
302 | |
303 // Template to extract stats from a data vector. | |
304 // In order to use the template, the functions that are called from it, | |
305 // ExtractStats and ExtractRemoteStats, must be defined and overloaded | |
306 // for each type. | |
307 template<typename T> | |
308 void ExtractStatsFromList(const std::vector<T>& data, | |
309 const StatsReport::Id& transport_id, | |
310 StatsCollector* collector, | |
311 StatsReport::Direction direction) { | |
312 for (const auto& d : data) { | |
313 uint32_t ssrc = d.ssrc(); | |
314 // Each track can have stats for both local and remote objects. | |
315 // TODO(hta): Handle the case of multiple SSRCs per object. | |
316 StatsReport* report = collector->PrepareReport(true, ssrc, transport_id, | |
317 direction); | |
318 if (report) | |
319 ExtractStats(d, report); | |
320 | |
321 if (!d.remote_stats.empty()) { | |
322 report = collector->PrepareReport(false, ssrc, transport_id, direction); | |
323 if (report) | |
324 ExtractRemoteStats(d, report); | |
325 } | |
326 } | |
327 } | |
328 | |
329 } // namespace | |
330 | |
331 const char* IceCandidateTypeToStatsType(const std::string& candidate_type) { | |
332 if (candidate_type == cricket::LOCAL_PORT_TYPE) { | |
333 return STATSREPORT_LOCAL_PORT_TYPE; | |
334 } | |
335 if (candidate_type == cricket::STUN_PORT_TYPE) { | |
336 return STATSREPORT_STUN_PORT_TYPE; | |
337 } | |
338 if (candidate_type == cricket::PRFLX_PORT_TYPE) { | |
339 return STATSREPORT_PRFLX_PORT_TYPE; | |
340 } | |
341 if (candidate_type == cricket::RELAY_PORT_TYPE) { | |
342 return STATSREPORT_RELAY_PORT_TYPE; | |
343 } | |
344 RTC_DCHECK(false); | |
345 return "unknown"; | |
346 } | |
347 | |
348 const char* AdapterTypeToStatsType(rtc::AdapterType type) { | |
349 switch (type) { | |
350 case rtc::ADAPTER_TYPE_UNKNOWN: | |
351 return "unknown"; | |
352 case rtc::ADAPTER_TYPE_ETHERNET: | |
353 return STATSREPORT_ADAPTER_TYPE_ETHERNET; | |
354 case rtc::ADAPTER_TYPE_WIFI: | |
355 return STATSREPORT_ADAPTER_TYPE_WIFI; | |
356 case rtc::ADAPTER_TYPE_CELLULAR: | |
357 return STATSREPORT_ADAPTER_TYPE_WWAN; | |
358 case rtc::ADAPTER_TYPE_VPN: | |
359 return STATSREPORT_ADAPTER_TYPE_VPN; | |
360 case rtc::ADAPTER_TYPE_LOOPBACK: | |
361 return STATSREPORT_ADAPTER_TYPE_LOOPBACK; | |
362 default: | |
363 RTC_DCHECK(false); | |
364 return ""; | |
365 } | |
366 } | |
367 | |
368 StatsCollector::StatsCollector(PeerConnection* pc) | |
369 : pc_(pc), stats_gathering_started_(0) { | |
370 RTC_DCHECK(pc_); | |
371 } | |
372 | |
373 StatsCollector::~StatsCollector() { | |
374 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
375 } | |
376 | |
377 double StatsCollector::GetTimeNow() { | |
378 return rtc::Timing::WallTimeNow() * rtc::kNumMillisecsPerSec; | |
379 } | |
380 | |
381 // Adds a MediaStream with tracks that can be used as a |selector| in a call | |
382 // to GetStats. | |
383 void StatsCollector::AddStream(MediaStreamInterface* stream) { | |
384 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
385 RTC_DCHECK(stream != NULL); | |
386 | |
387 CreateTrackReports<AudioTrackVector>(stream->GetAudioTracks(), | |
388 &reports_, track_ids_); | |
389 CreateTrackReports<VideoTrackVector>(stream->GetVideoTracks(), | |
390 &reports_, track_ids_); | |
391 } | |
392 | |
393 void StatsCollector::AddLocalAudioTrack(AudioTrackInterface* audio_track, | |
394 uint32_t ssrc) { | |
395 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
396 RTC_DCHECK(audio_track != NULL); | |
397 #if (!defined(NDEBUG) || defined(DCHECK_ALWAYS_ON)) | |
398 for (const auto& track : local_audio_tracks_) | |
399 RTC_DCHECK(track.first != audio_track || track.second != ssrc); | |
400 #endif | |
401 | |
402 local_audio_tracks_.push_back(std::make_pair(audio_track, ssrc)); | |
403 | |
404 // Create the kStatsReportTypeTrack report for the new track if there is no | |
405 // report yet. | |
406 StatsReport::Id id(StatsReport::NewTypedId(StatsReport::kStatsReportTypeTrack, | |
407 audio_track->id())); | |
408 StatsReport* report = reports_.Find(id); | |
409 if (!report) { | |
410 report = reports_.InsertNew(id); | |
411 report->AddString(StatsReport::kStatsValueNameTrackId, audio_track->id()); | |
412 } | |
413 } | |
414 | |
415 void StatsCollector::RemoveLocalAudioTrack(AudioTrackInterface* audio_track, | |
416 uint32_t ssrc) { | |
417 RTC_DCHECK(audio_track != NULL); | |
418 local_audio_tracks_.erase(std::remove_if(local_audio_tracks_.begin(), | |
419 local_audio_tracks_.end(), | |
420 [audio_track, ssrc](const LocalAudioTrackVector::value_type& track) { | |
421 return track.first == audio_track && track.second == ssrc; | |
422 })); | |
423 } | |
424 | |
425 void StatsCollector::GetStats(MediaStreamTrackInterface* track, | |
426 StatsReports* reports) { | |
427 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
428 RTC_DCHECK(reports != NULL); | |
429 RTC_DCHECK(reports->empty()); | |
430 | |
431 rtc::Thread::ScopedDisallowBlockingCalls no_blocking_calls; | |
432 | |
433 if (!track) { | |
434 reports->reserve(reports_.size()); | |
435 for (auto* r : reports_) | |
436 reports->push_back(r); | |
437 return; | |
438 } | |
439 | |
440 StatsReport* report = reports_.Find(StatsReport::NewTypedId( | |
441 StatsReport::kStatsReportTypeSession, pc_->session()->id())); | |
442 if (report) | |
443 reports->push_back(report); | |
444 | |
445 report = reports_.Find(StatsReport::NewTypedId( | |
446 StatsReport::kStatsReportTypeTrack, track->id())); | |
447 | |
448 if (!report) | |
449 return; | |
450 | |
451 reports->push_back(report); | |
452 | |
453 std::string track_id; | |
454 for (const auto* r : reports_) { | |
455 if (r->type() != StatsReport::kStatsReportTypeSsrc) | |
456 continue; | |
457 | |
458 const StatsReport::Value* v = | |
459 r->FindValue(StatsReport::kStatsValueNameTrackId); | |
460 if (v && v->string_val() == track->id()) | |
461 reports->push_back(r); | |
462 } | |
463 } | |
464 | |
465 void | |
466 StatsCollector::UpdateStats(PeerConnectionInterface::StatsOutputLevel level) { | |
467 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
468 double time_now = GetTimeNow(); | |
469 // Calls to UpdateStats() that occur less than kMinGatherStatsPeriod number of | |
470 // ms apart will be ignored. | |
471 const double kMinGatherStatsPeriod = 50; | |
472 if (stats_gathering_started_ != 0 && | |
473 stats_gathering_started_ + kMinGatherStatsPeriod > time_now) { | |
474 return; | |
475 } | |
476 stats_gathering_started_ = time_now; | |
477 | |
478 if (pc_->session()) { | |
479 // TODO(tommi): All of these hop over to the worker thread to fetch | |
480 // information. We could use an AsyncInvoker to run all of these and post | |
481 // the information back to the signaling thread where we can create and | |
482 // update stats reports. That would also clean up the threading story a bit | |
483 // since we'd be creating/updating the stats report objects consistently on | |
484 // the same thread (this class has no locks right now). | |
485 ExtractSessionInfo(); | |
486 ExtractVoiceInfo(); | |
487 ExtractVideoInfo(level); | |
488 ExtractDataInfo(); | |
489 UpdateTrackReports(); | |
490 } | |
491 } | |
492 | |
493 StatsReport* StatsCollector::PrepareReport( | |
494 bool local, | |
495 uint32_t ssrc, | |
496 const StatsReport::Id& transport_id, | |
497 StatsReport::Direction direction) { | |
498 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
499 StatsReport::Id id(StatsReport::NewIdWithDirection( | |
500 local ? StatsReport::kStatsReportTypeSsrc | |
501 : StatsReport::kStatsReportTypeRemoteSsrc, | |
502 rtc::ToString<uint32_t>(ssrc), direction)); | |
503 StatsReport* report = reports_.Find(id); | |
504 | |
505 // Use the ID of the track that is currently mapped to the SSRC, if any. | |
506 std::string track_id; | |
507 if (!GetTrackIdBySsrc(ssrc, &track_id, direction)) { | |
508 if (!report) { | |
509 // The ssrc is not used by any track or existing report, return NULL | |
510 // in such case to indicate no report is prepared for the ssrc. | |
511 return NULL; | |
512 } | |
513 | |
514 // The ssrc is not used by any existing track. Keeps the old track id | |
515 // since we want to report the stats for inactive ssrc. | |
516 const StatsReport::Value* v = | |
517 report->FindValue(StatsReport::kStatsValueNameTrackId); | |
518 if (v) | |
519 track_id = v->string_val(); | |
520 } | |
521 | |
522 if (!report) | |
523 report = reports_.InsertNew(id); | |
524 | |
525 // FYI - for remote reports, the timestamp will be overwritten later. | |
526 report->set_timestamp(stats_gathering_started_); | |
527 | |
528 report->AddInt64(StatsReport::kStatsValueNameSsrc, ssrc); | |
529 report->AddString(StatsReport::kStatsValueNameTrackId, track_id); | |
530 // Add the mapping of SSRC to transport. | |
531 report->AddId(StatsReport::kStatsValueNameTransportId, transport_id); | |
532 return report; | |
533 } | |
534 | |
535 StatsReport* StatsCollector::AddOneCertificateReport( | |
536 const rtc::SSLCertificate* cert, const StatsReport* issuer) { | |
537 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
538 | |
539 // TODO(bemasc): Move this computation to a helper class that caches these | |
540 // values to reduce CPU use in GetStats. This will require adding a fast | |
541 // SSLCertificate::Equals() method to detect certificate changes. | |
542 | |
543 std::string digest_algorithm; | |
544 if (!cert->GetSignatureDigestAlgorithm(&digest_algorithm)) | |
545 return nullptr; | |
546 | |
547 rtc::scoped_ptr<rtc::SSLFingerprint> ssl_fingerprint( | |
548 rtc::SSLFingerprint::Create(digest_algorithm, cert)); | |
549 | |
550 // SSLFingerprint::Create can fail if the algorithm returned by | |
551 // SSLCertificate::GetSignatureDigestAlgorithm is not supported by the | |
552 // implementation of SSLCertificate::ComputeDigest. This currently happens | |
553 // with MD5- and SHA-224-signed certificates when linked to libNSS. | |
554 if (!ssl_fingerprint) | |
555 return nullptr; | |
556 | |
557 std::string fingerprint = ssl_fingerprint->GetRfc4572Fingerprint(); | |
558 | |
559 rtc::Buffer der_buffer; | |
560 cert->ToDER(&der_buffer); | |
561 std::string der_base64; | |
562 rtc::Base64::EncodeFromArray(der_buffer.data(), der_buffer.size(), | |
563 &der_base64); | |
564 | |
565 StatsReport::Id id(StatsReport::NewTypedId( | |
566 StatsReport::kStatsReportTypeCertificate, fingerprint)); | |
567 StatsReport* report = reports_.ReplaceOrAddNew(id); | |
568 report->set_timestamp(stats_gathering_started_); | |
569 report->AddString(StatsReport::kStatsValueNameFingerprint, fingerprint); | |
570 report->AddString(StatsReport::kStatsValueNameFingerprintAlgorithm, | |
571 digest_algorithm); | |
572 report->AddString(StatsReport::kStatsValueNameDer, der_base64); | |
573 if (issuer) | |
574 report->AddId(StatsReport::kStatsValueNameIssuerId, issuer->id()); | |
575 return report; | |
576 } | |
577 | |
578 StatsReport* StatsCollector::AddCertificateReports( | |
579 const rtc::SSLCertificate* cert) { | |
580 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
581 // Produces a chain of StatsReports representing this certificate and the rest | |
582 // of its chain, and adds those reports to |reports_|. The return value is | |
583 // the id of the leaf report. The provided cert must be non-null, so at least | |
584 // one report will always be provided and the returned string will never be | |
585 // empty. | |
586 RTC_DCHECK(cert != NULL); | |
587 | |
588 StatsReport* issuer = nullptr; | |
589 rtc::scoped_ptr<rtc::SSLCertChain> chain; | |
590 if (cert->GetChain(chain.accept())) { | |
591 // This loop runs in reverse, i.e. from root to leaf, so that each | |
592 // certificate's issuer's report ID is known before the child certificate's | |
593 // report is generated. The root certificate does not have an issuer ID | |
594 // value. | |
595 for (ptrdiff_t i = chain->GetSize() - 1; i >= 0; --i) { | |
596 const rtc::SSLCertificate& cert_i = chain->Get(i); | |
597 issuer = AddOneCertificateReport(&cert_i, issuer); | |
598 } | |
599 } | |
600 // Add the leaf certificate. | |
601 return AddOneCertificateReport(cert, issuer); | |
602 } | |
603 | |
604 StatsReport* StatsCollector::AddConnectionInfoReport( | |
605 const std::string& content_name, int component, int connection_id, | |
606 const StatsReport::Id& channel_report_id, | |
607 const cricket::ConnectionInfo& info) { | |
608 StatsReport::Id id(StatsReport::NewCandidatePairId(content_name, component, | |
609 connection_id)); | |
610 StatsReport* report = reports_.ReplaceOrAddNew(id); | |
611 report->set_timestamp(stats_gathering_started_); | |
612 | |
613 const BoolForAdd bools[] = { | |
614 {StatsReport::kStatsValueNameActiveConnection, info.best_connection}, | |
615 {StatsReport::kStatsValueNameReceiving, info.receiving}, | |
616 {StatsReport::kStatsValueNameWritable, info.writable}, | |
617 }; | |
618 for (const auto& b : bools) | |
619 report->AddBoolean(b.name, b.value); | |
620 | |
621 report->AddId(StatsReport::kStatsValueNameChannelId, channel_report_id); | |
622 report->AddId(StatsReport::kStatsValueNameLocalCandidateId, | |
623 AddCandidateReport(info.local_candidate, true)->id()); | |
624 report->AddId(StatsReport::kStatsValueNameRemoteCandidateId, | |
625 AddCandidateReport(info.remote_candidate, false)->id()); | |
626 | |
627 const Int64ForAdd int64s[] = { | |
628 { StatsReport::kStatsValueNameBytesReceived, info.recv_total_bytes }, | |
629 { StatsReport::kStatsValueNameBytesSent, info.sent_total_bytes }, | |
630 { StatsReport::kStatsValueNamePacketsSent, info.sent_total_packets }, | |
631 { StatsReport::kStatsValueNameRtt, info.rtt }, | |
632 { StatsReport::kStatsValueNameSendPacketsDiscarded, | |
633 info.sent_discarded_packets }, | |
634 }; | |
635 for (const auto& i : int64s) | |
636 report->AddInt64(i.name, i.value); | |
637 | |
638 report->AddString(StatsReport::kStatsValueNameLocalAddress, | |
639 info.local_candidate.address().ToString()); | |
640 report->AddString(StatsReport::kStatsValueNameLocalCandidateType, | |
641 info.local_candidate.type()); | |
642 report->AddString(StatsReport::kStatsValueNameRemoteAddress, | |
643 info.remote_candidate.address().ToString()); | |
644 report->AddString(StatsReport::kStatsValueNameRemoteCandidateType, | |
645 info.remote_candidate.type()); | |
646 report->AddString(StatsReport::kStatsValueNameTransportType, | |
647 info.local_candidate.protocol()); | |
648 | |
649 return report; | |
650 } | |
651 | |
652 StatsReport* StatsCollector::AddCandidateReport( | |
653 const cricket::Candidate& candidate, | |
654 bool local) { | |
655 StatsReport::Id id(StatsReport::NewCandidateId(local, candidate.id())); | |
656 StatsReport* report = reports_.Find(id); | |
657 if (!report) { | |
658 report = reports_.InsertNew(id); | |
659 report->set_timestamp(stats_gathering_started_); | |
660 if (local) { | |
661 report->AddString(StatsReport::kStatsValueNameCandidateNetworkType, | |
662 AdapterTypeToStatsType(candidate.network_type())); | |
663 } | |
664 report->AddString(StatsReport::kStatsValueNameCandidateIPAddress, | |
665 candidate.address().ipaddr().ToString()); | |
666 report->AddString(StatsReport::kStatsValueNameCandidatePortNumber, | |
667 candidate.address().PortAsString()); | |
668 report->AddInt(StatsReport::kStatsValueNameCandidatePriority, | |
669 candidate.priority()); | |
670 report->AddString(StatsReport::kStatsValueNameCandidateType, | |
671 IceCandidateTypeToStatsType(candidate.type())); | |
672 report->AddString(StatsReport::kStatsValueNameCandidateTransportType, | |
673 candidate.protocol()); | |
674 } | |
675 | |
676 return report; | |
677 } | |
678 | |
679 void StatsCollector::ExtractSessionInfo() { | |
680 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
681 | |
682 // Extract information from the base session. | |
683 StatsReport::Id id(StatsReport::NewTypedId( | |
684 StatsReport::kStatsReportTypeSession, pc_->session()->id())); | |
685 StatsReport* report = reports_.ReplaceOrAddNew(id); | |
686 report->set_timestamp(stats_gathering_started_); | |
687 report->AddBoolean(StatsReport::kStatsValueNameInitiator, | |
688 pc_->session()->initial_offerer()); | |
689 | |
690 SessionStats stats; | |
691 if (!pc_->session()->GetTransportStats(&stats)) { | |
692 return; | |
693 } | |
694 | |
695 // Store the proxy map away for use in SSRC reporting. | |
696 // TODO(tommi): This shouldn't be necessary if we post the stats back to the | |
697 // signaling thread after fetching them on the worker thread, then just use | |
698 // the proxy map directly from the session stats. | |
699 // As is, if GetStats() failed, we could be using old (incorrect?) proxy | |
700 // data. | |
701 proxy_to_transport_ = stats.proxy_to_transport; | |
702 | |
703 for (const auto& transport_iter : stats.transport_stats) { | |
704 // Attempt to get a copy of the certificates from the transport and | |
705 // expose them in stats reports. All channels in a transport share the | |
706 // same local and remote certificates. | |
707 // | |
708 StatsReport::Id local_cert_report_id, remote_cert_report_id; | |
709 rtc::scoped_refptr<rtc::RTCCertificate> certificate; | |
710 if (pc_->session()->GetLocalCertificate( | |
711 transport_iter.second.transport_name, &certificate)) { | |
712 StatsReport* r = AddCertificateReports(&(certificate->ssl_certificate())); | |
713 if (r) | |
714 local_cert_report_id = r->id(); | |
715 } | |
716 | |
717 rtc::scoped_ptr<rtc::SSLCertificate> cert; | |
718 if (pc_->session()->GetRemoteSSLCertificate( | |
719 transport_iter.second.transport_name, cert.accept())) { | |
720 StatsReport* r = AddCertificateReports(cert.get()); | |
721 if (r) | |
722 remote_cert_report_id = r->id(); | |
723 } | |
724 | |
725 for (const auto& channel_iter : transport_iter.second.channel_stats) { | |
726 StatsReport::Id id(StatsReport::NewComponentId( | |
727 transport_iter.second.transport_name, channel_iter.component)); | |
728 StatsReport* channel_report = reports_.ReplaceOrAddNew(id); | |
729 channel_report->set_timestamp(stats_gathering_started_); | |
730 channel_report->AddInt(StatsReport::kStatsValueNameComponent, | |
731 channel_iter.component); | |
732 if (local_cert_report_id.get()) { | |
733 channel_report->AddId(StatsReport::kStatsValueNameLocalCertificateId, | |
734 local_cert_report_id); | |
735 } | |
736 if (remote_cert_report_id.get()) { | |
737 channel_report->AddId(StatsReport::kStatsValueNameRemoteCertificateId, | |
738 remote_cert_report_id); | |
739 } | |
740 int srtp_crypto_suite = channel_iter.srtp_crypto_suite; | |
741 if (srtp_crypto_suite != rtc::SRTP_INVALID_CRYPTO_SUITE && | |
742 rtc::SrtpCryptoSuiteToName(srtp_crypto_suite).length()) { | |
743 channel_report->AddString( | |
744 StatsReport::kStatsValueNameSrtpCipher, | |
745 rtc::SrtpCryptoSuiteToName(srtp_crypto_suite)); | |
746 } | |
747 int ssl_cipher_suite = channel_iter.ssl_cipher_suite; | |
748 if (ssl_cipher_suite != rtc::TLS_NULL_WITH_NULL_NULL && | |
749 rtc::SSLStreamAdapter::SslCipherSuiteToName(ssl_cipher_suite) | |
750 .length()) { | |
751 channel_report->AddString( | |
752 StatsReport::kStatsValueNameDtlsCipher, | |
753 rtc::SSLStreamAdapter::SslCipherSuiteToName(ssl_cipher_suite)); | |
754 } | |
755 | |
756 int connection_id = 0; | |
757 for (const cricket::ConnectionInfo& info : | |
758 channel_iter.connection_infos) { | |
759 StatsReport* connection_report = AddConnectionInfoReport( | |
760 transport_iter.first, channel_iter.component, connection_id++, | |
761 channel_report->id(), info); | |
762 if (info.best_connection) { | |
763 channel_report->AddId( | |
764 StatsReport::kStatsValueNameSelectedCandidatePairId, | |
765 connection_report->id()); | |
766 } | |
767 } | |
768 } | |
769 } | |
770 } | |
771 | |
772 void StatsCollector::ExtractVoiceInfo() { | |
773 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
774 | |
775 if (!pc_->session()->voice_channel()) { | |
776 return; | |
777 } | |
778 cricket::VoiceMediaInfo voice_info; | |
779 if (!pc_->session()->voice_channel()->GetStats(&voice_info)) { | |
780 LOG(LS_ERROR) << "Failed to get voice channel stats."; | |
781 return; | |
782 } | |
783 | |
784 // TODO(tommi): The above code should run on the worker thread and post the | |
785 // results back to the signaling thread, where we can add data to the reports. | |
786 rtc::Thread::ScopedDisallowBlockingCalls no_blocking_calls; | |
787 | |
788 StatsReport::Id transport_id(GetTransportIdFromProxy( | |
789 proxy_to_transport_, pc_->session()->voice_channel()->content_name())); | |
790 if (!transport_id.get()) { | |
791 LOG(LS_ERROR) << "Failed to get transport name for proxy " | |
792 << pc_->session()->voice_channel()->content_name(); | |
793 return; | |
794 } | |
795 | |
796 ExtractStatsFromList(voice_info.receivers, transport_id, this, | |
797 StatsReport::kReceive); | |
798 ExtractStatsFromList(voice_info.senders, transport_id, this, | |
799 StatsReport::kSend); | |
800 | |
801 UpdateStatsFromExistingLocalAudioTracks(); | |
802 } | |
803 | |
804 void StatsCollector::ExtractVideoInfo( | |
805 PeerConnectionInterface::StatsOutputLevel level) { | |
806 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
807 | |
808 if (!pc_->session()->video_channel()) | |
809 return; | |
810 | |
811 cricket::VideoMediaInfo video_info; | |
812 if (!pc_->session()->video_channel()->GetStats(&video_info)) { | |
813 LOG(LS_ERROR) << "Failed to get video channel stats."; | |
814 return; | |
815 } | |
816 | |
817 // TODO(tommi): The above code should run on the worker thread and post the | |
818 // results back to the signaling thread, where we can add data to the reports. | |
819 rtc::Thread::ScopedDisallowBlockingCalls no_blocking_calls; | |
820 | |
821 StatsReport::Id transport_id(GetTransportIdFromProxy( | |
822 proxy_to_transport_, pc_->session()->video_channel()->content_name())); | |
823 if (!transport_id.get()) { | |
824 LOG(LS_ERROR) << "Failed to get transport name for proxy " | |
825 << pc_->session()->video_channel()->content_name(); | |
826 return; | |
827 } | |
828 ExtractStatsFromList(video_info.receivers, transport_id, this, | |
829 StatsReport::kReceive); | |
830 ExtractStatsFromList(video_info.senders, transport_id, this, | |
831 StatsReport::kSend); | |
832 if (video_info.bw_estimations.size() != 1) { | |
833 LOG(LS_ERROR) << "BWEs count: " << video_info.bw_estimations.size(); | |
834 } else { | |
835 StatsReport::Id report_id(StatsReport::NewBandwidthEstimationId()); | |
836 StatsReport* report = reports_.FindOrAddNew(report_id); | |
837 ExtractStats( | |
838 video_info.bw_estimations[0], stats_gathering_started_, level, report); | |
839 } | |
840 } | |
841 | |
842 void StatsCollector::ExtractDataInfo() { | |
843 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
844 | |
845 rtc::Thread::ScopedDisallowBlockingCalls no_blocking_calls; | |
846 | |
847 for (const auto& dc : pc_->sctp_data_channels()) { | |
848 StatsReport::Id id(StatsReport::NewTypedIntId( | |
849 StatsReport::kStatsReportTypeDataChannel, dc->id())); | |
850 StatsReport* report = reports_.ReplaceOrAddNew(id); | |
851 report->set_timestamp(stats_gathering_started_); | |
852 report->AddString(StatsReport::kStatsValueNameLabel, dc->label()); | |
853 report->AddInt(StatsReport::kStatsValueNameDataChannelId, dc->id()); | |
854 report->AddString(StatsReport::kStatsValueNameProtocol, dc->protocol()); | |
855 report->AddString(StatsReport::kStatsValueNameState, | |
856 DataChannelInterface::DataStateString(dc->state())); | |
857 } | |
858 } | |
859 | |
860 StatsReport* StatsCollector::GetReport(const StatsReport::StatsType& type, | |
861 const std::string& id, | |
862 StatsReport::Direction direction) { | |
863 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
864 RTC_DCHECK(type == StatsReport::kStatsReportTypeSsrc || | |
865 type == StatsReport::kStatsReportTypeRemoteSsrc); | |
866 return reports_.Find(StatsReport::NewIdWithDirection(type, id, direction)); | |
867 } | |
868 | |
869 void StatsCollector::UpdateStatsFromExistingLocalAudioTracks() { | |
870 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
871 // Loop through the existing local audio tracks. | |
872 for (const auto& it : local_audio_tracks_) { | |
873 AudioTrackInterface* track = it.first; | |
874 uint32_t ssrc = it.second; | |
875 StatsReport* report = | |
876 GetReport(StatsReport::kStatsReportTypeSsrc, | |
877 rtc::ToString<uint32_t>(ssrc), StatsReport::kSend); | |
878 if (report == NULL) { | |
879 // This can happen if a local audio track is added to a stream on the | |
880 // fly and the report has not been set up yet. Do nothing in this case. | |
881 LOG(LS_ERROR) << "Stats report does not exist for ssrc " << ssrc; | |
882 continue; | |
883 } | |
884 | |
885 // The same ssrc can be used by both local and remote audio tracks. | |
886 const StatsReport::Value* v = | |
887 report->FindValue(StatsReport::kStatsValueNameTrackId); | |
888 if (!v || v->string_val() != track->id()) | |
889 continue; | |
890 | |
891 report->set_timestamp(stats_gathering_started_); | |
892 UpdateReportFromAudioTrack(track, report); | |
893 } | |
894 } | |
895 | |
896 void StatsCollector::UpdateReportFromAudioTrack(AudioTrackInterface* track, | |
897 StatsReport* report) { | |
898 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
899 RTC_DCHECK(track != NULL); | |
900 | |
901 // Don't overwrite report values if they're not available. | |
902 int signal_level; | |
903 if (track->GetSignalLevel(&signal_level)) { | |
904 RTC_DCHECK_GE(signal_level, 0); | |
905 report->AddInt(StatsReport::kStatsValueNameAudioInputLevel, signal_level); | |
906 } | |
907 | |
908 auto audio_processor(track->GetAudioProcessor()); | |
909 | |
910 if (audio_processor.get()) { | |
911 AudioProcessorInterface::AudioProcessorStats stats; | |
912 audio_processor->GetStats(&stats); | |
913 | |
914 SetAudioProcessingStats( | |
915 report, stats.typing_noise_detected, stats.echo_return_loss, | |
916 stats.echo_return_loss_enhancement, stats.echo_delay_median_ms, | |
917 stats.aec_quality_min, stats.echo_delay_std_ms); | |
918 } | |
919 } | |
920 | |
921 bool StatsCollector::GetTrackIdBySsrc(uint32_t ssrc, | |
922 std::string* track_id, | |
923 StatsReport::Direction direction) { | |
924 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
925 if (direction == StatsReport::kSend) { | |
926 if (!pc_->session()->GetLocalTrackIdBySsrc(ssrc, track_id)) { | |
927 LOG(LS_WARNING) << "The SSRC " << ssrc | |
928 << " is not associated with a sending track"; | |
929 return false; | |
930 } | |
931 } else { | |
932 RTC_DCHECK(direction == StatsReport::kReceive); | |
933 if (!pc_->session()->GetRemoteTrackIdBySsrc(ssrc, track_id)) { | |
934 LOG(LS_WARNING) << "The SSRC " << ssrc | |
935 << " is not associated with a receiving track"; | |
936 return false; | |
937 } | |
938 } | |
939 | |
940 return true; | |
941 } | |
942 | |
943 void StatsCollector::UpdateTrackReports() { | |
944 RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent()); | |
945 | |
946 rtc::Thread::ScopedDisallowBlockingCalls no_blocking_calls; | |
947 | |
948 for (const auto& entry : track_ids_) { | |
949 StatsReport* report = entry.second; | |
950 report->set_timestamp(stats_gathering_started_); | |
951 } | |
952 } | |
953 | |
954 void StatsCollector::ClearUpdateStatsCacheForTest() { | |
955 stats_gathering_started_ = 0; | |
956 } | |
957 | |
958 } // namespace webrtc | |
OLD | NEW |