Introduce EncodedImageIdInjector.
EncodedImageIdInjector is responsible for injection of frame id into
encoded image before it will be sent to the transport layer. It will
help to track video frame from capturing on 1st peer side to rendering
on 2nd peer side and will make it possible to calculate video quality
stats between these frames.
This CL also introduces two different implementations for injector:
1. DefaultEncodedImageIdInjector will prepend all encoded images with
extra data and then will restore them on another side. This injector
can work even if peers are running on different devices.
2. SingleProcessEncodedImageIdInjector can work only when all peers
are running in the same process, but won't use any extra data
to propagate frame id between peers, so it won't affect any
transport level metrics and bitrate estimator.
This CL is first part of new video quality analyzer for end-2-end
peer connection level test framework.
Bug: webrtc:10138
Change-Id: I77defc8e8c95cb244a695a9732980a47bd7a2e9b
Reviewed-on: https://webrtc-review.googlesource.com/c/116682
Reviewed-by: Stefan Holmer <stefan@webrtc.org>
Reviewed-by: Peter Slatala <psla@webrtc.org>
Reviewed-by: Niels Moller <nisse@webrtc.org>
Reviewed-by: Ilya Nikolaevskiy <ilnik@webrtc.org>
Commit-Queue: Artem Titov <titovartem@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#26251}
diff --git a/test/BUILD.gn b/test/BUILD.gn
index 3d4b884..bb49e2e 100644
--- a/test/BUILD.gn
+++ b/test/BUILD.gn
@@ -21,6 +21,7 @@
":test_renderer",
":test_support",
":video_test_common",
+ "pc/e2e",
"pc/e2e/api:peer_connection_quality_test_fixture_api",
]
@@ -349,6 +350,7 @@
"../modules/video_coding:simulcast_test_fixture_impl",
"../rtc_base:rtc_base_approved",
"../test:single_threaded_task_queue",
+ "pc/e2e:e2e_unittests",
"scenario:scenario_unittests",
"//testing/gmock",
"//testing/gtest",
diff --git a/test/pc/e2e/BUILD.gn b/test/pc/e2e/BUILD.gn
new file mode 100644
index 0000000..c9d1f48
--- /dev/null
+++ b/test/pc/e2e/BUILD.gn
@@ -0,0 +1,101 @@
+# Copyright (c) 2019 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.
+
+import("../../../webrtc.gni")
+
+group("e2e") {
+ testonly = true
+
+ deps = [
+ ":default_encoded_image_id_injector",
+ ":encoded_image_id_injector_api",
+ ":single_process_encoded_image_id_injector",
+ ]
+}
+
+if (rtc_include_tests) {
+ group("e2e_unittests") {
+ testonly = true
+
+ deps = [
+ ":default_encoded_image_id_injector_unittest",
+ ":single_process_encoded_image_id_injector_unittest",
+ ]
+ }
+}
+
+rtc_source_set("encoded_image_id_injector_api") {
+ visibility = [ "*" ]
+ sources = [
+ "analyzer/video/encoded_image_id_injector.h",
+ ]
+
+ deps = [
+ "../../../api/video:encoded_image",
+ ]
+}
+
+rtc_source_set("default_encoded_image_id_injector") {
+ visibility = [ "*" ]
+ sources = [
+ "analyzer/video/default_encoded_image_id_injector.cc",
+ "analyzer/video/default_encoded_image_id_injector.h",
+ ]
+
+ deps = [
+ ":encoded_image_id_injector_api",
+ "../../../api/video:encoded_image",
+ "../../../rtc_base:checks",
+ "../../../rtc_base:criticalsection",
+ "//third_party/abseil-cpp/absl/memory:memory",
+ ]
+}
+
+rtc_source_set("single_process_encoded_image_id_injector") {
+ visibility = [ "*" ]
+ sources = [
+ "analyzer/video/single_process_encoded_image_id_injector.cc",
+ "analyzer/video/single_process_encoded_image_id_injector.h",
+ ]
+
+ deps = [
+ ":encoded_image_id_injector_api",
+ "../../../api/video:encoded_image",
+ "../../../rtc_base:checks",
+ "../../../rtc_base:criticalsection",
+ "//third_party/abseil-cpp/absl/memory:memory",
+ ]
+}
+
+if (rtc_include_tests) {
+ rtc_source_set("single_process_encoded_image_id_injector_unittest") {
+ testonly = true
+ sources = [
+ "analyzer/video/single_process_encoded_image_id_injector_unittest.cc",
+ ]
+ deps = [
+ ":single_process_encoded_image_id_injector",
+ "../../../api/video:encoded_image",
+ "../../../rtc_base:rtc_base_approved",
+ "../../../test:test_support",
+ ]
+ }
+
+ rtc_source_set("default_encoded_image_id_injector_unittest") {
+ testonly = true
+ sources = [
+ "analyzer/video/default_encoded_image_id_injector_unittest.cc",
+ ]
+ deps = [
+ ":default_encoded_image_id_injector",
+ "../../../api/video:encoded_image",
+ "../../../rtc_base:rtc_base_approved",
+ "../../../test:test_support",
+ ]
+ }
+}
diff --git a/test/pc/e2e/analyzer/video/default_encoded_image_id_injector.cc b/test/pc/e2e/analyzer/video/default_encoded_image_id_injector.cc
new file mode 100644
index 0000000..b0a5aac
--- /dev/null
+++ b/test/pc/e2e/analyzer/video/default_encoded_image_id_injector.cc
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2019 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 "test/pc/e2e/analyzer/video/default_encoded_image_id_injector.h"
+
+#include <cstddef>
+
+#include "absl/memory/memory.h"
+#include "api/video/encoded_image.h"
+#include "rtc_base/checks.h"
+
+namespace webrtc {
+namespace test {
+namespace {
+
+// The amount on which encoded image buffer will be expanded to inject frame id.
+// This is 2 bytes for uint16_t frame id itself and 4 bytes for original length
+// of the buffer.
+constexpr int kEncodedImageBufferExpansion = 6;
+constexpr size_t kInitialBufferSize = 2 * 1024;
+// Count of coding entities for which buffers pools will be added on
+// construction.
+constexpr int kPreInitCodingEntitiesCount = 2;
+constexpr size_t kBuffersPoolPerCodingEntity = 256;
+
+} // namespace
+
+DefaultEncodedImageIdInjector::DefaultEncodedImageIdInjector() {
+ for (size_t i = 0;
+ i < kPreInitCodingEntitiesCount * kBuffersPoolPerCodingEntity; ++i) {
+ bufs_pool_.push_back(
+ absl::make_unique<std::vector<uint8_t>>(kInitialBufferSize));
+ }
+}
+DefaultEncodedImageIdInjector::~DefaultEncodedImageIdInjector() = default;
+
+EncodedImage DefaultEncodedImageIdInjector::InjectId(uint16_t id,
+ const EncodedImage& source,
+ int coding_entity_id) {
+ ExtendIfRequired(coding_entity_id);
+
+ EncodedImage out = source;
+ std::vector<uint8_t>* buffer = NextBuffer();
+ if (buffer->size() < source.size() + kEncodedImageBufferExpansion) {
+ buffer->resize(source.size() + kEncodedImageBufferExpansion);
+ }
+ out.set_buffer(buffer->data(), buffer->size());
+ out.set_size(source.size() + kEncodedImageBufferExpansion);
+ memcpy(&out.data()[kEncodedImageBufferExpansion], source.data(),
+ source.size());
+ out.data()[0] = id & 0x00ff;
+ out.data()[1] = (id & 0xff00) >> 8;
+ out.data()[2] = source.size() & 0x00ff;
+ out.data()[3] = (source.size() & 0xff00) >> 8;
+ out.data()[4] = (source.size() & 0xff00) >> 16;
+ out.data()[5] = (source.size() & 0xff00) >> 24;
+ return out;
+}
+
+std::pair<uint16_t, EncodedImage> DefaultEncodedImageIdInjector::ExtractId(
+ const EncodedImage& source,
+ int coding_entity_id) {
+ ExtendIfRequired(coding_entity_id);
+
+ EncodedImage out = source;
+ std::vector<uint8_t>* buffer = NextBuffer();
+ if (buffer->size() < source.capacity() - kEncodedImageBufferExpansion) {
+ buffer->resize(source.capacity() - kEncodedImageBufferExpansion);
+ }
+ out.set_buffer(buffer->data(), buffer->size());
+
+ size_t source_pos = 0;
+ size_t out_pos = 0;
+ absl::optional<uint16_t> id = absl::nullopt;
+ while (source_pos < source.size()) {
+ RTC_CHECK_LE(source_pos + kEncodedImageBufferExpansion, source.size());
+ uint16_t next_id =
+ source.data()[source_pos] + (source.data()[source_pos + 1] << 8);
+ RTC_CHECK(!id || id.value() == next_id)
+ << "Different frames encoded into single encoded image: " << id.value()
+ << " vs " << next_id;
+ id = next_id;
+ uint32_t length = source.data()[source_pos + 2] +
+ (source.data()[source_pos + 3] << 8) +
+ (source.data()[source_pos + 3] << 16) +
+ (source.data()[source_pos + 3] << 24);
+ RTC_CHECK_LE(source_pos + kEncodedImageBufferExpansion + length,
+ source.size());
+ memcpy(&out.data()[out_pos],
+ &source.data()[source_pos + kEncodedImageBufferExpansion], length);
+ source_pos += length + kEncodedImageBufferExpansion;
+ out_pos += length;
+ }
+ out.set_size(out_pos);
+
+ return std::pair<uint16_t, EncodedImage>(id.value(), out);
+}
+
+void DefaultEncodedImageIdInjector::ExtendIfRequired(int coding_entity_id) {
+ rtc::CritScope crit(&lock_);
+ if (coding_entities_.find(coding_entity_id) != coding_entities_.end()) {
+ // This entity is already known for this injector, so buffers are allocated.
+ return;
+ }
+
+ // New coding entity. We need allocate extra buffers for this encoder/decoder
+ // We will put them into front of the queue to use them first.
+ coding_entities_.insert(coding_entity_id);
+ if (coding_entities_.size() <= kPreInitCodingEntitiesCount) {
+ // Buffers for the first kPreInitCodingEntitiesCount coding entities were
+ // allocated during construction.
+ return;
+ }
+ for (size_t i = 0; i < kBuffersPoolPerCodingEntity; ++i) {
+ bufs_pool_.push_front(
+ absl::make_unique<std::vector<uint8_t>>(kInitialBufferSize));
+ }
+}
+
+std::vector<uint8_t>* DefaultEncodedImageIdInjector::NextBuffer() {
+ rtc::CritScope crit(&lock_);
+ // Get buffer from the front of the queue, return it to the caller and
+ // put in the back
+ std::vector<uint8_t>* out = bufs_pool_.front().get();
+ bufs_pool_.push_back(std::move(bufs_pool_.front()));
+ bufs_pool_.pop_front();
+ return out;
+}
+
+} // namespace test
+} // namespace webrtc
diff --git a/test/pc/e2e/analyzer/video/default_encoded_image_id_injector.h b/test/pc/e2e/analyzer/video/default_encoded_image_id_injector.h
new file mode 100644
index 0000000..1f19dc3
--- /dev/null
+++ b/test/pc/e2e/analyzer/video/default_encoded_image_id_injector.h
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2019 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 TEST_PC_E2E_ANALYZER_VIDEO_DEFAULT_ENCODED_IMAGE_ID_INJECTOR_H_
+#define TEST_PC_E2E_ANALYZER_VIDEO_DEFAULT_ENCODED_IMAGE_ID_INJECTOR_H_
+
+#include <cstdint>
+#include <deque>
+#include <memory>
+#include <set>
+#include <utility>
+#include <vector>
+
+#include "api/video/encoded_image.h"
+#include "rtc_base/critical_section.h"
+#include "test/pc/e2e/analyzer/video/encoded_image_id_injector.h"
+
+namespace webrtc {
+namespace test {
+
+// Injects frame id into EncodedImage payload buffer. The payload buffer will
+// be prepended in the injector with 2 bytes frame id and 4 bytes original
+// buffer length. It is assumed, that frame's data can't be more then 2^32
+// bytes. In the decoder, frame id will be extracted and the length will be used
+// to restore original buffer.
+//
+// The data in the EncodedImage on encoder side after injection will look like
+// this:
+// 4 bytes frame length
+// _ _ _↓_ _ _ _________________
+// | | | original buffer |
+// ¯↑¯ ¯ ¯ ¯ ¯ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// 2 bytes frame id
+//
+// But on decoder side multiple payloads can be concatenated into single
+// EncodedImage in jitter buffer and its payload will look like this:
+// _ _ _ _ _ _ _________ _ _ _ _ _ _ _________ _ _ _ _ _ _ _________
+// buf: | | | payload | | | payload | | | payload |
+// ¯ ¯ ¯ ¯ ¯ ¯ ¯¯¯¯¯¯¯¯¯ ¯ ¯ ¯ ¯ ¯ ¯ ¯¯¯¯¯¯¯¯¯ ¯ ¯ ¯ ¯ ¯ ¯ ¯¯¯¯¯¯¯¯¯
+// To correctly restore such images we will extract id by this algorithm:
+// 1. pos = 0
+// 2. Extract id from buf[pos] and buf[pos + 1]
+// 3. Extract length from buf[pos + 2]..buf[pos + 5]
+// 4. Copy |length| bytes starting from buf[pos + 6] to output buffer.
+// 5. pos = pos + length + 6
+// 6. If pos < buf.length - go to the step 2.
+// Also it will be checked, that all extracted ids are equals.
+//
+// Because EncodedImage doesn't take ownership of its buffer, injector will keep
+// ownership of the buffers that will be used for EncodedImages with injected
+// data. This is needed because there is no way to inform the injector that
+// a buffer can be disposed. To address this issue injector will use a pool
+// of buffers in round robin manner and will assume, that when it overlaps
+// the buffer can be disposed.
+//
+// Because single injector can be used for different coding entities (encoders
+// or decoders), it will store a |coding_entity_id| in the set for each
+// coding entity seen and if the new one arrives, it will extend its buffers
+// pool, adding 256 more buffers. During initialization injector will
+// preallocate buffers for 2 coding entities, so 512 buffers with initial size
+// 2KB. If in some point of time bigger buffer will be required, it will be also
+// extended.
+class DefaultEncodedImageIdInjector : public EncodedImageIdInjector,
+ public EncodedImageIdExtractor {
+ public:
+ DefaultEncodedImageIdInjector();
+ ~DefaultEncodedImageIdInjector() override;
+
+ EncodedImage InjectId(uint16_t id,
+ const EncodedImage& source,
+ int coding_entity_id) override;
+ std::pair<uint16_t, EncodedImage> ExtractId(const EncodedImage& source,
+ int coding_entity_id) override;
+
+ private:
+ void ExtendIfRequired(int coding_entity_id) RTC_LOCKS_EXCLUDED(lock_);
+ std::vector<uint8_t>* NextBuffer() RTC_LOCKS_EXCLUDED(lock_);
+
+ // Because single injector will be used for all encoder and decoders in one
+ // peer and in case of the single process for all encoders and decoders in
+ // another peer, it can be called from different threads. So we need to ensure
+ // that buffers are given consecutively from pools and pool extension won't
+ // be interrupted by getting buffer in other thread.
+ rtc::CriticalSection lock_;
+
+ // Store coding entities for which buffers pool have been already extended.
+ std::set<int> coding_entities_ RTC_GUARDED_BY(lock_);
+ std::deque<std::unique_ptr<std::vector<uint8_t>>> bufs_pool_
+ RTC_GUARDED_BY(lock_);
+};
+
+} // namespace test
+} // namespace webrtc
+
+#endif // TEST_PC_E2E_ANALYZER_VIDEO_DEFAULT_ENCODED_IMAGE_ID_INJECTOR_H_
diff --git a/test/pc/e2e/analyzer/video/default_encoded_image_id_injector_unittest.cc b/test/pc/e2e/analyzer/video/default_encoded_image_id_injector_unittest.cc
new file mode 100644
index 0000000..5886c86
--- /dev/null
+++ b/test/pc/e2e/analyzer/video/default_encoded_image_id_injector_unittest.cc
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2019 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 "test/pc/e2e/analyzer/video/default_encoded_image_id_injector.h"
+
+#include <utility>
+
+#include "api/video/encoded_image.h"
+#include "rtc_base/buffer.h"
+#include "test/gtest.h"
+
+namespace webrtc {
+namespace test {
+namespace {
+
+rtc::Buffer CreateBufferOfSizeNFilledWithValuesFromX(size_t n, uint8_t x) {
+ rtc::Buffer buffer(n);
+ for (size_t i = 0; i < n; ++i) {
+ buffer[i] = static_cast<uint8_t>(x + i);
+ }
+ return buffer;
+}
+
+} // namespace
+
+TEST(DefaultEncodedImageIdInjector, InjectExtract) {
+ DefaultEncodedImageIdInjector injector;
+
+ rtc::Buffer buffer = CreateBufferOfSizeNFilledWithValuesFromX(10, 1);
+
+ EncodedImage source(buffer.data(), 10, 10);
+ source.SetTimestamp(123456789);
+
+ std::pair<uint16_t, EncodedImage> out =
+ injector.ExtractId(injector.InjectId(512, source, 1), 2);
+ ASSERT_EQ(out.first, 512);
+ ASSERT_EQ(out.second.size(), 10ul);
+ for (int i = 0; i < 10; ++i) {
+ ASSERT_EQ(out.second.data()[i], i + 1);
+ }
+}
+
+TEST(DefaultEncodedImageIdInjector, Inject3Extract3) {
+ DefaultEncodedImageIdInjector injector;
+
+ rtc::Buffer buffer1 = CreateBufferOfSizeNFilledWithValuesFromX(10, 1);
+ rtc::Buffer buffer2 = CreateBufferOfSizeNFilledWithValuesFromX(10, 11);
+ rtc::Buffer buffer3 = CreateBufferOfSizeNFilledWithValuesFromX(10, 21);
+
+ // 1st frame
+ EncodedImage source1(buffer1.data(), 10, 10);
+ source1.SetTimestamp(123456710);
+ // 2nd frame 1st spatial layer
+ EncodedImage source2(buffer2.data(), 10, 10);
+ source2.SetTimestamp(123456720);
+ // 2nd frame 2nd spatial layer
+ EncodedImage source3(buffer3.data(), 10, 10);
+ source3.SetTimestamp(123456720);
+
+ EncodedImage intermediate1 = injector.InjectId(510, source1, 1);
+ EncodedImage intermediate2 = injector.InjectId(520, source2, 1);
+ EncodedImage intermediate3 = injector.InjectId(520, source3, 1);
+
+ // Extract ids in different order.
+ std::pair<uint16_t, EncodedImage> out3 = injector.ExtractId(intermediate3, 2);
+ std::pair<uint16_t, EncodedImage> out1 = injector.ExtractId(intermediate1, 2);
+ std::pair<uint16_t, EncodedImage> out2 = injector.ExtractId(intermediate2, 2);
+
+ ASSERT_EQ(out1.first, 510);
+ ASSERT_EQ(out1.second.size(), 10ul);
+ for (int i = 0; i < 10; ++i) {
+ ASSERT_EQ(out1.second.data()[i], i + 1);
+ }
+ ASSERT_EQ(out2.first, 520);
+ ASSERT_EQ(out2.second.size(), 10ul);
+ for (int i = 0; i < 10; ++i) {
+ ASSERT_EQ(out2.second.data()[i], i + 11);
+ }
+ ASSERT_EQ(out3.first, 520);
+ ASSERT_EQ(out3.second.size(), 10ul);
+ for (int i = 0; i < 10; ++i) {
+ ASSERT_EQ(out3.second.data()[i], i + 21);
+ }
+}
+
+TEST(DefaultEncodedImageIdInjector, InjectExtractFromConcatenated) {
+ DefaultEncodedImageIdInjector injector;
+
+ rtc::Buffer buffer1 = CreateBufferOfSizeNFilledWithValuesFromX(10, 1);
+ rtc::Buffer buffer2 = CreateBufferOfSizeNFilledWithValuesFromX(10, 11);
+ rtc::Buffer buffer3 = CreateBufferOfSizeNFilledWithValuesFromX(10, 21);
+
+ EncodedImage source1(buffer1.data(), 10, 10);
+ source1.SetTimestamp(123456710);
+ EncodedImage source2(buffer2.data(), 10, 10);
+ source2.SetTimestamp(123456710);
+ EncodedImage source3(buffer3.data(), 10, 10);
+ source3.SetTimestamp(123456710);
+
+ // Inject id into 3 images with same frame id.
+ EncodedImage intermediate1 = injector.InjectId(512, source1, 1);
+ EncodedImage intermediate2 = injector.InjectId(512, source2, 1);
+ EncodedImage intermediate3 = injector.InjectId(512, source3, 1);
+
+ // Concatenate them into single encoded image, like it can be done in jitter
+ // buffer.
+ size_t concatenated_length =
+ intermediate1.size() + intermediate2.size() + intermediate3.size();
+ rtc::Buffer concatenated_buffer;
+ concatenated_buffer.AppendData(intermediate1.data(), intermediate1.size());
+ concatenated_buffer.AppendData(intermediate2.data(), intermediate2.size());
+ concatenated_buffer.AppendData(intermediate3.data(), intermediate3.size());
+ EncodedImage concatenated(concatenated_buffer.data(), concatenated_length,
+ concatenated_length);
+
+ // Extract frame id from concatenated image
+ std::pair<uint16_t, EncodedImage> out = injector.ExtractId(concatenated, 2);
+
+ ASSERT_EQ(out.first, 512);
+ ASSERT_EQ(out.second.size(), 3 * 10ul);
+ for (int i = 0; i < 10; ++i) {
+ ASSERT_EQ(out.second.data()[i], i + 1);
+ ASSERT_EQ(out.second.data()[i + 10], i + 11);
+ ASSERT_EQ(out.second.data()[i + 20], i + 21);
+ }
+}
+
+} // namespace test
+} // namespace webrtc
diff --git a/test/pc/e2e/analyzer/video/encoded_image_id_injector.h b/test/pc/e2e/analyzer/video/encoded_image_id_injector.h
new file mode 100644
index 0000000..952c23a
--- /dev/null
+++ b/test/pc/e2e/analyzer/video/encoded_image_id_injector.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2019 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 TEST_PC_E2E_ANALYZER_VIDEO_ENCODED_IMAGE_ID_INJECTOR_H_
+#define TEST_PC_E2E_ANALYZER_VIDEO_ENCODED_IMAGE_ID_INJECTOR_H_
+
+#include <cstdint>
+#include <utility>
+
+#include "api/video/encoded_image.h"
+
+namespace webrtc {
+namespace test {
+
+// Injects frame id into EncodedImage on encoder side
+class EncodedImageIdInjector {
+ public:
+ virtual ~EncodedImageIdInjector() = default;
+
+ // Return encoded image with specified |id| injected into its payload.
+ // |coding_entity_id| is unique id of decoder or encoder.
+ virtual EncodedImage InjectId(uint16_t id,
+ const EncodedImage& source,
+ int coding_entity_id) = 0;
+};
+
+// Extracts frame id from EncodedImage on decoder side.
+class EncodedImageIdExtractor {
+ public:
+ virtual ~EncodedImageIdExtractor() = default;
+
+ // Returns encoded image id, extracted from payload and also encoded image
+ // with its original payload. For concatenated spatial layers it should be the
+ // same id. |coding_entity_id| is unique id of decoder or encoder.
+ virtual std::pair<uint16_t, EncodedImage> ExtractId(
+ const EncodedImage& source,
+ int coding_entity_id) = 0;
+};
+
+} // namespace test
+} // namespace webrtc
+
+#endif // TEST_PC_E2E_ANALYZER_VIDEO_ENCODED_IMAGE_ID_INJECTOR_H_
diff --git a/test/pc/e2e/analyzer/video/single_process_encoded_image_id_injector.cc b/test/pc/e2e/analyzer/video/single_process_encoded_image_id_injector.cc
new file mode 100644
index 0000000..0422797
--- /dev/null
+++ b/test/pc/e2e/analyzer/video/single_process_encoded_image_id_injector.cc
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2019 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 "test/pc/e2e/analyzer/video/single_process_encoded_image_id_injector.h"
+
+#include <cstddef>
+
+#include "absl/memory/memory.h"
+#include "api/video/encoded_image.h"
+#include "rtc_base/checks.h"
+
+namespace webrtc {
+namespace test {
+namespace {
+
+// Number of bytes from the beginning of the EncodedImage buffer that will be
+// used to store frame id and sub id.
+constexpr size_t kUsedBufferSize = 3;
+
+} // namespace
+
+SingleProcessEncodedImageIdInjector::SingleProcessEncodedImageIdInjector() =
+ default;
+SingleProcessEncodedImageIdInjector::~SingleProcessEncodedImageIdInjector() =
+ default;
+
+EncodedImage SingleProcessEncodedImageIdInjector::InjectId(
+ uint16_t id,
+ const EncodedImage& source,
+ int coding_entity_id) {
+ RTC_CHECK(source.size() >= kUsedBufferSize);
+
+ ExtractionInfo info;
+ info.length = source.size();
+ memcpy(info.origin_data, source.data(), kUsedBufferSize);
+ {
+ rtc::CritScope crit(&lock_);
+ // Will create new one if missed.
+ ExtractionInfoVector& ev = extraction_cache_[id];
+ info.sub_id = ev.next_sub_id++;
+ ev.infos[info.sub_id] = std::move(info);
+ }
+
+ EncodedImage out = source;
+ out.data()[0] = id & 0x00ff;
+ out.data()[1] = (id & 0xff00) >> 8;
+ out.data()[2] = info.sub_id;
+ return out;
+}
+
+std::pair<uint16_t, EncodedImage>
+SingleProcessEncodedImageIdInjector::ExtractId(const EncodedImage& source,
+ int coding_entity_id) {
+ EncodedImage out = source;
+
+ size_t pos = 0;
+ absl::optional<uint16_t> id = absl::nullopt;
+ while (pos < source.size()) {
+ // Extract frame id from first 2 bytes of the payload.
+ uint16_t next_id = source.data()[pos] + (source.data()[pos + 1] << 8);
+ // Extract frame sub id from second 2 byte of the payload.
+ uint16_t sub_id = source.data()[pos + 2];
+
+ RTC_CHECK(!id || id.value() == next_id)
+ << "Different frames encoded into single encoded image: " << id.value()
+ << " vs " << next_id;
+ id = next_id;
+
+ ExtractionInfo info;
+ {
+ rtc::CritScope crit(&lock_);
+ auto ext_vector_it = extraction_cache_.find(next_id);
+ RTC_CHECK(ext_vector_it != extraction_cache_.end())
+ << "Unknown frame id " << next_id;
+
+ auto info_it = ext_vector_it->second.infos.find(sub_id);
+ RTC_CHECK(info_it != ext_vector_it->second.infos.end())
+ << "Unknown sub id " << sub_id << " for frame " << next_id;
+ info = std::move(info_it->second);
+ ext_vector_it->second.infos.erase(info_it);
+ }
+
+ memcpy(&out.data()[pos], info.origin_data, kUsedBufferSize);
+ pos += info.length;
+ }
+ out.set_size(pos);
+
+ return std::pair<uint16_t, EncodedImage>(id.value(), out);
+}
+
+SingleProcessEncodedImageIdInjector::ExtractionInfoVector::
+ ExtractionInfoVector() = default;
+SingleProcessEncodedImageIdInjector::ExtractionInfoVector::
+ ~ExtractionInfoVector() = default;
+
+} // namespace test
+} // namespace webrtc
diff --git a/test/pc/e2e/analyzer/video/single_process_encoded_image_id_injector.h b/test/pc/e2e/analyzer/video/single_process_encoded_image_id_injector.h
new file mode 100644
index 0000000..3570f03
--- /dev/null
+++ b/test/pc/e2e/analyzer/video/single_process_encoded_image_id_injector.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2019 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 TEST_PC_E2E_ANALYZER_VIDEO_SINGLE_PROCESS_ENCODED_IMAGE_ID_INJECTOR_H_
+#define TEST_PC_E2E_ANALYZER_VIDEO_SINGLE_PROCESS_ENCODED_IMAGE_ID_INJECTOR_H_
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "api/video/encoded_image.h"
+#include "rtc_base/critical_section.h"
+#include "test/pc/e2e/analyzer/video/encoded_image_id_injector.h"
+
+namespace webrtc {
+namespace test {
+
+// Based on assumption that all call participants are in the same OS process
+// and uses same QualityAnalyzingVideoContext to obtain EncodedImageIdInjector.
+//
+// To inject frame id into EncodedImage injector uses first 2 bytes of
+// EncodedImage payload. Then it uses 3rd byte for frame sub id, that is
+// required to distinguish different spatial layers. The origin data from these
+// 3 bytes will be stored inside injector's internal storage and then will be
+// restored during extraction phase.
+//
+// This injector won't add any extra overhead into EncodedImage payload and
+// support frames with any size of payload. Also assumes that every EncodedImage
+// payload size is greater or equals to 3 bytes
+class SingleProcessEncodedImageIdInjector : public EncodedImageIdInjector,
+ public EncodedImageIdExtractor {
+ public:
+ SingleProcessEncodedImageIdInjector();
+ ~SingleProcessEncodedImageIdInjector() override;
+
+ // Id will be injected into EncodedImage buffer directly. This buffer won't
+ // be fully copied, so |source| image buffer will be also changed.
+ EncodedImage InjectId(uint16_t id,
+ const EncodedImage& source,
+ int coding_entity_id) override;
+ std::pair<uint16_t, EncodedImage> ExtractId(const EncodedImage& source,
+ int coding_entity_id) override;
+
+ private:
+ // Contains data required to extract frame id from EncodedImage and restore
+ // original buffer.
+ struct ExtractionInfo {
+ // Frame sub id to distinguish encoded images for different spatial layers.
+ uint8_t sub_id = 0;
+ // Length of the origin buffer encoded image.
+ size_t length;
+ // Data from first 3 bytes of origin encoded image's payload.
+ uint8_t origin_data[3];
+ };
+
+ struct ExtractionInfoVector {
+ ExtractionInfoVector();
+ ~ExtractionInfoVector();
+
+ // Next sub id, that have to be used for this frame id.
+ uint8_t next_sub_id = 0;
+ std::map<uint8_t, ExtractionInfo> infos;
+ };
+
+ rtc::CriticalSection lock_;
+ // Stores a mapping from frame id to extraction info for spatial layers
+ // for this frame id. There can be a lot of them, because if frame was
+ // dropped we can't clean it up, because we won't receive a signal on
+ // decoder side about that frame. In such case it will be replaced
+ // when sub id will overlap.
+ std::map<uint16_t, ExtractionInfoVector> extraction_cache_
+ RTC_GUARDED_BY(lock_);
+};
+
+} // namespace test
+} // namespace webrtc
+
+#endif // TEST_PC_E2E_ANALYZER_VIDEO_SINGLE_PROCESS_ENCODED_IMAGE_ID_INJECTOR_H_
diff --git a/test/pc/e2e/analyzer/video/single_process_encoded_image_id_injector_unittest.cc b/test/pc/e2e/analyzer/video/single_process_encoded_image_id_injector_unittest.cc
new file mode 100644
index 0000000..930a9d9
--- /dev/null
+++ b/test/pc/e2e/analyzer/video/single_process_encoded_image_id_injector_unittest.cc
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2019 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 "test/pc/e2e/analyzer/video/single_process_encoded_image_id_injector.h"
+
+#include <utility>
+
+#include "api/video/encoded_image.h"
+#include "rtc_base/buffer.h"
+#include "test/gtest.h"
+
+namespace webrtc {
+namespace test {
+namespace {
+
+rtc::Buffer CreateBufferOfSizeNFilledWithValuesFromX(size_t n, uint8_t x) {
+ rtc::Buffer buffer(n);
+ for (size_t i = 0; i < n; ++i) {
+ buffer[i] = static_cast<uint8_t>(x + i);
+ }
+ return buffer;
+}
+
+} // namespace
+
+TEST(SingleProcessEncodedImageIdInjector, InjectExtract) {
+ SingleProcessEncodedImageIdInjector injector;
+
+ rtc::Buffer buffer = CreateBufferOfSizeNFilledWithValuesFromX(10, 1);
+
+ EncodedImage source(buffer.data(), 10, 10);
+ source.SetTimestamp(123456789);
+
+ std::pair<uint16_t, EncodedImage> out =
+ injector.ExtractId(injector.InjectId(512, source, 1), 2);
+ ASSERT_EQ(out.first, 512);
+ ASSERT_EQ(out.second.size(), 10ul);
+ ASSERT_EQ(out.second.capacity(), 10ul);
+ for (int i = 0; i < 10; ++i) {
+ ASSERT_EQ(out.second.data()[i], i + 1);
+ }
+}
+
+TEST(SingleProcessEncodedImageIdInjector, Inject3Extract3) {
+ SingleProcessEncodedImageIdInjector injector;
+
+ rtc::Buffer buffer1 = CreateBufferOfSizeNFilledWithValuesFromX(10, 1);
+ rtc::Buffer buffer2 = CreateBufferOfSizeNFilledWithValuesFromX(10, 11);
+ rtc::Buffer buffer3 = CreateBufferOfSizeNFilledWithValuesFromX(10, 21);
+
+ // 1st frame
+ EncodedImage source1(buffer1.data(), 10, 10);
+ source1.SetTimestamp(123456710);
+ // 2nd frame 1st spatial layer
+ EncodedImage source2(buffer2.data(), 10, 10);
+ source2.SetTimestamp(123456720);
+ // 2nd frame 2nd spatial layer
+ EncodedImage source3(buffer3.data(), 10, 10);
+ source3.SetTimestamp(123456720);
+
+ EncodedImage intermediate1 = injector.InjectId(510, source1, 1);
+ EncodedImage intermediate2 = injector.InjectId(520, source2, 1);
+ EncodedImage intermediate3 = injector.InjectId(520, source3, 1);
+
+ // Extract ids in different order.
+ std::pair<uint16_t, EncodedImage> out3 = injector.ExtractId(intermediate3, 2);
+ std::pair<uint16_t, EncodedImage> out1 = injector.ExtractId(intermediate1, 2);
+ std::pair<uint16_t, EncodedImage> out2 = injector.ExtractId(intermediate2, 2);
+
+ ASSERT_EQ(out1.first, 510);
+ ASSERT_EQ(out1.second.size(), 10ul);
+ ASSERT_EQ(out1.second.capacity(), 10ul);
+ for (int i = 0; i < 10; ++i) {
+ ASSERT_EQ(out1.second.data()[i], i + 1);
+ }
+ ASSERT_EQ(out2.first, 520);
+ ASSERT_EQ(out2.second.size(), 10ul);
+ ASSERT_EQ(out2.second.capacity(), 10ul);
+ for (int i = 0; i < 10; ++i) {
+ ASSERT_EQ(out2.second.data()[i], i + 11);
+ }
+ ASSERT_EQ(out3.first, 520);
+ ASSERT_EQ(out3.second.size(), 10ul);
+ ASSERT_EQ(out3.second.capacity(), 10ul);
+ for (int i = 0; i < 10; ++i) {
+ ASSERT_EQ(out3.second.data()[i], i + 21);
+ }
+}
+
+TEST(SingleProcessEncodedImageIdInjector, InjectExtractFromConcatenated) {
+ SingleProcessEncodedImageIdInjector injector;
+
+ rtc::Buffer buffer1 = CreateBufferOfSizeNFilledWithValuesFromX(10, 1);
+ rtc::Buffer buffer2 = CreateBufferOfSizeNFilledWithValuesFromX(10, 11);
+ rtc::Buffer buffer3 = CreateBufferOfSizeNFilledWithValuesFromX(10, 21);
+
+ EncodedImage source1(buffer1.data(), 10, 10);
+ source1.SetTimestamp(123456710);
+ EncodedImage source2(buffer2.data(), 10, 10);
+ source2.SetTimestamp(123456710);
+ EncodedImage source3(buffer3.data(), 10, 10);
+ source3.SetTimestamp(123456710);
+
+ // Inject id into 3 images with same frame id.
+ EncodedImage intermediate1 = injector.InjectId(512, source1, 1);
+ EncodedImage intermediate2 = injector.InjectId(512, source2, 1);
+ EncodedImage intermediate3 = injector.InjectId(512, source3, 1);
+
+ // Concatenate them into single encoded image, like it can be done in jitter
+ // buffer.
+ size_t concatenated_length =
+ intermediate1.size() + intermediate2.size() + intermediate3.size();
+ rtc::Buffer concatenated_buffer;
+ concatenated_buffer.AppendData(intermediate1.data(), intermediate1.size());
+ concatenated_buffer.AppendData(intermediate2.data(), intermediate2.size());
+ concatenated_buffer.AppendData(intermediate3.data(), intermediate3.size());
+ EncodedImage concatenated(concatenated_buffer.data(), concatenated_length,
+ concatenated_length);
+
+ // Extract frame id from concatenated image
+ std::pair<uint16_t, EncodedImage> out = injector.ExtractId(concatenated, 2);
+
+ ASSERT_EQ(out.first, 512);
+ ASSERT_EQ(out.second.size(), 3 * 10ul);
+ ASSERT_EQ(out.second.capacity(), 3 * 10ul);
+ for (int i = 0; i < 10; ++i) {
+ ASSERT_EQ(out.second.data()[i], i + 1);
+ ASSERT_EQ(out.second.data()[i + 10], i + 11);
+ ASSERT_EQ(out.second.data()[i + 20], i + 21);
+ }
+}
+
+} // namespace test
+} // namespace webrtc