| /* |
| * Copyright 2020 The Chromium OS Authors. All rights reserved. |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include <cstdlib> |
| #include <set> |
| #include <vector> |
| |
| #include <base/check_op.h> |
| #include <base/command_line.h> |
| #include <base/files/file_util.h> |
| #include <base/process/launch.h> |
| #include <base/posix/safe_strerror.h> |
| #include <base/stl_util.h> |
| #include <base/strings/string_piece.h> |
| #include <base/strings/string_split.h> |
| #include <base/strings/string_util.h> |
| #include <base/strings/stringprintf.h> |
| #include <base/synchronization/waitable_event.h> |
| #include <base/threading/thread.h> |
| #include <brillo/syslog_logging.h> |
| #include <gtest/gtest.h> |
| #include <libyuv.h> |
| #include <linux/videodev2.h> |
| |
| #include "common/libcamera_connector_test/i420_buffer.h" |
| #include "common/libcamera_connector_test/util.h" |
| #include "cros-camera/camera_service_connector.h" |
| #include "cros-camera/common.h" |
| #include "cros-camera/future.h" |
| #include "cros-camera/ipc_util.h" |
| |
| namespace cros { |
| namespace tests { |
| |
| namespace { |
| |
| constexpr auto kDefaultTimeout = base::TimeDelta::FromSeconds(5); |
| |
| // TODO(b/151047930): Test hotplugging with vivid. |
| bool IsVividLoaded() { |
| std::string output; |
| if (!base::GetAppOutput({"lsmod"}, &output)) { |
| return false; |
| } |
| |
| std::vector<base::StringPiece> lines = base::SplitStringPieceUsingSubstr( |
| output, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| |
| return std::any_of(lines.begin(), lines.end(), [](const auto& line) { |
| return base::StartsWith(line, "vivid", base::CompareCase::SENSITIVE); |
| }); |
| } |
| |
| const std::array<cros_cam_format_info_t, 2> GetTestFormats() { |
| // TODO(b/151047930): Wait for vivid to be loaded in Tast test. |
| static bool vivid_loaded = IsVividLoaded(); |
| if (vivid_loaded) { |
| // vivid is an upstream Linux kernel module we use for testing and only |
| // supports 640x480 up to 25fps. |
| return {{{V4L2_PIX_FMT_NV12, 640, 480, 25}, |
| {V4L2_PIX_FMT_MJPEG, 640, 480, 25}}}; |
| } else { |
| // All camera modules on Chrome OS are required to support this. |
| return {{{V4L2_PIX_FMT_NV12, 640, 480, 30}, |
| {V4L2_PIX_FMT_MJPEG, 640, 480, 30}}}; |
| } |
| } |
| |
| cros_cam_format_info_t GetTestFormat() { |
| auto test_formats = GetTestFormats(); |
| EXPECT_GE(test_formats.size(), 1u); |
| return test_formats[0]; |
| } |
| |
| } // namespace |
| |
| class ConnectorEnvironment : public ::testing::Environment { |
| public: |
| void SetUp() override { |
| static constexpr char kTestClientTokenPath[] = |
| "/run/camera_tokens/testing/token"; |
| |
| base::FilePath token_path(kTestClientTokenPath); |
| std::string token_string; |
| ASSERT_TRUE(base::ReadFileToString(token_path, &token_string)) |
| << "Failed to read token from " << kTestClientTokenPath; |
| const cros_cam_init_option_t option = { |
| .api_version = 1, |
| .token = token_string.c_str(), |
| }; |
| ASSERT_EQ(cros_cam_init(&option), 0); |
| LOGF(INFO) << "Camera connector initialized"; |
| } |
| |
| void TearDown() override { |
| EXPECT_EQ(cros_cam_exit(), 0); |
| LOGF(INFO) << "Camera connector exited"; |
| } |
| }; |
| |
| class FrameCapturer { |
| public: |
| FrameCapturer() : thread_("FrameCapturer") { thread_.Start(); } |
| ~FrameCapturer() { thread_.Stop(); } |
| |
| FrameCapturer& SetNumFrames(int num_frames) { |
| num_frames_ = num_frames; |
| return *this; |
| } |
| |
| FrameCapturer& SetDuration(base::TimeDelta duration) { |
| duration_ = duration; |
| return *this; |
| } |
| |
| // Run starts a capture session with the given |id| and |format|. Returns the |
| // number of frames captured or the error status if an error is encountered. |
| int Run(int id, cros_cam_format_info_t format) { |
| auto future = cros::Future<int>::Create(nullptr); |
| thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&FrameCapturer::RunAsync, base::Unretained(this), id, |
| std::move(format), cros::GetFutureCallback(future))); |
| return future->Get(); |
| } |
| |
| // Run starts a capture session with the given |id| and |format|. Fires |
| // |callback| with the number of frames captured or the error status if an |
| // error is encountered. |
| // TODO(b/168769598): Change |callback| to base::OnceCallback once |
| // cros::GetFutureCallback() returns base::OnceCallback. |
| void RunAsync(int id, |
| cros_cam_format_info_t format, |
| base::Callback<void(int)> callback) { |
| int ret = StartCapture(id, std::move(format)); |
| if (ret != 0) { |
| LOGF(ERROR) << "Failed to start capture"; |
| callback.Run(ret); |
| } |
| thread_.task_runner()->PostTask( |
| FROM_HERE, base::BindOnce(&FrameCapturer::WaitForCaptureResult, |
| base::Unretained(this), std::move(callback))); |
| } |
| |
| I420Buffer LastI420Frame() const { return last_i420_frame_; } |
| |
| private: |
| int StartCapture(int id, cros_cam_format_info_t format) { |
| num_frames_captured_ = 0; |
| last_status_ = 0; |
| capture_done_.Reset(); |
| |
| id_ = id; |
| format_ = format; |
| const cros_cam_capture_request_t request = { |
| .id = id, |
| .format = &format, |
| }; |
| return cros_cam_start_capture(&request, &FrameCapturer::CaptureCallback, |
| this); |
| } |
| |
| void WaitForCaptureResult(base::Callback<void(int)> callback) { |
| // Wait until |duration_| passed or |num_frames_| captured. Fires |callback| |
| // with the number of frames captured or the error status if an error is |
| // encountered. |
| if (!capture_done_.TimedWait(duration_)) { |
| cros_cam_stop_capture(id_); |
| capture_done_.Signal(); |
| } |
| LOGF(INFO) << "Last status = " << last_status_; |
| LOGF(INFO) << "Captured " << num_frames_captured_ << " frames"; |
| callback.Run(last_status_ != 0 ? last_status_ : num_frames_captured_); |
| } |
| |
| // Non-zero return value should stop the capture. |
| int GotCaptureResult(const cros_cam_capture_result_t* result) { |
| if (capture_done_.IsSignaled()) { |
| ADD_FAILURE() << "got capture result after capture is done"; |
| return -1; |
| } |
| |
| if (result->status != 0) { |
| LOGF(WARNING) << "Capture result error: " |
| << base::safe_strerror(-result->status); |
| last_status_ = result->status; |
| capture_done_.Signal(); |
| return -1; |
| } |
| |
| const cros_cam_frame_t* frame = result->frame; |
| EXPECT_TRUE(IsSameFormat(frame->format, format_)); |
| last_i420_frame_ = I420Buffer::Create(frame); |
| |
| num_frames_captured_++; |
| if (num_frames_captured_ == num_frames_) { |
| capture_done_.Signal(); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| static int CaptureCallback(void* context, |
| const cros_cam_capture_result_t* result) { |
| auto* self = reinterpret_cast<FrameCapturer*>(context); |
| return self->GotCaptureResult(result); |
| } |
| |
| base::Thread thread_; |
| |
| int num_frames_ = INT_MAX; |
| base::TimeDelta duration_ = kDefaultTimeout; |
| int id_; |
| cros_cam_format_info_t format_; |
| |
| int num_frames_captured_; |
| base::WaitableEvent capture_done_; |
| I420Buffer last_i420_frame_; |
| int last_status_; |
| }; |
| |
| class CameraClient { |
| public: |
| void ProbeCameraInfo() { |
| ASSERT_EQ(cros_cam_get_cam_info(&CameraClient::GetCamInfoCallback, this), |
| 0); |
| EXPECT_GT(GetNumberOfCameras(), 0) << "no camera found"; |
| // All connected cameras should be already reported by the callback |
| // function, set the frozen flag to capture unexpected hotplug events |
| // during test. Please see the comment of cros_cam_get_cam_info() for more |
| // details. |
| camera_info_frozen_ = true; |
| } |
| |
| void DumpCameraInfo() { |
| for (const auto& info : camera_infos_) { |
| LOGF(INFO) << "id: " << info.id; |
| LOGF(INFO) << "name: " << info.name; |
| LOGF(INFO) << "facing: " << FacingToString(info.facing); |
| LOGF(INFO) << "format_count: " << info.format_count; |
| for (int i = 0; i < info.format_count; i++) { |
| LOGF(INFO) << base::StringPrintf( |
| "Format %2d: %s", i, |
| CameraFormatInfoToString(info.format_info[i]).c_str()); |
| } |
| } |
| } |
| |
| size_t GetNumberOfCameras() { return camera_infos_.size(); } |
| |
| int FindIdForFormat(const cros_cam_format_info_t& format) { |
| for (const auto& info : camera_infos_) { |
| for (int i = 0; i < info.format_count; i++) { |
| if (IsSameFormat(format, info.format_info[i])) { |
| return info.id; |
| } |
| } |
| } |
| return -1; |
| } |
| |
| void RestartCrosCamera() { |
| prev_num_cameras_ = GetNumberOfCameras(); |
| camera_info_frozen_ = false; |
| // We don't clear |camera_infos_| here expecting that libcamera_connector |
| // would notify us that the cameras are down. |
| init_done_event_.Reset(); |
| LOGF(INFO) << "Restarting cros-camera"; |
| ASSERT_EQ(system("stop cros-camera"), 0) << "Failed to stop camera"; |
| ASSERT_EQ(system("start cros-camera"), 0) << "Failed to start camera"; |
| ASSERT_TRUE(init_done_event_.TimedWait(kDefaultTimeout)) |
| << "Failed to get all camera info after timeout"; |
| } |
| |
| private: |
| int GotCameraInfo(const cros_cam_info_t* info, int is_removed) { |
| EXPECT_FALSE(camera_info_frozen_) << "unexpected hotplug events"; |
| if (is_removed) { |
| LOGF(INFO) << "Camera " << info->id << " removed"; |
| // TODO(lnishan): Check return value of base::EraseIf after libchrome in |
| // Chrome OS includes crrev.com/c/2072038. |
| size_t old_size = camera_infos_.size(); |
| base::EraseIf(camera_infos_, [&](const auto& my_info) { |
| return my_info.id == info->id; |
| }); |
| CHECK_EQ(camera_infos_.size(), old_size - 1); |
| } else { |
| LOGF(INFO) << "Got camera info for camera " << info->id; |
| EXPECT_GT(info->format_count, 0) << "no available formats"; |
| camera_infos_.push_back(*info); |
| if (GetNumberOfCameras() == prev_num_cameras_) { |
| init_done_event_.Signal(); |
| } |
| } |
| return 0; |
| } |
| |
| static int GetCamInfoCallback(void* context, |
| const cros_cam_info_t* info, |
| int is_removed) { |
| auto* self = reinterpret_cast<CameraClient*>(context); |
| return self->GotCameraInfo(info, is_removed); |
| } |
| std::vector<cros_cam_info_t> camera_infos_; |
| bool camera_info_frozen_ = false; |
| int prev_num_cameras_ = -1; |
| base::WaitableEvent init_done_event_; |
| }; |
| |
| class CaptureTest |
| : public ::testing::Test, |
| public ::testing::WithParamInterface<cros_cam_format_info_t> { |
| protected: |
| void SetUp() override { |
| client_.ProbeCameraInfo(); |
| format_ = GetParam(); |
| camera_id_ = client_.FindIdForFormat(format_); |
| ASSERT_NE(camera_id_, -1); |
| } |
| |
| CameraClient client_; |
| FrameCapturer capturer_; |
| |
| int camera_id_; |
| cros_cam_format_info_t format_; |
| }; |
| |
| TEST(ConnectorTest, GetInfo) { |
| CameraClient client1; |
| client1.ProbeCameraInfo(); |
| client1.DumpCameraInfo(); |
| |
| // Check that we can unsubscribe the info callback. |
| ASSERT_EQ(0, cros_cam_get_cam_info(nullptr, nullptr)); |
| |
| CameraClient client2; |
| client2.ProbeCameraInfo(); |
| ASSERT_EQ(client1.GetNumberOfCameras(), client2.GetNumberOfCameras()); |
| } |
| |
| TEST_P(CaptureTest, OneFrame) { |
| int num_frames_captured = capturer_.SetNumFrames(1).Run(camera_id_, format_); |
| EXPECT_EQ(num_frames_captured, 1); |
| } |
| |
| TEST_P(CaptureTest, ThreeSeconds) { |
| const auto kDuration = base::TimeDelta::FromSeconds(3); |
| int num_frames_captured = |
| capturer_.SetDuration(kDuration).Run(camera_id_, format_); |
| // It's expected to get more than 1 frame in 3s. |
| EXPECT_GT(num_frames_captured, 1); |
| } |
| |
| TEST(ConnectorTest, CompareFrames) { |
| CameraClient client; |
| client.ProbeCameraInfo(); |
| auto test_formats = GetTestFormats(); |
| CHECK_GE(test_formats.size(), 2u); |
| |
| int id = client.FindIdForFormat(test_formats[0]); |
| ASSERT_NE(id, -1); |
| |
| FrameCapturer capturer; |
| capturer.SetNumFrames(1); |
| |
| ASSERT_EQ(capturer.Run(id, test_formats[0]), 1); |
| I420Buffer frame1 = capturer.LastI420Frame(); |
| |
| ASSERT_EQ(capturer.Run(id, test_formats[1]), 1); |
| I420Buffer frame2 = capturer.LastI420Frame(); |
| |
| double ssim = libyuv::I420Ssim( |
| frame1.DataY(), frame1.StrideY(), frame1.DataU(), frame1.StrideU(), |
| frame1.DataV(), frame1.StrideV(), frame2.DataY(), frame2.StrideY(), |
| frame2.DataU(), frame2.StrideU(), frame2.DataV(), frame2.StrideV(), |
| frame1.Width(), frame1.Height()); |
| LOGF(INFO) << "ssim = " << ssim; |
| |
| // It's expected have two similar but not exactly same frames captured in the |
| // short period with MJPEG and NV12. The normal values are around 0.7~0.8. |
| EXPECT_GE(ssim, 0.3); |
| |
| // If the frames are exactly same (ssim = 1.0), the frame is likely broken |
| // such as all pixels are black. Set the threshold as 0.99 for potential jpeg |
| // artifacts and floating point error. |
| EXPECT_LE(ssim, 0.99); |
| } |
| |
| TEST(ConnectorTest, RestartCrosCameraIdle) { |
| CameraClient client; |
| FrameCapturer capturer; |
| auto format = GetTestFormat(); |
| |
| client.ProbeCameraInfo(); |
| client.RestartCrosCamera(); |
| |
| int id = client.FindIdForFormat(format); |
| ASSERT_NE(id, -1); |
| int num_frames_captured = capturer.SetNumFrames(1).Run(id, format); |
| EXPECT_EQ(num_frames_captured, 1); |
| } |
| |
| TEST(ConnectorTest, RestartCrosCameraActive) { |
| CameraClient client; |
| FrameCapturer capturer; |
| auto format = GetTestFormat(); |
| |
| client.ProbeCameraInfo(); |
| int id = client.FindIdForFormat(format); |
| ASSERT_NE(id, -1); |
| |
| auto future = cros::Future<int>::Create(nullptr); |
| capturer.SetDuration(kDefaultTimeout) |
| .RunAsync(id, format, cros::GetFutureCallback(future)); |
| client.RestartCrosCamera(); |
| ASSERT_EQ(future->Get(), -ENODEV); |
| int num_frames_captured = capturer.SetNumFrames(1).Run(id, format); |
| EXPECT_EQ(num_frames_captured, 1); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(ConnectorTest, |
| CaptureTest, |
| ::testing::ValuesIn(GetTestFormats()), |
| [](const auto& info) { |
| const cros_cam_format_info_t& fmt = info.param; |
| return base::StringPrintf( |
| "%s_%ux%u_%ufps", |
| FourccToString(fmt.fourcc).c_str(), fmt.width, |
| fmt.height, fmt.fps); |
| }); |
| |
| } // namespace tests |
| } // namespace cros |
| |
| int main(int argc, char** argv) { |
| base::CommandLine::Init(argc, argv); |
| brillo::InitLog(brillo::kLogToStderr); |
| logging::SetLogItems(/*enable_process_id=*/true, /*enable_thread_id=*/true, |
| /*enable_timestamp=*/true, /*enable_tickcount=*/false); |
| |
| ::testing::AddGlobalTestEnvironment(new cros::tests::ConnectorEnvironment()); |
| ::testing::InitGoogleTest(&argc, argv); |
| return RUN_ALL_TESTS(); |
| } |