Improve adaptation to reordered packets in delay manager.
This is done by adding a reorder optimizer that estimates the probability of receiving reordered packets.
The optimal delay is decided by balancing the cost of increasing the delay against the probability of missing a reordered packet, resulting in a loss. This balance is decided using the `ms_per_loss_percent` parameter.
The usage and parameters can be controlled via field trial.
Bug: webrtc:10178
Change-Id: Ic484df0412af35610e74b3a6070f2bac7a926a63
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/231541
Reviewed-by: Ivo Creusen <ivoc@webrtc.org>
Commit-Queue: Jakob Ivarsson <jakobi@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#34954}
diff --git a/modules/audio_coding/BUILD.gn b/modules/audio_coding/BUILD.gn
index d5fb1a4..e67a05d 100644
--- a/modules/audio_coding/BUILD.gn
+++ b/modules/audio_coding/BUILD.gn
@@ -976,6 +976,8 @@
"neteq/red_payload_splitter.h",
"neteq/relative_arrival_delay_tracker.cc",
"neteq/relative_arrival_delay_tracker.h",
+ "neteq/reorder_optimizer.cc",
+ "neteq/reorder_optimizer.h",
"neteq/statistics_calculator.cc",
"neteq/statistics_calculator.h",
"neteq/sync_buffer.cc",
@@ -2016,6 +2018,7 @@
"neteq/random_vector_unittest.cc",
"neteq/red_payload_splitter_unittest.cc",
"neteq/relative_arrival_delay_tracker_unittest.cc",
+ "neteq/reorder_optimizer_unittest.cc",
"neteq/statistics_calculator_unittest.cc",
"neteq/sync_buffer_unittest.cc",
"neteq/time_stretch_unittest.cc",
diff --git a/modules/audio_coding/neteq/delay_manager.cc b/modules/audio_coding/neteq/delay_manager.cc
index 9f7eebd..97d1d2e 100644
--- a/modules/audio_coding/neteq/delay_manager.cc
+++ b/modules/audio_coding/neteq/delay_manager.cc
@@ -32,6 +32,16 @@
constexpr int kMaxBaseMinimumDelayMs = 10000;
constexpr int kStartDelayMs = 80;
+std::unique_ptr<ReorderOptimizer> MaybeCreateReorderOptimizer(
+ const DelayManager::Config& config) {
+ if (!config.use_reorder_optimizer) {
+ return nullptr;
+ }
+ return std::make_unique<ReorderOptimizer>(
+ (1 << 15) * config.reorder_forget_factor, config.ms_per_loss_percent,
+ config.start_forget_weight);
+}
+
} // namespace
DelayManager::Config::Config() {
@@ -47,16 +57,22 @@
<< " start_forget_weight=" << start_forget_weight.value_or(0)
<< " resample_interval_ms="
<< resample_interval_ms.value_or(0)
- << " max_history_ms=" << max_history_ms;
+ << " max_history_ms=" << max_history_ms
+ << " use_reorder_optimizer=" << use_reorder_optimizer
+ << " reorder_forget_factor=" << reorder_forget_factor
+ << " ms_per_loss_percent=" << ms_per_loss_percent;
}
std::unique_ptr<StructParametersParser> DelayManager::Config::Parser() {
- return StructParametersParser::Create( //
- "quantile", &quantile, //
- "forget_factor", &forget_factor, //
- "start_forget_weight", &start_forget_weight, //
- "resample_interval_ms", &resample_interval_ms, //
- "max_history_ms", &max_history_ms);
+ return StructParametersParser::Create( //
+ "quantile", &quantile, //
+ "forget_factor", &forget_factor, //
+ "start_forget_weight", &start_forget_weight, //
+ "resample_interval_ms", &resample_interval_ms, //
+ "max_history_ms", &max_history_ms, //
+ "use_reorder_optimizer", &use_reorder_optimizer, //
+ "reorder_forget_factor", &reorder_forget_factor, //
+ "ms_per_loss_percent", &ms_per_loss_percent);
}
// TODO(jakobi): remove legacy field trial.
@@ -90,6 +106,7 @@
(1 << 15) * config.forget_factor,
config.start_forget_weight,
config.resample_interval_ms),
+ reorder_optimizer_(MaybeCreateReorderOptimizer(config)),
relative_arrival_delay_tracker_(tick_timer, config.max_history_ms),
base_minimum_delay_ms_(config.base_minimum_delay_ms),
effective_minimum_delay_ms_(config.base_minimum_delay_ms),
@@ -115,9 +132,18 @@
return absl::nullopt;
}
- underrun_optimizer_.Update(*relative_delay);
+ bool reordered =
+ relative_arrival_delay_tracker_.newest_timestamp() != timestamp;
+ if (!reorder_optimizer_ || !reordered) {
+ underrun_optimizer_.Update(*relative_delay);
+ }
target_level_ms_ =
underrun_optimizer_.GetOptimalDelayMs().value_or(kStartDelayMs);
+ if (reorder_optimizer_) {
+ reorder_optimizer_->Update(*relative_delay, reordered, target_level_ms_);
+ target_level_ms_ = std::max(
+ target_level_ms_, reorder_optimizer_->GetOptimalDelayMs().value_or(0));
+ }
target_level_ms_ = std::max(target_level_ms_, effective_minimum_delay_ms_);
if (maximum_delay_ms_ > 0) {
target_level_ms_ = std::min(target_level_ms_, maximum_delay_ms_);
@@ -148,6 +174,9 @@
underrun_optimizer_.Reset();
relative_arrival_delay_tracker_.Reset();
target_level_ms_ = kStartDelayMs;
+ if (reorder_optimizer_) {
+ reorder_optimizer_->Reset();
+ }
}
int DelayManager::TargetDelayMs() const {
diff --git a/modules/audio_coding/neteq/delay_manager.h b/modules/audio_coding/neteq/delay_manager.h
index c751836..277b80d 100644
--- a/modules/audio_coding/neteq/delay_manager.h
+++ b/modules/audio_coding/neteq/delay_manager.h
@@ -20,6 +20,7 @@
#include "api/neteq/tick_timer.h"
#include "modules/audio_coding/neteq/histogram.h"
#include "modules/audio_coding/neteq/relative_arrival_delay_tracker.h"
+#include "modules/audio_coding/neteq/reorder_optimizer.h"
#include "modules/audio_coding/neteq/underrun_optimizer.h"
#include "rtc_base/constructor_magic.h"
#include "rtc_base/experiments/struct_parameters_parser.h"
@@ -39,6 +40,10 @@
absl::optional<int> resample_interval_ms;
int max_history_ms = 2000;
+ bool use_reorder_optimizer = true;
+ double reorder_forget_factor = 0.9993;
+ int ms_per_loss_percent = 20;
+
// Options that are externally populated.
int max_packets_in_buffer = 200;
int base_minimum_delay_ms = 0;
@@ -104,6 +109,7 @@
// TODO(jakobi): set maximum buffer delay instead of number of packets.
const int max_packets_in_buffer_;
UnderrunOptimizer underrun_optimizer_;
+ std::unique_ptr<ReorderOptimizer> reorder_optimizer_;
RelativeArrivalDelayTracker relative_arrival_delay_tracker_;
int base_minimum_delay_ms_;
diff --git a/modules/audio_coding/neteq/histogram.h b/modules/audio_coding/neteq/histogram.h
index 5b2f2b1..265a10e 100644
--- a/modules/audio_coding/neteq/histogram.h
+++ b/modules/audio_coding/neteq/histogram.h
@@ -42,7 +42,7 @@
virtual int NumBuckets() const;
// Returns the probability for each bucket in Q30.
- std::vector<int> buckets() const { return buckets_; }
+ const std::vector<int>& buckets() const { return buckets_; }
// Accessors only intended for testing purposes.
int base_forget_factor_for_testing() const { return base_forget_factor_; }
diff --git a/modules/audio_coding/neteq/relative_arrival_delay_tracker.cc b/modules/audio_coding/neteq/relative_arrival_delay_tracker.cc
index 5b9580c..02c5a43 100644
--- a/modules/audio_coding/neteq/relative_arrival_delay_tracker.cc
+++ b/modules/audio_coding/neteq/relative_arrival_delay_tracker.cc
@@ -12,6 +12,8 @@
#include <algorithm>
+#include "modules/include/module_common_types_public.h"
+
namespace webrtc {
absl::optional<int> RelativeArrivalDelayTracker::Update(uint32_t timestamp,
@@ -23,6 +25,7 @@
// Restart relative delay esimation from this packet.
delay_history_.clear();
packet_iat_stopwatch_ = tick_timer_->GetNewStopwatch();
+ newest_timestamp_ = timestamp;
last_timestamp_ = timestamp;
return absl::nullopt;
}
@@ -37,6 +40,9 @@
packet_iat_stopwatch_ = tick_timer_->GetNewStopwatch();
last_timestamp_ = timestamp;
+ if (IsNewerTimestamp(timestamp, *newest_timestamp_)) {
+ newest_timestamp_ = timestamp;
+ }
return relative_delay;
}
@@ -44,6 +50,7 @@
void RelativeArrivalDelayTracker::Reset() {
delay_history_.clear();
packet_iat_stopwatch_ = tick_timer_->GetNewStopwatch();
+ newest_timestamp_ = absl::nullopt;
last_timestamp_ = absl::nullopt;
}
diff --git a/modules/audio_coding/neteq/relative_arrival_delay_tracker.h b/modules/audio_coding/neteq/relative_arrival_delay_tracker.h
index da7f121..fed56be 100644
--- a/modules/audio_coding/neteq/relative_arrival_delay_tracker.h
+++ b/modules/audio_coding/neteq/relative_arrival_delay_tracker.h
@@ -28,6 +28,10 @@
void Reset();
+ absl::optional<uint32_t> newest_timestamp() const {
+ return newest_timestamp_;
+ }
+
private:
// Updates `delay_history_`.
void UpdateDelayHistory(int iat_delay_ms,
@@ -46,8 +50,8 @@
};
std::deque<PacketDelay> delay_history_;
- absl::optional<uint32_t>
- last_timestamp_; // Timestamp for the last received packet.
+ absl::optional<uint32_t> newest_timestamp_;
+ absl::optional<uint32_t> last_timestamp_;
std::unique_ptr<TickTimer::Stopwatch>
packet_iat_stopwatch_; // Time elapsed since last packet.
diff --git a/modules/audio_coding/neteq/relative_arrival_delay_tracker_unittest.cc b/modules/audio_coding/neteq/relative_arrival_delay_tracker_unittest.cc
index f764b85..b4e9456 100644
--- a/modules/audio_coding/neteq/relative_arrival_delay_tracker_unittest.cc
+++ b/modules/audio_coding/neteq/relative_arrival_delay_tracker_unittest.cc
@@ -49,14 +49,17 @@
// Insert reordered packet.
EXPECT_EQ(tracker.Update(kTs - 4 * kTsIncrement, kFs), 80);
+ EXPECT_EQ(tracker.newest_timestamp(), kTs);
// Insert another reordered packet.
EXPECT_EQ(tracker.Update(kTs - kTsIncrement, kFs), 20);
+ EXPECT_EQ(tracker.newest_timestamp(), kTs);
// Insert the next packet in order and verify that the relative delay is
// estimated based on the first inserted packet.
tick_timer.Increment(4 * kFrameSizeMs / tick_timer.ms_per_tick());
EXPECT_EQ(tracker.Update(kTs + kTsIncrement, kFs), 60);
+ EXPECT_EQ(tracker.newest_timestamp(), kTs + kTsIncrement);
}
TEST(RelativeArrivalDelayTrackerTest, MaxDelayHistory) {
diff --git a/modules/audio_coding/neteq/reorder_optimizer.cc b/modules/audio_coding/neteq/reorder_optimizer.cc
new file mode 100644
index 0000000..f6e073f
--- /dev/null
+++ b/modules/audio_coding/neteq/reorder_optimizer.cc
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2021 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 "modules/audio_coding/neteq/reorder_optimizer.h"
+
+#include <algorithm>
+#include <limits>
+#include <vector>
+
+namespace webrtc {
+
+namespace {
+
+constexpr int kDelayBuckets = 100;
+constexpr int kBucketSizeMs = 20;
+
+} // namespace
+
+ReorderOptimizer::ReorderOptimizer(int forget_factor,
+ int ms_per_loss_percent,
+ absl::optional<int> start_forget_weight)
+ : histogram_(kDelayBuckets, forget_factor, start_forget_weight),
+ ms_per_loss_percent_(ms_per_loss_percent) {}
+
+void ReorderOptimizer::Update(int relative_delay_ms,
+ bool reordered,
+ int base_delay_ms) {
+ const int index = reordered ? relative_delay_ms / kBucketSizeMs : 0;
+ if (index < histogram_.NumBuckets()) {
+ // Maximum delay to register is 2000 ms.
+ histogram_.Add(index);
+ }
+ int bucket_index = MinimizeCostFunction(base_delay_ms);
+ optimal_delay_ms_ = (1 + bucket_index) * kBucketSizeMs;
+}
+
+void ReorderOptimizer::Reset() {
+ histogram_.Reset();
+ optimal_delay_ms_.reset();
+}
+
+int ReorderOptimizer::MinimizeCostFunction(int base_delay_ms) const {
+ const std::vector<int>& buckets = histogram_.buckets();
+
+ // Values are calculated in Q30.
+ int64_t loss_probability = 1 << 30;
+ int64_t min_cost = std::numeric_limits<int64_t>::max();
+ int min_bucket = 0;
+ for (int i = 0; i < static_cast<int>(buckets.size()); ++i) {
+ loss_probability -= buckets[i];
+ int64_t delay_ms =
+ static_cast<int64_t>(std::max(0, i * kBucketSizeMs - base_delay_ms))
+ << 30;
+ int64_t cost = delay_ms + 100 * ms_per_loss_percent_ * loss_probability;
+
+ if (cost < min_cost) {
+ min_cost = cost;
+ min_bucket = i;
+ }
+ if (loss_probability == 0) {
+ break;
+ }
+ }
+
+ return min_bucket;
+}
+
+} // namespace webrtc
diff --git a/modules/audio_coding/neteq/reorder_optimizer.h b/modules/audio_coding/neteq/reorder_optimizer.h
new file mode 100644
index 0000000..06f6bc7
--- /dev/null
+++ b/modules/audio_coding/neteq/reorder_optimizer.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2021 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.
+ */
+
+#ifndef MODULES_AUDIO_CODING_NETEQ_REORDER_OPTIMIZER_H_
+#define MODULES_AUDIO_CODING_NETEQ_REORDER_OPTIMIZER_H_
+
+#include "absl/types/optional.h"
+#include "modules/audio_coding/neteq/histogram.h"
+
+namespace webrtc {
+
+// Calculates an optimal delay to reduce the chance of missing reordered
+// packets. The delay/loss trade-off can be tune using the `ms_per_loss_percent`
+// parameter.
+class ReorderOptimizer {
+ public:
+ ReorderOptimizer(int forget_factor,
+ int ms_per_loss_percent,
+ absl::optional<int> start_forget_weight);
+
+ void Update(int relative_delay_ms, bool reordered, int base_delay_ms);
+
+ absl::optional<int> GetOptimalDelayMs() const { return optimal_delay_ms_; }
+
+ void Reset();
+
+ private:
+ int MinimizeCostFunction(int base_delay_ms) const;
+
+ Histogram histogram_;
+ const int ms_per_loss_percent_;
+ absl::optional<int> optimal_delay_ms_;
+};
+
+} // namespace webrtc
+#endif // MODULES_AUDIO_CODING_NETEQ_REORDER_OPTIMIZER_H_
diff --git a/modules/audio_coding/neteq/reorder_optimizer_unittest.cc b/modules/audio_coding/neteq/reorder_optimizer_unittest.cc
new file mode 100644
index 0000000..aaa1062
--- /dev/null
+++ b/modules/audio_coding/neteq/reorder_optimizer_unittest.cc
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2021 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 "modules/audio_coding/neteq/reorder_optimizer.h"
+
+#include "test/gtest.h"
+
+namespace webrtc {
+
+namespace {
+
+constexpr int kForgetFactor = 32745; // 0.9993 in Q15.
+constexpr int kMsPerLossPercent = 20;
+constexpr int kStartForgetWeight = 1;
+
+} // namespace
+
+TEST(ReorderOptimizerTest, OnlyIncreaseDelayForReorderedPackets) {
+ ReorderOptimizer reorder_optimizer(kForgetFactor, kMsPerLossPercent,
+ kStartForgetWeight);
+ EXPECT_FALSE(reorder_optimizer.GetOptimalDelayMs());
+
+ // Delay should not increase for in-order packets.
+ reorder_optimizer.Update(60, /*reordered=*/false, 0);
+ EXPECT_EQ(reorder_optimizer.GetOptimalDelayMs(), 20);
+
+ reorder_optimizer.Update(100, /*reordered=*/false, 0);
+ EXPECT_EQ(reorder_optimizer.GetOptimalDelayMs(), 20);
+
+ reorder_optimizer.Update(80, /*reordered=*/true, 0);
+ EXPECT_EQ(reorder_optimizer.GetOptimalDelayMs(), 100);
+}
+
+TEST(ReorderOptimizerTest, AvoidIncreasingDelayWhenProbabilityIsLow) {
+ ReorderOptimizer reorder_optimizer(kForgetFactor, kMsPerLossPercent,
+ kStartForgetWeight);
+
+ reorder_optimizer.Update(40, /*reordered=*/true, 0);
+ reorder_optimizer.Update(40, /*reordered=*/true, 0);
+ reorder_optimizer.Update(40, /*reordered=*/true, 0);
+ EXPECT_EQ(reorder_optimizer.GetOptimalDelayMs(), 60);
+
+ // The cost of the delay is too high relative the probability.
+ reorder_optimizer.Update(600, /*reordered=*/true, 0);
+ EXPECT_EQ(reorder_optimizer.GetOptimalDelayMs(), 60);
+}
+
+TEST(ReorderOptimizerTest, BaseDelayIsSubtractedFromCost) {
+ constexpr int kBaseDelayMs = 200;
+ ReorderOptimizer reorder_optimizer(kForgetFactor, kMsPerLossPercent,
+ kStartForgetWeight);
+
+ reorder_optimizer.Update(40, /*reordered=*/true, kBaseDelayMs);
+ reorder_optimizer.Update(40, /*reordered=*/true, kBaseDelayMs);
+ reorder_optimizer.Update(40, /*reordered=*/true, kBaseDelayMs);
+ EXPECT_EQ(reorder_optimizer.GetOptimalDelayMs(), 60);
+
+ // The cost of the delay is too high relative the probability.
+ reorder_optimizer.Update(600, /*reordered=*/true, kBaseDelayMs);
+ EXPECT_EQ(reorder_optimizer.GetOptimalDelayMs(), 620);
+}
+
+} // namespace webrtc