blob: e909b2f88e78b6e3488fc5143ae6dafb26104ab0 [file] [log] [blame]
pbos@webrtc.orga0d78272014-09-12 11:51:47 +00001/*
2 * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 *
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
6 * tree. An additional intellectual property rights grant can be found
7 * in the file PATENTS. All contributing project authors may
8 * be found in the AUTHORS file in the root of the source tree.
9 */
Niels Möller718a7632016-06-13 13:06:01 +020010
Mirko Bonadei92ea95e2017-09-15 06:47:31 +020011#include "modules/video_coding/utility/quality_scaler.h"
pbos@webrtc.orga0d78272014-09-12 11:51:47 +000012
kthelgason876222f2016-11-29 01:44:11 -080013#include <memory>
Sebastian Janssonecb68972019-01-18 10:30:54 +010014#include <utility>
Kári Tristan Helgason5a20ed32016-09-15 10:56:19 +020015
Evan Shrubsolece0a11d2020-04-16 11:36:55 +020016#include "api/video/video_adaptation_reason.h"
Mirko Bonadei92ea95e2017-09-15 06:47:31 +020017#include "rtc_base/checks.h"
Åsa Persson517678c2019-05-06 14:17:35 +020018#include "rtc_base/experiments/quality_scaler_settings.h"
Mirko Bonadei92ea95e2017-09-15 06:47:31 +020019#include "rtc_base/logging.h"
Åsa Perssona945aee2018-04-24 16:53:25 +020020#include "rtc_base/numerics/exp_filter.h"
Mirko Bonadei92ea95e2017-09-15 06:47:31 +020021#include "rtc_base/task_queue.h"
Henrik Boström012aa372020-04-27 17:40:55 +020022#include "rtc_base/task_utils/to_queued_task.h"
23#include "rtc_base/weak_ptr.h"
kthelgason478681e2016-09-28 08:17:43 -070024
Kári Tristan Helgason5a20ed32016-09-15 10:56:19 +020025// TODO(kthelgason): Some versions of Android have issues with log2.
26// See https://code.google.com/p/android/issues/detail?id=212634 for details
27#if defined(WEBRTC_ANDROID)
28#define log2(x) (log(x) / log(2))
29#endif
kthelgason194f40a2016-09-14 02:14:58 -070030
pbos@webrtc.orga0d78272014-09-12 11:51:47 +000031namespace webrtc {
32
Peter Boström926dfcd2016-04-14 14:48:10 +020033namespace {
Niels Möller225c7872018-02-22 15:03:53 +010034// TODO(nisse): Delete, delegate to encoders.
pboscbac40d2016-04-13 02:51:02 -070035// Threshold constant used until first downscale (to permit fast rampup).
kthelgason876222f2016-11-29 01:44:11 -080036static const int kMeasureMs = 2000;
37static const float kSamplePeriodScaleFactor = 2.5;
pbos@webrtc.orga0d78272014-09-12 11:51:47 +000038static const int kFramedropPercentThreshold = 60;
Åsa Persson517678c2019-05-06 14:17:35 +020039static const size_t kMinFramesNeededToScale = 2 * 30;
pbos@webrtc.orga0d78272014-09-12 11:51:47 +000040
kthelgason876222f2016-11-29 01:44:11 -080041} // namespace
kthelgason478681e2016-09-28 08:17:43 -070042
Åsa Perssona945aee2018-04-24 16:53:25 +020043class QualityScaler::QpSmoother {
44 public:
45 explicit QpSmoother(float alpha)
Sebastian Janssonb6789402019-03-01 15:40:49 +010046 : alpha_(alpha),
47 // The initial value of last_sample_ms doesn't matter since the smoother
48 // will ignore the time delta for the first update.
49 last_sample_ms_(0),
50 smoother_(alpha) {}
Åsa Perssona945aee2018-04-24 16:53:25 +020051
Danil Chapovalov0040b662018-06-18 10:48:16 +020052 absl::optional<int> GetAvg() const {
Åsa Perssona945aee2018-04-24 16:53:25 +020053 float value = smoother_.filtered();
54 if (value == rtc::ExpFilter::kValueUndefined) {
Danil Chapovalov0040b662018-06-18 10:48:16 +020055 return absl::nullopt;
Åsa Perssona945aee2018-04-24 16:53:25 +020056 }
57 return static_cast<int>(value);
58 }
59
Sebastian Janssonb6789402019-03-01 15:40:49 +010060 void Add(float sample, int64_t time_sent_us) {
61 int64_t now_ms = time_sent_us / 1000;
Åsa Perssona945aee2018-04-24 16:53:25 +020062 smoother_.Apply(static_cast<float>(now_ms - last_sample_ms_), sample);
63 last_sample_ms_ = now_ms;
64 }
65
66 void Reset() { smoother_.Reset(alpha_); }
67
68 private:
69 const float alpha_;
70 int64_t last_sample_ms_;
71 rtc::ExpFilter smoother_;
72};
73
Henrik Boström012aa372020-04-27 17:40:55 +020074// The QualityScaler checks for QP periodically by queuing CheckQpTasks. The
75// task will either run to completion and trigger a new task being queued, or it
76// will be destroyed because the QualityScaler is destroyed.
77//
78// When high or low QP is reported, the task will be pending until a callback is
79// invoked. This lets the QualityScalerQpUsageHandlerInterface react to QP usage
80// asynchronously and prevents checking for QP until the stream has potentially
81// been reconfigured.
82class QualityScaler::CheckQpTask {
83 public:
84 // The result of one CheckQpTask may influence the delay of the next
85 // CheckQpTask.
86 struct Result {
87 bool observed_enough_frames = false;
88 bool qp_usage_reported = false;
89 bool clear_qp_samples = false;
90 };
91
92 CheckQpTask(QualityScaler* quality_scaler, Result previous_task_result)
93 : quality_scaler_(quality_scaler),
94 state_(State::kNotStarted),
95 previous_task_result_(previous_task_result),
96 weak_ptr_factory_(this) {}
97
98 void StartDelayedTask() {
99 RTC_DCHECK_EQ(state_, State::kNotStarted);
100 state_ = State::kCheckingQp;
101 TaskQueueBase::Current()->PostDelayedTask(
102 ToQueuedTask([this_weak_ptr = weak_ptr_factory_.GetWeakPtr(), this] {
103 if (!this_weak_ptr) {
104 // The task has been cancelled through destruction.
105 return;
106 }
107 RTC_DCHECK_EQ(state_, State::kCheckingQp);
108 RTC_DCHECK_RUN_ON(&quality_scaler_->task_checker_);
109 switch (quality_scaler_->CheckQp()) {
110 case QualityScaler::CheckQpResult::kInsufficientSamples: {
111 result_.observed_enough_frames = false;
112 // After this line, |this| may be deleted.
113 DoCompleteTask();
114 return;
115 }
116 case QualityScaler::CheckQpResult::kNormalQp: {
117 result_.observed_enough_frames = true;
118 // After this line, |this| may be deleted.
119 DoCompleteTask();
120 return;
121 }
122 case QualityScaler::CheckQpResult::kHighQp: {
123 result_.observed_enough_frames = true;
124 result_.qp_usage_reported = true;
125 state_ = State::kAwaitingQpUsageHandled;
126 rtc::scoped_refptr<QualityScalerQpUsageHandlerCallbackInterface>
127 callback = ConstructCallback();
128 quality_scaler_->fast_rampup_ = false;
129 // After this line, |this| may be deleted.
130 quality_scaler_->handler_->OnReportQpUsageHigh(callback);
131 return;
132 }
133 case QualityScaler::CheckQpResult::kLowQp: {
134 result_.observed_enough_frames = true;
135 result_.qp_usage_reported = true;
136 state_ = State::kAwaitingQpUsageHandled;
137 rtc::scoped_refptr<QualityScalerQpUsageHandlerCallbackInterface>
138 callback = ConstructCallback();
139 // After this line, |this| may be deleted.
140 quality_scaler_->handler_->OnReportQpUsageLow(callback);
141 return;
142 }
143 }
144 }),
145 GetCheckingQpDelayMs());
146 }
147
148 void OnQpUsageHandled(bool clear_qp_samples) {
149 RTC_DCHECK_EQ(state_, State::kAwaitingQpUsageHandled);
150 result_.clear_qp_samples = clear_qp_samples;
151 if (clear_qp_samples)
152 quality_scaler_->ClearSamples();
153 DoCompleteTask();
154 }
155
156 bool HasCompletedTask() const { return state_ == State::kCompleted; }
157
158 Result result() const {
159 RTC_DCHECK(HasCompletedTask());
160 return result_;
161 }
162
163 private:
164 enum class State {
165 kNotStarted,
166 kCheckingQp,
167 kAwaitingQpUsageHandled,
168 kCompleted,
169 };
170
171 // Defined after the definition of QualityScaler::CheckQpTaskHandlerCallback.
172 // Gets around a forward declaration issue.
173 rtc::scoped_refptr<QualityScaler::CheckQpTaskHandlerCallback>
174 ConstructCallback();
175
176 // Determines the sampling period of CheckQpTasks.
177 int64_t GetCheckingQpDelayMs() const {
178 RTC_DCHECK_RUN_ON(&quality_scaler_->task_checker_);
179 if (quality_scaler_->fast_rampup_) {
180 return quality_scaler_->sampling_period_ms_;
181 }
182 if (quality_scaler_->experiment_enabled_ &&
183 !previous_task_result_.observed_enough_frames) {
184 // Use half the interval while waiting for enough frames.
185 return quality_scaler_->sampling_period_ms_ / 2;
186 }
187 if (!previous_task_result_.clear_qp_samples) {
188 // Check shortly again.
189 return quality_scaler_->sampling_period_ms_ / 8;
190 }
191 if (quality_scaler_->scale_factor_ &&
192 !previous_task_result_.qp_usage_reported) {
193 // Last CheckQp did not call AdaptDown/Up, possibly reduce interval.
194 return quality_scaler_->sampling_period_ms_ *
195 quality_scaler_->scale_factor_.value();
196 }
197 return quality_scaler_->sampling_period_ms_ *
198 quality_scaler_->initial_scale_factor_;
199 }
200
201 void DoCompleteTask() {
202 RTC_DCHECK(state_ == State::kCheckingQp ||
203 state_ == State::kAwaitingQpUsageHandled);
204 state_ = State::kCompleted;
205 // Starting the next task deletes the pending task. After this line, |this|
206 // has been deleted.
207 quality_scaler_->StartNextCheckQpTask();
208 }
209
210 QualityScaler* const quality_scaler_;
211 State state_;
212 const Result previous_task_result_;
213 Result result_;
214
215 rtc::WeakPtrFactory<CheckQpTask> weak_ptr_factory_;
216};
217
218class QualityScaler::CheckQpTaskHandlerCallback
219 : public QualityScalerQpUsageHandlerCallbackInterface {
220 public:
221 CheckQpTaskHandlerCallback(
222 rtc::WeakPtr<QualityScaler::CheckQpTask> check_qp_task)
223 : QualityScalerQpUsageHandlerCallbackInterface(),
224 check_qp_task_(std::move(check_qp_task)),
225 was_handled_(false) {}
226
227 ~CheckQpTaskHandlerCallback() { RTC_DCHECK(was_handled_); }
228
229 void OnQpUsageHandled(bool clear_qp_samples) {
230 RTC_DCHECK(!was_handled_);
231 was_handled_ = true;
232 if (!check_qp_task_) {
233 // The task has been cancelled through destruction; the result of the
234 // operation is ignored.
235 return;
236 }
237 check_qp_task_->OnQpUsageHandled(clear_qp_samples);
238 }
239
240 private:
241 // The callback may outlive the QualityScaler and its task.
242 rtc::WeakPtr<QualityScaler::CheckQpTask> const check_qp_task_;
243 bool was_handled_;
244};
245
246rtc::scoped_refptr<QualityScaler::CheckQpTaskHandlerCallback>
247QualityScaler::CheckQpTask::ConstructCallback() {
248 return new CheckQpTaskHandlerCallback(weak_ptr_factory_.GetWeakPtr());
249}
250
251QualityScaler::QualityScaler(QualityScalerQpUsageHandlerInterface* handler,
kthelgason876222f2016-11-29 01:44:11 -0800252 VideoEncoder::QpThresholds thresholds)
Henrik Boström012aa372020-04-27 17:40:55 +0200253 : QualityScaler(handler, thresholds, kMeasureMs) {}
kthelgason876222f2016-11-29 01:44:11 -0800254
255// Protected ctor, should not be called directly.
Henrik Boström012aa372020-04-27 17:40:55 +0200256QualityScaler::QualityScaler(QualityScalerQpUsageHandlerInterface* handler,
kthelgason876222f2016-11-29 01:44:11 -0800257 VideoEncoder::QpThresholds thresholds,
Åsa Persson0ad2d8a2018-04-19 11:06:11 +0200258 int64_t sampling_period_ms)
Henrik Boström012aa372020-04-27 17:40:55 +0200259 : handler_(handler),
Åsa Persson0ad2d8a2018-04-19 11:06:11 +0200260 thresholds_(thresholds),
261 sampling_period_ms_(sampling_period_ms),
kthelgason876222f2016-11-29 01:44:11 -0800262 fast_rampup_(true),
263 // Arbitrarily choose size based on 30 fps for 5 seconds.
264 average_qp_(5 * 30),
Åsa Perssona945aee2018-04-24 16:53:25 +0200265 framedrop_percent_media_opt_(5 * 30),
266 framedrop_percent_all_(5 * 30),
267 experiment_enabled_(QualityScalingExperiment::Enabled()),
Åsa Persson517678c2019-05-06 14:17:35 +0200268 min_frames_needed_(
269 QualityScalerSettings::ParseFromFieldTrials().MinFrames().value_or(
270 kMinFramesNeededToScale)),
271 initial_scale_factor_(QualityScalerSettings::ParseFromFieldTrials()
272 .InitialScaleFactor()
273 .value_or(kSamplePeriodScaleFactor)),
274 scale_factor_(
Henrik Boström012aa372020-04-27 17:40:55 +0200275 QualityScalerSettings::ParseFromFieldTrials().ScaleFactor()) {
Sebastian Janssonb55015e2019-04-09 13:44:04 +0200276 RTC_DCHECK_RUN_ON(&task_checker_);
Åsa Perssona945aee2018-04-24 16:53:25 +0200277 if (experiment_enabled_) {
278 config_ = QualityScalingExperiment::GetConfig();
279 qp_smoother_high_.reset(new QpSmoother(config_.alpha_high));
280 qp_smoother_low_.reset(new QpSmoother(config_.alpha_low));
281 }
Henrik Boström012aa372020-04-27 17:40:55 +0200282 RTC_DCHECK(handler_ != nullptr);
283 StartNextCheckQpTask();
Mirko Bonadei675513b2017-11-09 11:09:25 +0100284 RTC_LOG(LS_INFO) << "QP thresholds: low: " << thresholds_.low
285 << ", high: " << thresholds_.high;
pbos@webrtc.orga0d78272014-09-12 11:51:47 +0000286}
287
kthelgason876222f2016-11-29 01:44:11 -0800288QualityScaler::~QualityScaler() {
Sebastian Janssonb55015e2019-04-09 13:44:04 +0200289 RTC_DCHECK_RUN_ON(&task_checker_);
kthelgason876222f2016-11-29 01:44:11 -0800290}
291
Henrik Boström012aa372020-04-27 17:40:55 +0200292void QualityScaler::StartNextCheckQpTask() {
Sebastian Janssonb55015e2019-04-09 13:44:04 +0200293 RTC_DCHECK_RUN_ON(&task_checker_);
Henrik Boström012aa372020-04-27 17:40:55 +0200294 RTC_DCHECK(!pending_qp_task_ || pending_qp_task_->HasCompletedTask())
295 << "A previous CheckQpTask has not completed yet!";
296 CheckQpTask::Result previous_task_result;
297 if (pending_qp_task_) {
298 previous_task_result = pending_qp_task_->result();
Åsa Perssona945aee2018-04-24 16:53:25 +0200299 }
Henrik Boström012aa372020-04-27 17:40:55 +0200300 pending_qp_task_ = std::make_unique<CheckQpTask>(this, previous_task_result);
301 pending_qp_task_->StartDelayedTask();
kthelgason876222f2016-11-29 01:44:11 -0800302}
303
Åsa Persson12314192019-06-20 15:45:07 +0200304void QualityScaler::SetQpThresholds(VideoEncoder::QpThresholds thresholds) {
305 RTC_DCHECK_RUN_ON(&task_checker_);
306 thresholds_ = thresholds;
307}
308
Åsa Perssona945aee2018-04-24 16:53:25 +0200309void QualityScaler::ReportDroppedFrameByMediaOpt() {
Sebastian Janssonb55015e2019-04-09 13:44:04 +0200310 RTC_DCHECK_RUN_ON(&task_checker_);
Åsa Perssona945aee2018-04-24 16:53:25 +0200311 framedrop_percent_media_opt_.AddSample(100);
312 framedrop_percent_all_.AddSample(100);
313}
314
315void QualityScaler::ReportDroppedFrameByEncoder() {
Sebastian Janssonb55015e2019-04-09 13:44:04 +0200316 RTC_DCHECK_RUN_ON(&task_checker_);
Åsa Perssona945aee2018-04-24 16:53:25 +0200317 framedrop_percent_all_.AddSample(100);
pbos@webrtc.orga0d78272014-09-12 11:51:47 +0000318}
319
Sebastian Janssonb6789402019-03-01 15:40:49 +0100320void QualityScaler::ReportQp(int qp, int64_t time_sent_us) {
Sebastian Janssonb55015e2019-04-09 13:44:04 +0200321 RTC_DCHECK_RUN_ON(&task_checker_);
Åsa Perssona945aee2018-04-24 16:53:25 +0200322 framedrop_percent_media_opt_.AddSample(0);
323 framedrop_percent_all_.AddSample(0);
kthelgason194f40a2016-09-14 02:14:58 -0700324 average_qp_.AddSample(qp);
Åsa Perssona945aee2018-04-24 16:53:25 +0200325 if (qp_smoother_high_)
Sebastian Janssonb6789402019-03-01 15:40:49 +0100326 qp_smoother_high_->Add(qp, time_sent_us);
Åsa Perssona945aee2018-04-24 16:53:25 +0200327 if (qp_smoother_low_)
Sebastian Janssonb6789402019-03-01 15:40:49 +0100328 qp_smoother_low_->Add(qp, time_sent_us);
pbos@webrtc.orga0d78272014-09-12 11:51:47 +0000329}
330
Åsa Perssone644a032019-11-08 15:56:00 +0100331bool QualityScaler::QpFastFilterLow() const {
332 RTC_DCHECK_RUN_ON(&task_checker_);
333 size_t num_frames = config_.use_all_drop_reasons
334 ? framedrop_percent_all_.Size()
335 : framedrop_percent_media_opt_.Size();
336 const size_t kMinNumFrames = 10;
337 if (num_frames < kMinNumFrames) {
338 return false; // Wait for more frames before making a decision.
339 }
340 absl::optional<int> avg_qp_high = qp_smoother_high_
341 ? qp_smoother_high_->GetAvg()
342 : average_qp_.GetAverageRoundedDown();
343 return (avg_qp_high) ? (avg_qp_high.value() <= thresholds_.low) : false;
344}
345
Henrik Boström012aa372020-04-27 17:40:55 +0200346QualityScaler::CheckQpResult QualityScaler::CheckQp() const {
Sebastian Janssonb55015e2019-04-09 13:44:04 +0200347 RTC_DCHECK_RUN_ON(&task_checker_);
jackychen61b4d512015-04-21 15:30:11 -0700348 // Should be set through InitEncode -> Should be set by now.
kthelgason876222f2016-11-29 01:44:11 -0800349 RTC_DCHECK_GE(thresholds_.low, 0);
kthelgason55a01352017-04-04 02:31:42 -0700350
Åsa Persson0ad2d8a2018-04-19 11:06:11 +0200351 // If we have not observed at least this many frames we can't make a good
352 // scaling decision.
Åsa Perssona945aee2018-04-24 16:53:25 +0200353 const size_t frames = config_.use_all_drop_reasons
Ilya Nikolaevskiy26341992018-11-05 12:55:18 +0100354 ? framedrop_percent_all_.Size()
355 : framedrop_percent_media_opt_.Size();
Åsa Persson517678c2019-05-06 14:17:35 +0200356 if (frames < min_frames_needed_) {
Henrik Boström012aa372020-04-27 17:40:55 +0200357 return CheckQpResult::kInsufficientSamples;
Åsa Perssona945aee2018-04-24 16:53:25 +0200358 }
kthelgason55a01352017-04-04 02:31:42 -0700359
kthelgason194f40a2016-09-14 02:14:58 -0700360 // Check if we should scale down due to high frame drop.
Danil Chapovalov0040b662018-06-18 10:48:16 +0200361 const absl::optional<int> drop_rate =
Ilya Nikolaevskiy26341992018-11-05 12:55:18 +0100362 config_.use_all_drop_reasons
363 ? framedrop_percent_all_.GetAverageRoundedDown()
364 : framedrop_percent_media_opt_.GetAverageRoundedDown();
kthelgason194f40a2016-09-14 02:14:58 -0700365 if (drop_rate && *drop_rate >= kFramedropPercentThreshold) {
Åsa Persson0ad2d8a2018-04-19 11:06:11 +0200366 RTC_LOG(LS_INFO) << "Reporting high QP, framedrop percent " << *drop_rate;
Henrik Boström012aa372020-04-27 17:40:55 +0200367 return CheckQpResult::kHighQp;
kthelgason194f40a2016-09-14 02:14:58 -0700368 }
369
370 // Check if we should scale up or down based on QP.
Ilya Nikolaevskiy26341992018-11-05 12:55:18 +0100371 const absl::optional<int> avg_qp_high =
372 qp_smoother_high_ ? qp_smoother_high_->GetAvg()
373 : average_qp_.GetAverageRoundedDown();
Danil Chapovalov0040b662018-06-18 10:48:16 +0200374 const absl::optional<int> avg_qp_low =
Ilya Nikolaevskiy26341992018-11-05 12:55:18 +0100375 qp_smoother_low_ ? qp_smoother_low_->GetAvg()
376 : average_qp_.GetAverageRoundedDown();
Åsa Perssona945aee2018-04-24 16:53:25 +0200377 if (avg_qp_high && avg_qp_low) {
378 RTC_LOG(LS_INFO) << "Checking average QP " << *avg_qp_high << " ("
379 << *avg_qp_low << ").";
380 if (*avg_qp_high > thresholds_.high) {
Henrik Boström012aa372020-04-27 17:40:55 +0200381 return CheckQpResult::kHighQp;
glaznevd1c44352017-03-23 14:40:08 -0700382 }
Åsa Perssona945aee2018-04-24 16:53:25 +0200383 if (*avg_qp_low <= thresholds_.low) {
glaznevd1c44352017-03-23 14:40:08 -0700384 // QP has been low. We want to try a higher resolution.
Henrik Boström012aa372020-04-27 17:40:55 +0200385 return CheckQpResult::kLowQp;
glaznevd1c44352017-03-23 14:40:08 -0700386 }
kthelgason194f40a2016-09-14 02:14:58 -0700387 }
Henrik Boström012aa372020-04-27 17:40:55 +0200388 return CheckQpResult::kNormalQp;
jackychen6e2ce6e2015-07-13 16:26:33 -0700389}
pbos@webrtc.orga0d78272014-09-12 11:51:47 +0000390
pbos@webrtc.orga0d78272014-09-12 11:51:47 +0000391void QualityScaler::ClearSamples() {
Sebastian Janssonb55015e2019-04-09 13:44:04 +0200392 RTC_DCHECK_RUN_ON(&task_checker_);
Åsa Perssona945aee2018-04-24 16:53:25 +0200393 framedrop_percent_media_opt_.Reset();
394 framedrop_percent_all_.Reset();
kthelgason194f40a2016-09-14 02:14:58 -0700395 average_qp_.Reset();
Åsa Perssona945aee2018-04-24 16:53:25 +0200396 if (qp_smoother_high_)
397 qp_smoother_high_->Reset();
398 if (qp_smoother_low_)
399 qp_smoother_low_->Reset();
pboscbac40d2016-04-13 02:51:02 -0700400}
Henrik Boström012aa372020-04-27 17:40:55 +0200401
402QualityScalerQpUsageHandlerInterface::~QualityScalerQpUsageHandlerInterface() {}
403
404QualityScalerQpUsageHandlerCallbackInterface::
405 QualityScalerQpUsageHandlerCallbackInterface() {}
406
407QualityScalerQpUsageHandlerCallbackInterface::
408 ~QualityScalerQpUsageHandlerCallbackInterface() {}
409
pbos@webrtc.orga0d78272014-09-12 11:51:47 +0000410} // namespace webrtc