summaryrefslogtreecommitdiffstats
path: root/dom/media/gtest/TestVideoFrameConverter.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/gtest/TestVideoFrameConverter.cpp')
-rw-r--r--dom/media/gtest/TestVideoFrameConverter.cpp302
1 files changed, 302 insertions, 0 deletions
diff --git a/dom/media/gtest/TestVideoFrameConverter.cpp b/dom/media/gtest/TestVideoFrameConverter.cpp
new file mode 100644
index 0000000000..04ce47c473
--- /dev/null
+++ b/dom/media/gtest/TestVideoFrameConverter.cpp
@@ -0,0 +1,302 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "VideoFrameConverter.h"
+#include "YUVBufferGenerator.h"
+
+using namespace mozilla;
+
+class VideoFrameConverterTest;
+
+class FrameListener : public VideoConverterListener {
+ public:
+ explicit FrameListener(VideoFrameConverterTest* aTest);
+ void OnVideoFrameConverted(const webrtc::VideoFrame& aVideoFrame) override;
+
+ private:
+ VideoFrameConverterTest* mTest;
+};
+
+class VideoFrameConverterTest : public ::testing::Test {
+ protected:
+ using FrameType = std::pair<webrtc::VideoFrame, TimeStamp>;
+ Monitor mMonitor;
+ RefPtr<VideoFrameConverter> mConverter;
+ RefPtr<FrameListener> mListener;
+ std::vector<FrameType> mConvertedFrames;
+
+ VideoFrameConverterTest()
+ : mMonitor("PacingFixture::mMonitor"),
+ mConverter(MakeAndAddRef<VideoFrameConverter>()),
+ mListener(MakeAndAddRef<FrameListener>(this)) {
+ mConverter->AddListener(mListener);
+ }
+
+ void TearDown() override { mConverter->Shutdown(); }
+
+ size_t NumConvertedFrames() {
+ MonitorAutoLock lock(mMonitor);
+ return mConvertedFrames.size();
+ }
+
+ std::vector<FrameType> WaitForNConverted(size_t aN) {
+ MonitorAutoLock l(mMonitor);
+ while (mConvertedFrames.size() < aN) {
+ l.Wait();
+ }
+ std::vector<FrameType> v(mConvertedFrames.begin(),
+ mConvertedFrames.begin() + aN);
+ return v;
+ }
+
+ public:
+ void OnVideoFrameConverted(const webrtc::VideoFrame& aVideoFrame) {
+ MonitorAutoLock lock(mMonitor);
+ EXPECT_NE(aVideoFrame.timestamp_us(), 0);
+ mConvertedFrames.push_back(std::make_pair(aVideoFrame, TimeStamp::Now()));
+ mMonitor.Notify();
+ }
+};
+
+FrameListener::FrameListener(VideoFrameConverterTest* aTest) : mTest(aTest) {}
+void FrameListener::OnVideoFrameConverted(
+ const webrtc::VideoFrame& aVideoFrame) {
+ mTest->OnVideoFrameConverted(aVideoFrame);
+}
+
+static bool IsPlane(const uint8_t* aData, int aWidth, int aHeight, int aStride,
+ uint8_t aValue) {
+ for (int i = 0; i < aHeight; ++i) {
+ for (int j = 0; j < aWidth; ++j) {
+ if (aData[i * aStride + j] != aValue) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+static bool IsFrameBlack(const webrtc::VideoFrame& aFrame) {
+ RefPtr<webrtc::I420BufferInterface> buffer =
+ aFrame.video_frame_buffer()->ToI420().get();
+ return IsPlane(buffer->DataY(), buffer->width(), buffer->height(),
+ buffer->StrideY(), 0x00) &&
+ IsPlane(buffer->DataU(), buffer->ChromaWidth(), buffer->ChromaHeight(),
+ buffer->StrideU(), 0x80) &&
+ IsPlane(buffer->DataV(), buffer->ChromaWidth(), buffer->ChromaHeight(),
+ buffer->StrideV(), 0x80);
+}
+
+VideoChunk GenerateChunk(int32_t aWidth, int32_t aHeight, TimeStamp aTime) {
+ YUVBufferGenerator generator;
+ generator.Init(gfx::IntSize(aWidth, aHeight));
+ VideoFrame f(generator.GenerateI420Image(), gfx::IntSize(aWidth, aHeight));
+ VideoChunk c;
+ c.mFrame.TakeFrom(&f);
+ c.mTimeStamp = aTime;
+ c.mDuration = 0;
+ return c;
+}
+
+static TimeDuration SameFrameTimeDuration() {
+ // On some platforms, particularly Windows, we have observed the same-frame
+ // timer firing early. To not unittest the timer itself we allow a tiny amount
+ // of fuzziness in when the timer is allowed to fire.
+ return TimeDuration::FromSeconds(1) - TimeDuration::FromMilliseconds(0.5);
+}
+
+TEST_F(VideoFrameConverterTest, BasicConversion) {
+ TimeStamp now = TimeStamp::Now();
+ VideoChunk chunk = GenerateChunk(640, 480, now);
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(chunk, false);
+ auto frames = WaitForNConverted(1);
+ ASSERT_EQ(frames.size(), 1U);
+ EXPECT_EQ(frames[0].first.width(), 640);
+ EXPECT_EQ(frames[0].first.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frames[0].first));
+ EXPECT_GT(frames[0].second - now, TimeDuration::FromMilliseconds(0));
+}
+
+TEST_F(VideoFrameConverterTest, BasicPacing) {
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp future = now + TimeDuration::FromMilliseconds(100);
+ VideoChunk chunk = GenerateChunk(640, 480, future);
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(chunk, false);
+ auto frames = WaitForNConverted(1);
+ EXPECT_GT(TimeStamp::Now(), future);
+ ASSERT_EQ(frames.size(), 1U);
+ EXPECT_EQ(frames[0].first.width(), 640);
+ EXPECT_EQ(frames[0].first.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frames[0].first));
+ EXPECT_GT(frames[0].second - now, future - now);
+}
+
+TEST_F(VideoFrameConverterTest, MultiPacing) {
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp future1 = now + TimeDuration::FromMilliseconds(100);
+ TimeStamp future2 = now + TimeDuration::FromMilliseconds(200);
+ VideoChunk chunk = GenerateChunk(640, 480, future1);
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(chunk, false);
+ chunk = GenerateChunk(640, 480, future2);
+ mConverter->QueueVideoChunk(chunk, false);
+ auto frames = WaitForNConverted(2);
+ EXPECT_GT(TimeStamp::Now(), future2);
+ ASSERT_EQ(frames.size(), 2U);
+ EXPECT_EQ(frames[0].first.width(), 640);
+ EXPECT_EQ(frames[0].first.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frames[0].first));
+ EXPECT_GT(frames[0].second - now, future1 - now);
+ EXPECT_EQ(frames[1].first.width(), 640);
+ EXPECT_EQ(frames[1].first.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frames[1].first));
+ EXPECT_GT(frames[1].second, future2);
+ EXPECT_GT(frames[1].second - now, frames[0].second - now);
+}
+
+TEST_F(VideoFrameConverterTest, Duplication) {
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp future1 = now + TimeDuration::FromMilliseconds(100);
+ VideoChunk chunk = GenerateChunk(640, 480, future1);
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(chunk, false);
+ auto frames = WaitForNConverted(2);
+ EXPECT_GT(TimeStamp::Now() - now,
+ SameFrameTimeDuration() + TimeDuration::FromMilliseconds(100));
+ ASSERT_EQ(frames.size(), 2U);
+ EXPECT_EQ(frames[0].first.width(), 640);
+ EXPECT_EQ(frames[0].first.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frames[0].first));
+ EXPECT_GT(frames[0].second, future1);
+ EXPECT_EQ(frames[1].first.width(), 640);
+ EXPECT_EQ(frames[1].first.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frames[1].first));
+ EXPECT_GT(frames[1].second - now,
+ SameFrameTimeDuration() + TimeDuration::FromMilliseconds(100));
+ // Check that the second frame comes between 1s and 2s after the first.
+ EXPECT_GT(TimeDuration::FromMicroseconds(frames[1].first.timestamp_us()) -
+ TimeDuration::FromMicroseconds(frames[0].first.timestamp_us()),
+ SameFrameTimeDuration());
+ EXPECT_LT(TimeDuration::FromMicroseconds(frames[1].first.timestamp_us()) -
+ TimeDuration::FromMicroseconds(frames[0].first.timestamp_us()),
+ TimeDuration::FromSeconds(2));
+}
+
+TEST_F(VideoFrameConverterTest, DropsOld) {
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp future1 = now + TimeDuration::FromMilliseconds(1000);
+ TimeStamp future2 = now + TimeDuration::FromMilliseconds(100);
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(GenerateChunk(800, 600, future1), false);
+ mConverter->QueueVideoChunk(GenerateChunk(640, 480, future2), false);
+ auto frames = WaitForNConverted(1);
+ EXPECT_GT(TimeStamp::Now(), future2);
+ ASSERT_EQ(frames.size(), 1U);
+ EXPECT_EQ(frames[0].first.width(), 640);
+ EXPECT_EQ(frames[0].first.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frames[0].first));
+ EXPECT_GT(frames[0].second - now, future2 - now);
+}
+
+// We check that the disabling code was triggered by sending multiple,
+// different, frames to the converter within one second. While black, it shall
+// treat all frames identical and issue one black frame per second.
+TEST_F(VideoFrameConverterTest, BlackOnDisable) {
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp future1 = now + TimeDuration::FromMilliseconds(100);
+ TimeStamp future2 = now + TimeDuration::FromMilliseconds(200);
+ TimeStamp future3 = now + TimeDuration::FromMilliseconds(400);
+ mConverter->SetActive(true);
+ mConverter->SetTrackEnabled(false);
+ mConverter->QueueVideoChunk(GenerateChunk(640, 480, future1), false);
+ mConverter->QueueVideoChunk(GenerateChunk(640, 480, future2), false);
+ mConverter->QueueVideoChunk(GenerateChunk(640, 480, future3), false);
+ auto frames = WaitForNConverted(2);
+ EXPECT_GT(TimeStamp::Now() - now, SameFrameTimeDuration());
+ ASSERT_EQ(frames.size(), 2U);
+ // The first frame was created instantly by SetTrackEnabled().
+ EXPECT_EQ(frames[0].first.width(), 640);
+ EXPECT_EQ(frames[0].first.height(), 480);
+ EXPECT_TRUE(IsFrameBlack(frames[0].first));
+ EXPECT_GT(frames[0].second - now, TimeDuration::FromSeconds(0));
+ // The second frame was created by the same-frame timer (after 1s).
+ EXPECT_EQ(frames[1].first.width(), 640);
+ EXPECT_EQ(frames[1].first.height(), 480);
+ EXPECT_TRUE(IsFrameBlack(frames[1].first));
+ EXPECT_GT(frames[1].second - now, SameFrameTimeDuration());
+ // Check that the second frame comes between 1s and 2s after the first.
+ EXPECT_NEAR(frames[1].first.timestamp_us(),
+ frames[0].first.timestamp_us() + ((PR_USEC_PER_SEC * 3) / 2),
+ PR_USEC_PER_SEC / 2);
+}
+
+TEST_F(VideoFrameConverterTest, ClearFutureFramesOnJumpingBack) {
+ TimeStamp start = TimeStamp::Now();
+ TimeStamp future1 = start + TimeDuration::FromMilliseconds(100);
+
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(GenerateChunk(640, 480, future1), false);
+ WaitForNConverted(1);
+
+ // We are now at t=100ms+. Queue a future frame and jump back in time to
+ // signal a reset.
+
+ TimeStamp step1 = TimeStamp::Now();
+ ASSERT_GT(step1 - start, future1 - start);
+ TimeStamp future2 = step1 + TimeDuration::FromMilliseconds(200);
+ TimeStamp future3 = step1 + TimeDuration::FromMilliseconds(100);
+ ASSERT_LT(future2 - start, future1 + SameFrameTimeDuration() - start);
+ mConverter->QueueVideoChunk(GenerateChunk(800, 600, future2), false);
+ VideoChunk nullChunk;
+ nullChunk.mFrame = VideoFrame(nullptr, gfx::IntSize(800, 600));
+ nullChunk.mTimeStamp = step1;
+ mConverter->QueueVideoChunk(nullChunk, false);
+
+ // We queue one more chunk after the reset so we don't have to wait a full
+ // second for the same-frame timer. It has a different time and resolution
+ // so we can differentiate them.
+ mConverter->QueueVideoChunk(GenerateChunk(320, 240, future3), false);
+
+ auto frames = WaitForNConverted(2);
+ TimeStamp step2 = TimeStamp::Now();
+ EXPECT_GT(step2 - start, future3 - start);
+ ASSERT_EQ(frames.size(), 2U);
+ EXPECT_EQ(frames[0].first.width(), 640);
+ EXPECT_EQ(frames[0].first.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frames[0].first));
+ EXPECT_GT(frames[0].second - start, future1 - start);
+ EXPECT_EQ(frames[1].first.width(), 320);
+ EXPECT_EQ(frames[1].first.height(), 240);
+ EXPECT_FALSE(IsFrameBlack(frames[1].first));
+ EXPECT_GT(frames[1].second - start, future3 - start);
+}
+
+// We check that the no frame is converted while inactive, and that on
+// activating the most recently queued frame gets converted.
+TEST_F(VideoFrameConverterTest, NoConversionsWhileInactive) {
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp future1 = now - TimeDuration::FromMilliseconds(1);
+ TimeStamp future2 = now;
+ mConverter->QueueVideoChunk(GenerateChunk(640, 480, future1), false);
+ mConverter->QueueVideoChunk(GenerateChunk(800, 600, future2), false);
+
+ // SetActive needs to follow the same async path as the frames to be in sync.
+ auto q =
+ MakeRefPtr<TaskQueue>(GetMediaThreadPool(MediaThreadType::WEBRTC_DECODER),
+ "VideoFrameConverterTest");
+ auto timer = MakeRefPtr<MediaTimer>(false);
+ timer->WaitFor(TimeDuration::FromMilliseconds(100), __func__)
+ ->Then(q, __func__,
+ [converter = mConverter] { converter->SetActive(true); });
+
+ auto frames = WaitForNConverted(1);
+ ASSERT_EQ(frames.size(), 1U);
+ EXPECT_EQ(frames[0].first.width(), 800);
+ EXPECT_EQ(frames[0].first.height(), 600);
+ EXPECT_FALSE(IsFrameBlack(frames[0].first));
+}