blob: b8f983874e86dea0f602a2f46b19ce29ea3f9f80 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/media/capture/desktop_capturer_android.h"
#include <memory>
#include <utility>
#include <vector>
#include "base/android/jni_android.h"
#include "base/android/jni_callback.h"
#include "base/android/jni_utils.h"
#include "base/android/scoped_java_ref.h"
#include "base/check.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capture_options.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capturer.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h"
#include "ui/gfx/geometry/rect.h"
using testing::Return;
namespace {
constexpr int kWidth = 8;
constexpr int kHeight = 4;
constexpr int kBytesPerPixel = 4;
constexpr int kStride = kWidth * kBytesPerPixel;
constexpr int kFrameSize = kStride * kHeight;
constexpr int kTimestampNs = 1000;
class RunnableFlag {
public:
RunnableFlag() = default;
void Run() { was_run_ = true; }
bool WasRun() const { return was_run_; }
base::android::ScopedJavaLocalRef<jobject> GetJavaObject() {
return base::android::ToJniCallback(
base::android::AttachCurrentThread(),
base::BindOnce(&RunnableFlag::Run, base::Unretained(this)));
}
private:
bool was_run_ = false;
};
base::android::ScopedJavaLocalRef<jobject> CreateJavaByteBuffer(
base::span<uint8_t> data) {
JNIEnv* env = base::android::AttachCurrentThread();
return jni_zero::ScopedJavaLocalRef<>::Adopt(
env, env->NewDirectByteBuffer(data.data(), data.size()));
}
} // namespace
namespace content {
class MockDesktopCapturerAndroidJni
: public DesktopCapturerAndroidJniInterface {
public:
~MockDesktopCapturerAndroidJni() override = default;
MOCK_METHOD(base::android::ScopedJavaLocalRef<jobject>,
Create,
(JNIEnv*, jlong),
(override));
MOCK_METHOD(jboolean,
StartCapture,
(JNIEnv*, const base::android::JavaRef<jobject>&),
(override));
MOCK_METHOD(void,
Destroy,
(JNIEnv*, const base::android::JavaRef<jobject>&),
(override));
};
class DesktopCapturerAndroidTest : public testing::Test,
public webrtc::DesktopCapturer::Callback {
public:
void SetUp() override {
env_ = base::android::AttachCurrentThread();
auto jni_interface = std::make_unique<MockDesktopCapturerAndroidJni>();
jni_interface_ = jni_interface.get();
capturer_ = std::make_unique<DesktopCapturerAndroid>(
webrtc::DesktopCaptureOptions(), std::move(jni_interface));
}
// webrtc::DesktopCapturer::Callback.
void OnCaptureResult(webrtc::DesktopCapturer::Result result,
std::unique_ptr<webrtc::DesktopFrame> frame) override {
last_result_ = result;
last_frame_ = std::move(frame);
}
void StartCapturer(bool start_success) {
base::android::ScopedJavaLocalRef<jclass> object_class =
base::android::GetClass(env_, "java/lang/Object");
jmethodID constructor =
env_->GetMethodID(object_class.obj(), "<init>", "()V");
ON_CALL(*jni_interface_, Create)
.WillByDefault(Return(jni_zero::ScopedJavaLocalRef<>::Adopt(
env_, env_->NewObject(object_class.obj(), constructor))));
ON_CALL(*jni_interface_, StartCapture).WillByDefault(Return(start_success));
EXPECT_CALL(*jni_interface_, Create).Times(1);
EXPECT_CALL(*jni_interface_, StartCapture).Times(1);
EXPECT_CALL(*jni_interface_, Destroy).Times(1);
capturer_->Start(this);
}
std::pair<webrtc::DesktopCapturer::Result,
std::unique_ptr<webrtc::DesktopFrame>>
CaptureFrame() {
CHECK(!last_result_.has_value());
CHECK_EQ(last_frame_, nullptr);
capturer_->CaptureFrame();
CHECK(last_result_.has_value());
return {std::exchange(last_result_, std::nullopt).value(),
std::move(last_frame_)};
}
protected:
raw_ptr<JNIEnv> env_ = nullptr;
std::unique_ptr<DesktopCapturerAndroid> capturer_;
void PushRgbaFrame(base::span<uint8_t> buffer, int64_t timestamp_ns) {
auto j_buf = CreateJavaByteBuffer(buffer);
RunnableFlag release_cb;
capturer_->OnRgbaFrameAvailable(env_, release_cb.GetJavaObject(),
timestamp_ns, j_buf, kBytesPerPixel,
kStride, 0, 0, kWidth, kHeight);
EXPECT_TRUE(release_cb.WasRun());
}
void PushRgbaFrame(base::span<uint8_t> buffer) {
PushRgbaFrame(buffer, kTimestampNs);
}
void PushRgbaFrame() { PushRgbaFrameWithTimestampNs(kTimestampNs); }
void PushRgbaFrameWithTimestampNs(int64_t timestamp_ns) {
std::vector<uint8_t> buffer(kFrameSize, 'A');
PushRgbaFrame(buffer, timestamp_ns);
}
void ExpectFrameWithCaptureTime(base::TimeDelta capture_time) {
const auto& [result, frame] = CaptureFrame();
EXPECT_EQ(result, webrtc::DesktopCapturer::Result::SUCCESS);
ASSERT_TRUE(frame);
EXPECT_EQ(frame->capture_time_ms(), capture_time.InMilliseconds());
}
private:
raw_ptr<MockDesktopCapturerAndroidJni> jni_interface_ = nullptr;
std::optional<webrtc::DesktopCapturer::Result> last_result_ = std::nullopt;
std::unique_ptr<webrtc::DesktopFrame> last_frame_;
};
TEST_F(DesktopCapturerAndroidTest, StartSuccessInitialState) {
StartCapturer(/*start_success=*/true);
// If Start succeeds, CaptureFrame should initially report TEMPORARY error
// until the first frame arrives from Java.
const auto& [result, frame] = CaptureFrame();
EXPECT_EQ(result, webrtc::DesktopCapturer::Result::ERROR_TEMPORARY);
EXPECT_EQ(frame, nullptr);
}
TEST_F(DesktopCapturerAndroidTest, StartFailurePermanentError) {
StartCapturer(/*start_success=*/false);
const auto& [result, frame] = CaptureFrame();
EXPECT_EQ(result, webrtc::DesktopCapturer::Result::ERROR_PERMANENT);
EXPECT_EQ(frame, nullptr);
}
TEST_F(DesktopCapturerAndroidTest, OnRgbaFrameAvailable) {
StartCapturer(/*start_success=*/true);
PushRgbaFrame();
const auto& [result, frame] = CaptureFrame();
EXPECT_EQ(result, webrtc::DesktopCapturer::Result::SUCCESS);
EXPECT_NE(frame, nullptr);
}
TEST_F(DesktopCapturerAndroidTest, OnStopBehavior) {
StartCapturer(/*start_success=*/true);
capturer_->OnStop(env_);
// Should return PERMANENT error because we are stopping.
const auto& [result0, frame0] = CaptureFrame();
EXPECT_EQ(result0, webrtc::DesktopCapturer::Result::ERROR_PERMANENT);
EXPECT_EQ(frame0, nullptr);
// Send a frame after OnStop.
PushRgbaFrame();
const auto& [result1, frame1] = CaptureFrame();
EXPECT_EQ(result1, webrtc::DesktopCapturer::Result::ERROR_PERMANENT);
EXPECT_EQ(frame1, nullptr);
}
TEST_F(DesktopCapturerAndroidTest, FrameArrivesThenOnStopReleasesFrame) {
StartCapturer(/*start_success=*/true);
// The release callback should be run even though the frame is never captured.
PushRgbaFrame();
// Stop the capturer before the frame is consumed.
capturer_->OnStop(env_);
// Subsequent captures should fail permanently.
const auto& [result, frame] = CaptureFrame();
EXPECT_EQ(result, webrtc::DesktopCapturer::Result::ERROR_PERMANENT);
EXPECT_EQ(frame, nullptr);
}
TEST_F(DesktopCapturerAndroidTest, OnStopCalledTwiceDeathTest) {
StartCapturer(/*start_success=*/true);
capturer_->OnStop(env_);
EXPECT_DEATH(capturer_->OnStop(env_), "");
}
TEST_F(DesktopCapturerAndroidTest, CaptureAndTemporaryError) {
StartCapturer(/*start_success=*/true);
const auto& [result0, frame0] = CaptureFrame();
EXPECT_EQ(result0, webrtc::DesktopCapturer::Result::ERROR_TEMPORARY);
// Simulate a frame from the OS.
std::vector<uint8_t> buffer(kFrameSize, 'A');
PushRgbaFrame(buffer);
// We should get the frame back.
const auto& [result1, frame1] = CaptureFrame();
EXPECT_EQ(result1, webrtc::DesktopCapturer::Result::SUCCESS);
ASSERT_TRUE(frame1);
EXPECT_EQ(frame1->size().width(), kWidth);
ASSERT_EQ(frame1->size().height(), kHeight);
ASSERT_EQ(frame1->stride(), kStride);
EXPECT_EQ(frame1->pixel_format(), webrtc::FOURCC_ABGR);
webrtc::DesktopRegion full_region(
webrtc::DesktopRect::MakeSize(frame1->size()));
EXPECT_TRUE(frame1->updated_region().Equals(full_region));
// SAFETY: No safe interface to DesktopFrame. Size must be equal to `buffer`
// if the stride and height are the same.
EXPECT_EQ(UNSAFE_BUFFERS(base::span(frame1->data(), buffer.size())),
base::span(buffer));
const auto& [result2, frame2] = CaptureFrame();
EXPECT_EQ(result2, webrtc::DesktopCapturer::Result::ERROR_TEMPORARY);
}
TEST_F(DesktopCapturerAndroidTest, MultipleFramesArriveBeforeCapture) {
StartCapturer(/*start_success=*/true);
std::vector<uint8_t> buffer_a(kFrameSize, 'A');
std::vector<uint8_t> buffer_b(kFrameSize, 'B');
PushRgbaFrame(buffer_a, kTimestampNs);
PushRgbaFrame(buffer_b, kTimestampNs + 1000);
// We should get the more recent frame back.
const auto& [result, frame] = CaptureFrame();
EXPECT_EQ(result, webrtc::DesktopCapturer::Result::SUCCESS);
ASSERT_TRUE(frame);
EXPECT_EQ(frame->size().width(), kWidth);
ASSERT_EQ(frame->size().height(), kHeight);
ASSERT_EQ(frame->stride(), kStride);
// Verify the data is from the second frame.
// SAFETY: No safe interface to DesktopFrame. Size must be equal to
// `buffer_b` if the stride and height are the same.
EXPECT_EQ(UNSAFE_BUFFERS(base::span(frame->data(), buffer_b.size())),
base::span(buffer_b));
}
TEST_F(DesktopCapturerAndroidTest, TimestampCalculation) {
StartCapturer(/*start_success=*/true);
// The capture time should be how long it took to capture the frame. Android
// does not provide this value, so we estimate it as the time since the last
// frame was produced. Initially, we have it be zero to represent an unknown
// amount.
PushRgbaFrameWithTimestampNs(100 * base::Time::kNanosecondsPerMillisecond);
ExpectFrameWithCaptureTime(base::Milliseconds(0));
// Frame 2: 33ms later.
PushRgbaFrameWithTimestampNs(133 * base::Time::kNanosecondsPerMillisecond);
ExpectFrameWithCaptureTime(base::Milliseconds(33));
// Frame 3: Timestamp goes backwards (non-monotonic). We have it be zero in
// this case, since it's unknown.
PushRgbaFrameWithTimestampNs(130 * base::Time::kNanosecondsPerMillisecond);
ExpectFrameWithCaptureTime(base::Milliseconds(0));
// Frame 4: Timestamp is the same. Time delta should be 0.
PushRgbaFrameWithTimestampNs(130 * base::Time::kNanosecondsPerMillisecond);
ExpectFrameWithCaptureTime(base::Milliseconds(0));
// Frame 5: Timestamp goes forward again. Difference should be based on the
// last timestamp, even if it was a non-monotonic update.
PushRgbaFrameWithTimestampNs(160 * base::Time::kNanosecondsPerMillisecond);
ExpectFrameWithCaptureTime(base::Milliseconds(30));
}
TEST_F(DesktopCapturerAndroidTest, RgbaFrameWithStrideAndCrop) {
StartCapturer(/*start_success=*/true);
// Setup a buffer larger than the actual image and crop a region.
constexpr int kBufferWidth = 32;
constexpr int kBufferHeight = 32;
// Add extra padding to the stride.
constexpr int kStride = kBufferWidth * kBytesPerPixel + 16;
constexpr gfx::Rect kCropRect(4, 8, 16, 12);
std::vector<uint8_t> buffer(kStride * kBufferHeight, 'A');
auto buffer_span = base::span(buffer);
for (int y = 0; y < kBufferHeight; ++y) {
for (int x = 0; x < kBufferWidth; ++x) {
auto pixel_span = buffer_span.subspan(
base::checked_cast<size_t>(y * kStride + x * kBytesPerPixel), 4u);
// Store coordinates in the first two bytes to verify correct copying.
pixel_span[0] = static_cast<uint8_t>(x);
pixel_span[1] = static_cast<uint8_t>(y);
}
}
auto j_buf = CreateJavaByteBuffer(buffer);
RunnableFlag release_cb;
capturer_->OnRgbaFrameAvailable(env_, release_cb.GetJavaObject(),
kTimestampNs, j_buf, kBytesPerPixel, kStride,
kCropRect.x(), kCropRect.y(),
kCropRect.right(), kCropRect.bottom());
EXPECT_TRUE(release_cb.WasRun());
const auto& [result, frame] = CaptureFrame();
EXPECT_EQ(result, webrtc::DesktopCapturer::Result::SUCCESS);
ASSERT_TRUE(frame);
EXPECT_EQ(frame->size().width(), kCropRect.width());
ASSERT_EQ(frame->size().height(), kCropRect.height());
// The frame should be tightly packed.
ASSERT_EQ(frame->stride(), kCropRect.width() * kBytesPerPixel);
// SAFETY: No safe interface to DesktopFrame.
auto frame_span = UNSAFE_BUFFERS(base::span(
frame->data(),
base::checked_cast<size_t>(frame->stride() * frame->size().height())));
for (int y = 0; y < kCropRect.height(); ++y) {
for (int x = 0; x < kCropRect.width(); ++x) {
const auto pixel_span = frame_span.subspan(
base::checked_cast<size_t>(y * frame->stride() + x * kBytesPerPixel),
2u);
const int src_x = x + kCropRect.x();
const int src_y = y + kCropRect.y();
EXPECT_EQ(pixel_span[0], static_cast<uint8_t>(src_x));
EXPECT_EQ(pixel_span[1], static_cast<uint8_t>(src_y));
}
}
}
TEST_F(DesktopCapturerAndroidTest, RgbaFrameInvalidBufferSizeDeathTest) {
StartCapturer(/*start_success=*/true);
// Buffer is one byte too small.
std::vector<uint8_t> buffer(kFrameSize - 1);
auto j_buf = CreateJavaByteBuffer(buffer);
RunnableFlag release_cb;
// Should CHECK.
EXPECT_DEATH(capturer_->OnRgbaFrameAvailable(
env_, release_cb.GetJavaObject(), kTimestampNs, j_buf,
kBytesPerPixel, kStride, 0, 0, kWidth, kHeight),
"");
EXPECT_FALSE(release_cb.WasRun());
}
TEST_F(DesktopCapturerAndroidTest, RgbaFrameInvalidStrideDeathTest) {
StartCapturer(/*start_success=*/true);
std::vector<uint8_t> buffer(kFrameSize);
auto j_buf = CreateJavaByteBuffer(buffer);
RunnableFlag release_cb;
// Stride is one byte too small.
const int kStride = kWidth * kBytesPerPixel - 1;
EXPECT_DEATH(capturer_->OnRgbaFrameAvailable(
env_, release_cb.GetJavaObject(), kTimestampNs, j_buf,
kBytesPerPixel, kStride, 0, 0, kWidth, kHeight),
"");
EXPECT_FALSE(release_cb.WasRun());
}
TEST_F(DesktopCapturerAndroidTest, RgbaFrameInvalidCropRectDeathTest) {
StartCapturer(/*start_success=*/true);
std::vector<uint8_t> buffer(kFrameSize);
auto j_buf = CreateJavaByteBuffer(buffer);
RunnableFlag release_cb;
EXPECT_DEATH(capturer_->OnRgbaFrameAvailable(
env_, release_cb.GetJavaObject(), kTimestampNs, j_buf,
kBytesPerPixel, kStride, 1, 0, 0, kHeight),
"");
EXPECT_FALSE(release_cb.WasRun());
}
TEST_F(DesktopCapturerAndroidTest, RgbaFramePreciseBufferBoundaryWithPadding) {
StartCapturer(/*start_success=*/true);
constexpr int kBufferWidth = 32;
constexpr gfx::Rect kCropRect(2, 3, 16, 12);
constexpr int kStride = kBufferWidth + 100;
constexpr int kOffset =
kCropRect.y() * kStride + kCropRect.x() * kBytesPerPixel;
constexpr size_t kBufferSize =
kStride * (kCropRect.bottom() - 1) + kCropRect.right() * kBytesPerPixel;
std::vector<uint8_t> buffer(kBufferSize, 'A');
auto buffer_span = base::span(buffer);
for (int y = 0; y < kCropRect.height(); ++y) {
for (int x = 0; x < kCropRect.width(); ++x) {
auto pixel_span =
buffer_span.subspan(base::checked_cast<size_t>(kOffset + y * kStride +
x * kBytesPerPixel),
2u);
pixel_span[0] = static_cast<uint8_t>(x + kCropRect.x());
pixel_span[1] = static_cast<uint8_t>(y + kCropRect.y());
}
}
auto j_buf = CreateJavaByteBuffer(buffer);
RunnableFlag release_cb;
capturer_->OnRgbaFrameAvailable(env_, release_cb.GetJavaObject(),
kTimestampNs, j_buf, kBytesPerPixel, kStride,
kCropRect.x(), kCropRect.y(),
kCropRect.right(), kCropRect.bottom());
EXPECT_TRUE(release_cb.WasRun());
auto [result, frame] = CaptureFrame();
EXPECT_EQ(result, webrtc::DesktopCapturer::Result::SUCCESS);
ASSERT_TRUE(frame);
EXPECT_EQ(frame->size().width(), kCropRect.width());
ASSERT_EQ(frame->size().height(), kCropRect.height());
ASSERT_EQ(frame->stride(), kCropRect.width() * kBytesPerPixel);
// SAFETY: No safe interface to DesktopFrame.
auto frame_span = UNSAFE_BUFFERS(base::span(
frame->data(),
base::checked_cast<size_t>(frame->stride() * frame->size().height())));
for (int y = 0; y < kCropRect.height(); ++y) {
for (int x = 0; x < kCropRect.width(); ++x) {
const auto pixel_span = frame_span.subspan(
base::checked_cast<size_t>(y * frame->stride() + x * kBytesPerPixel),
2u);
const int src_x = x + kCropRect.x();
const int src_y = y + kCropRect.y();
EXPECT_EQ(pixel_span[0], static_cast<uint8_t>(src_x));
EXPECT_EQ(pixel_span[1], static_cast<uint8_t>(src_y));
}
}
}
TEST_F(DesktopCapturerAndroidTest,
RgbaFramePreciseBufferBoundaryTooSmallDeathTest) {
StartCapturer(/*start_success=*/true);
constexpr int kBufferWidth = 32;
constexpr gfx::Rect kCropRect(2, 3, 16, 12);
constexpr int kStride = kBufferWidth + 100;
constexpr size_t kBufferSize =
kStride * (kCropRect.bottom() - 1) + kCropRect.right() * kBytesPerPixel;
std::vector<uint8_t> buffer(kBufferSize - 1, 'A');
auto j_buf = CreateJavaByteBuffer(buffer);
RunnableFlag release_cb;
// This should CHECK because the buffer was not large enough.
EXPECT_DEATH(capturer_->OnRgbaFrameAvailable(
env_, release_cb.GetJavaObject(), kTimestampNs, j_buf,
kBytesPerPixel, kStride, kCropRect.x(), kCropRect.y(),
kCropRect.right(), kCropRect.bottom()),
"");
EXPECT_FALSE(release_cb.WasRun());
}
} // namespace content