| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "media/fuchsia/camera/fake_fuchsia_camera.h" |
| |
| #include <fuchsia/sysmem/cpp/fidl.h> |
| #include <lib/sys/cpp/component_context.h> |
| |
| #include "base/fuchsia/fuchsia_logging.h" |
| #include "base/fuchsia/process_context.h" |
| #include "base/memory/platform_shared_memory_region.h" |
| #include "base/memory/writable_shared_memory_region.h" |
| #include "base/process/process_handle.h" |
| #include "base/task/current_thread.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace media { |
| |
| namespace { |
| |
| constexpr uint8_t kYPlaneSalt = 1; |
| constexpr uint8_t kUPlaneSalt = 2; |
| constexpr uint8_t kVPlaneSalt = 3; |
| |
| uint8_t GetTestFrameValue(gfx::Size size, int x, int y, uint8_t salt) { |
| return static_cast<uint8_t>(y + x * size.height() + salt); |
| } |
| |
| // Fills one plane of a test frame. |data| points at the location of the pixel |
| // (0, 0). |orientation| specifies frame orientation transformation that will be |
| // applied on the receiving end, so this function applies _reverse_ of the |
| // |orientation| transformation. |
| void FillPlane(uint8_t* data, |
| gfx::Size size, |
| int x_step, |
| int y_step, |
| fuchsia::camera3::Orientation orientation, |
| uint8_t salt) { |
| // First flip X axis for flipped orientation. |
| if (orientation == fuchsia::camera3::Orientation::UP_FLIPPED || |
| orientation == fuchsia::camera3::Orientation::DOWN_FLIPPED || |
| orientation == fuchsia::camera3::Orientation::RIGHT_FLIPPED || |
| orientation == fuchsia::camera3::Orientation::LEFT_FLIPPED) { |
| // Move the origin to the top right corner and flip the X axis. |
| data += (size.width() - 1) * x_step; |
| x_step = -x_step; |
| } |
| |
| switch (orientation) { |
| case fuchsia::camera3::Orientation::UP: |
| case fuchsia::camera3::Orientation::UP_FLIPPED: |
| break; |
| |
| case fuchsia::camera3::Orientation::DOWN: |
| case fuchsia::camera3::Orientation::DOWN_FLIPPED: |
| // Move |data| to point to the bottom right corner and reverse direction |
| // of both axes. |
| data += (size.width() - 1) * x_step + (size.height() - 1) * y_step; |
| x_step = -x_step; |
| y_step = -y_step; |
| break; |
| |
| case fuchsia::camera3::Orientation::LEFT: |
| case fuchsia::camera3::Orientation::LEFT_FLIPPED: |
| // Rotate 90 degrees clockwise by moving |data| to point to the right top |
| // corner, swapping the axes and reversing direction of the Y axis. |
| data += (size.width() - 1) * x_step; |
| size = gfx::Size(size.height(), size.width()); |
| std::swap(x_step, y_step); |
| y_step = -y_step; |
| break; |
| |
| case fuchsia::camera3::Orientation::RIGHT: |
| case fuchsia::camera3::Orientation::RIGHT_FLIPPED: |
| // Rotate 90 degrees counter-clockwise by moving |data| to point to the |
| // bottom left corner, swapping the axes and reversing direction of the X |
| // axis. |
| data += (size.height() - 1) * y_step; |
| size = gfx::Size(size.height(), size.width()); |
| std::swap(x_step, y_step); |
| x_step = -x_step; |
| break; |
| } |
| |
| for (int y = 0; y < size.height(); ++y) { |
| for (int x = 0; x < size.width(); ++x) { |
| data[x * x_step + y * y_step] = GetTestFrameValue(size, x, y, salt); |
| } |
| } |
| } |
| |
| void ValidatePlane(const uint8_t* data, |
| gfx::Size size, |
| size_t x_step, |
| size_t y_step, |
| uint8_t salt) { |
| for (int y = 0; y < size.height(); ++y) { |
| for (int x = 0; x < size.width(); ++x) { |
| SCOPED_TRACE(testing::Message() << "x=" << x << " y=" << y); |
| EXPECT_EQ(data[x * x_step + y * y_step], |
| GetTestFrameValue(size, x, y, salt)); |
| } |
| } |
| } |
| |
| } // namespace |
| |
| // static |
| const gfx::Size FakeCameraStream::kMaxFrameSize = gfx::Size(100, 60); |
| // static |
| const gfx::Size FakeCameraStream::kDefaultFrameSize = gfx::Size(60, 40); |
| |
| // static |
| void FakeCameraStream::ValidateFrameData(const uint8_t* data, |
| gfx::Size size, |
| uint8_t salt) { |
| const uint8_t* y_plane = data; |
| { |
| SCOPED_TRACE("Y plane"); |
| ValidatePlane(y_plane, size, 1, size.width(), salt + kYPlaneSalt); |
| } |
| |
| gfx::Size uv_size(size.width() / 2, size.height() / 2); |
| const uint8_t* u_plane = y_plane + size.width() * size.height(); |
| { |
| SCOPED_TRACE("U plane"); |
| ValidatePlane(u_plane, uv_size, 1, uv_size.width(), salt + kUPlaneSalt); |
| } |
| |
| const uint8_t* v_plane = u_plane + uv_size.width() * uv_size.height(); |
| { |
| SCOPED_TRACE("V plane"); |
| ValidatePlane(v_plane, uv_size, 1, uv_size.width(), salt + kVPlaneSalt); |
| } |
| } |
| |
| struct FakeCameraStream::Buffer { |
| explicit Buffer(base::WritableSharedMemoryMapping mapping) |
| : mapping(std::move(mapping)), |
| release_fence_watch_controller(FROM_HERE) {} |
| |
| base::WritableSharedMemoryMapping mapping; |
| |
| // Frame is used by the client when the |release_fence| is not null. |
| zx::eventpair release_fence; |
| |
| base::MessagePumpForIO::ZxHandleWatchController |
| release_fence_watch_controller; |
| }; |
| |
| FakeCameraStream::FakeCameraStream() |
| : binding_(this), |
| sysmem_allocator_(base::ComponentContextForProcess() |
| ->svc() |
| ->Connect<fuchsia::sysmem::Allocator>()) { |
| sysmem_allocator_->SetDebugClientInfo("ChromiumFakeCameraStream", |
| base::GetCurrentProcId()); |
| } |
| |
| FakeCameraStream::~FakeCameraStream() = default; |
| |
| void FakeCameraStream::Bind( |
| fidl::InterfaceRequest<fuchsia::camera3::Stream> request) { |
| binding_.Bind(std::move(request)); |
| } |
| |
| void FakeCameraStream::SetFirstBufferCollectionFailMode( |
| SysmemFailMode fail_mode) { |
| first_buffer_collection_fail_mode_ = fail_mode; |
| } |
| |
| void FakeCameraStream::SetFakeResolution(gfx::Size resolution) { |
| resolution_ = resolution; |
| resolution_update_ = |
| fuchsia::math::Size{resolution_.width(), resolution_.height()}; |
| SendResolution(); |
| } |
| |
| void FakeCameraStream::SetFakeOrientation( |
| fuchsia::camera3::Orientation orientation) { |
| orientation_ = orientation; |
| orientation_update_ = orientation; |
| SendOrientation(); |
| } |
| |
| bool FakeCameraStream::WaitBuffersAllocated() { |
| EXPECT_FALSE(wait_buffers_allocated_run_loop_); |
| |
| if (!buffers_.empty()) |
| return true; |
| |
| wait_buffers_allocated_run_loop_.emplace(); |
| wait_buffers_allocated_run_loop_->Run(); |
| wait_buffers_allocated_run_loop_.reset(); |
| |
| return !buffers_.empty(); |
| } |
| |
| bool FakeCameraStream::WaitFreeBuffer() { |
| EXPECT_FALSE(wait_free_buffer_run_loop_); |
| |
| if (num_used_buffers_ < buffers_.size()) |
| return true; |
| |
| wait_free_buffer_run_loop_.emplace(); |
| wait_free_buffer_run_loop_->Run(); |
| wait_free_buffer_run_loop_.reset(); |
| |
| return num_used_buffers_ < buffers_.size(); |
| } |
| |
| void FakeCameraStream::ProduceFrame(base::TimeTicks timestamp, uint8_t salt) { |
| ASSERT_LT(num_used_buffers_, buffers_.size()); |
| ASSERT_FALSE(next_frame_); |
| |
| size_t index = buffers_.size(); |
| for (size_t i = 0; i < buffers_.size(); ++i) { |
| if (!buffers_[i]->release_fence) { |
| index = i; |
| break; |
| } |
| } |
| EXPECT_LT(index, buffers_.size()); |
| |
| auto* buffer = buffers_[index].get(); |
| |
| gfx::Size coded_size((resolution_.width() + 1) & ~1, |
| (resolution_.height() + 1) & ~1); |
| |
| // Fill Y plane. |
| uint8_t* y_plane = reinterpret_cast<uint8_t*>(buffer->mapping.memory()); |
| size_t stride = kMaxFrameSize.width(); |
| FillPlane(y_plane, coded_size, /*x_step=*/1, /*y_step=*/stride, orientation_, |
| salt + kYPlaneSalt); |
| |
| // Fill UV plane. |
| gfx::Size uv_size(coded_size.width() / 2, coded_size.height() / 2); |
| uint8_t* uv_plane = y_plane + kMaxFrameSize.width() * kMaxFrameSize.height(); |
| FillPlane(uv_plane, uv_size, /*x_step=*/2, /*y_step=*/stride, orientation_, |
| salt + kUPlaneSalt); |
| FillPlane(uv_plane + 1, uv_size, /*x_step=*/2, /*y_step=*/stride, |
| orientation_, salt + kVPlaneSalt); |
| |
| // Create FrameInfo. |
| fuchsia::camera3::FrameInfo frame; |
| frame.frame_counter = frame_counter_++; |
| frame.buffer_index = 0; |
| frame.timestamp = timestamp.ToZxTime(); |
| EXPECT_EQ( |
| zx::eventpair::create(0u, &frame.release_fence, &buffer->release_fence), |
| ZX_OK); |
| |
| // Watch release fence to get notified when the frame is released. |
| base::CurrentIOThread::Get()->WatchZxHandle( |
| buffer->release_fence.get(), /*persistent=*/false, |
| ZX_EVENTPAIR_PEER_CLOSED, &buffer->release_fence_watch_controller, this); |
| |
| num_used_buffers_++; |
| next_frame_ = std::move(frame); |
| SendNextFrame(); |
| } |
| |
| void FakeCameraStream::WatchResolution(WatchResolutionCallback callback) { |
| EXPECT_FALSE(watch_resolution_callback_); |
| watch_resolution_callback_ = std::move(callback); |
| SendResolution(); |
| } |
| |
| void FakeCameraStream::WatchOrientation(WatchOrientationCallback callback) { |
| EXPECT_FALSE(watch_orientation_callback_); |
| watch_orientation_callback_ = std::move(callback); |
| SendOrientation(); |
| } |
| |
| void FakeCameraStream::SetBufferCollection( |
| fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> |
| token_handle) { |
| EXPECT_TRUE(token_handle); |
| |
| // Drop old buffers. |
| buffers_.clear(); |
| if (buffer_collection_) { |
| buffer_collection_->Close(); |
| buffer_collection_.Unbind(); |
| } |
| |
| new_buffer_collection_token_.Bind(std::move(token_handle)); |
| new_buffer_collection_token_.set_error_handler( |
| fit::bind_member(this, &FakeCameraStream::OnBufferCollectionError)); |
| |
| // Duplicate the token to access from the stream. |
| fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> |
| token_for_client; |
| new_buffer_collection_token_->Duplicate(ZX_RIGHT_SAME_RIGHTS, |
| token_for_client.NewRequest()); |
| |
| fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> failed_token; |
| if (first_buffer_collection_fail_mode_ == SysmemFailMode::kFailSync) { |
| // Create an additional token that's dropped in OnBufferCollectionSyncDone() |
| // before buffers are allocated. This will cause sysmem to fail the |
| // collection, so the future attempt to Sync() the collection from the |
| // production code will fail as well. |
| new_buffer_collection_token_->Duplicate( |
| /*rights_attenuation_mask=*/0, failed_token.NewRequest()); |
| } |
| |
| new_buffer_collection_token_->Sync( |
| [this, token_for_client = std::move(token_for_client), |
| failed_token = std::move(failed_token)]() mutable { |
| OnBufferCollectionSyncDone(std::move(token_for_client), |
| std::move(failed_token)); |
| }); |
| } |
| |
| void FakeCameraStream::WatchBufferCollection( |
| WatchBufferCollectionCallback callback) { |
| EXPECT_FALSE(watch_buffer_collection_callback_); |
| watch_buffer_collection_callback_ = std::move(callback); |
| SendBufferCollection(); |
| } |
| |
| void FakeCameraStream::GetNextFrame(GetNextFrameCallback callback) { |
| EXPECT_FALSE(get_next_frame_callback_); |
| get_next_frame_callback_ = std::move(callback); |
| SendNextFrame(); |
| } |
| |
| void FakeCameraStream::NotImplemented_(const std::string& name) { |
| ADD_FAILURE() << "NotImplemented_: " << name; |
| } |
| |
| void FakeCameraStream::OnBufferCollectionSyncDone( |
| fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> |
| token_for_client, |
| fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> |
| failed_token) { |
| // Return the token back to the client. |
| new_buffer_collection_token_for_client_ = std::move(token_for_client); |
| SendBufferCollection(); |
| |
| // Initialize the new collection using |local_token|. |
| sysmem_allocator_->BindSharedCollection( |
| std::move(new_buffer_collection_token_), buffer_collection_.NewRequest()); |
| |
| buffer_collection_.set_error_handler( |
| fit::bind_member(this, &FakeCameraStream::OnBufferCollectionError)); |
| |
| fuchsia::sysmem::BufferCollectionConstraints constraints; |
| constraints.usage.cpu = |
| fuchsia::sysmem::cpuUsageRead | fuchsia::sysmem::cpuUsageWrite; |
| |
| // The client is expected to request buffers it may need. We don't need to |
| // reserve any for the server side. |
| constraints.min_buffer_count_for_camping = 0; |
| |
| // Initialize image format. |
| constraints.image_format_constraints_count = 1; |
| constraints.image_format_constraints[0].pixel_format.type = |
| fuchsia::sysmem::PixelFormatType::NV12; |
| constraints.image_format_constraints[0].color_spaces_count = 1; |
| constraints.image_format_constraints[0].color_space[0].type = |
| fuchsia::sysmem::ColorSpaceType::REC601_NTSC; |
| constraints.image_format_constraints[0].required_max_coded_width = |
| kMaxFrameSize.width(); |
| constraints.image_format_constraints[0].required_max_coded_height = |
| kMaxFrameSize.height(); |
| |
| if (first_buffer_collection_fail_mode_ == SysmemFailMode::kFailAllocation) { |
| // Set color space to SRGB to trigger sysmem collection failure (SRGB is not |
| // compatible with NV12 pixel type). |
| constraints.image_format_constraints[0].color_space[0].type = |
| fuchsia::sysmem::ColorSpaceType::SRGB; |
| } |
| |
| buffer_collection_->SetConstraints(/*has_constraints=*/true, |
| std::move(constraints)); |
| buffer_collection_->WaitForBuffersAllocated( |
| fit::bind_member(this, &FakeCameraStream::OnBufferCollectionAllocated)); |
| } |
| |
| void FakeCameraStream::OnBufferCollectionError(zx_status_t status) { |
| if (first_buffer_collection_fail_mode_ != SysmemFailMode::kNone) { |
| first_buffer_collection_fail_mode_ = SysmemFailMode::kNone; |
| |
| // Create a new buffer collection to retry buffer allocation. |
| fuchsia::sysmem::BufferCollectionTokenPtr token; |
| sysmem_allocator_->AllocateSharedCollection(token.NewRequest()); |
| SetBufferCollection(std::move(token)); |
| return; |
| } |
| |
| ADD_FAILURE() << "BufferCollection failed."; |
| if (wait_buffers_allocated_run_loop_) |
| wait_buffers_allocated_run_loop_->Quit(); |
| if (wait_free_buffer_run_loop_) |
| wait_free_buffer_run_loop_->Quit(); |
| } |
| |
| void FakeCameraStream::OnBufferCollectionAllocated( |
| zx_status_t status, |
| fuchsia::sysmem::BufferCollectionInfo_2 buffer_collection_info) { |
| if (status != ZX_OK) { |
| OnBufferCollectionError(status); |
| return; |
| } |
| |
| EXPECT_TRUE(buffers_.empty()); |
| EXPECT_TRUE(buffer_collection_info.settings.has_image_format_constraints); |
| EXPECT_EQ(buffer_collection_info.settings.image_format_constraints |
| .pixel_format.type, |
| fuchsia::sysmem::PixelFormatType::NV12); |
| |
| size_t buffer_size = |
| buffer_collection_info.settings.buffer_settings.size_bytes; |
| for (size_t i = 0; i < buffer_collection_info.buffer_count; ++i) { |
| auto& buffer = buffer_collection_info.buffers[i]; |
| EXPECT_EQ(buffer.vmo_usable_start, 0U); |
| auto region = base::WritableSharedMemoryRegion::Deserialize( |
| base::subtle::PlatformSharedMemoryRegion::Take( |
| std::move(buffer.vmo), |
| base::subtle::PlatformSharedMemoryRegion::Mode::kWritable, |
| buffer_size, base::UnguessableToken::Create())); |
| auto mapping = region.Map(); |
| EXPECT_TRUE(mapping.IsValid()); |
| buffers_.push_back(std::make_unique<Buffer>(std::move(mapping))); |
| } |
| |
| if (wait_buffers_allocated_run_loop_) |
| wait_buffers_allocated_run_loop_->Quit(); |
| } |
| |
| void FakeCameraStream::SendResolution() { |
| if (!watch_resolution_callback_ || !resolution_update_) |
| return; |
| watch_resolution_callback_(resolution_update_.value()); |
| watch_resolution_callback_ = {}; |
| resolution_update_.reset(); |
| } |
| |
| void FakeCameraStream::SendOrientation() { |
| if (!watch_orientation_callback_ || !orientation_update_) |
| return; |
| watch_orientation_callback_(orientation_update_.value()); |
| watch_orientation_callback_ = {}; |
| orientation_update_.reset(); |
| } |
| |
| void FakeCameraStream::SendBufferCollection() { |
| if (!watch_buffer_collection_callback_ || |
| !new_buffer_collection_token_for_client_) { |
| return; |
| } |
| watch_buffer_collection_callback_( |
| std::move(new_buffer_collection_token_for_client_.value())); |
| watch_buffer_collection_callback_ = {}; |
| new_buffer_collection_token_for_client_.reset(); |
| } |
| |
| void FakeCameraStream::SendNextFrame() { |
| if (!get_next_frame_callback_ || !next_frame_) |
| return; |
| get_next_frame_callback_(std::move(next_frame_.value())); |
| get_next_frame_callback_ = {}; |
| next_frame_.reset(); |
| } |
| |
| void FakeCameraStream::OnZxHandleSignalled(zx_handle_t handle, |
| zx_signals_t signals) { |
| EXPECT_EQ(signals, ZX_EVENTPAIR_PEER_CLOSED); |
| |
| // Find the buffer that corresponds to the |handle|. |
| size_t index = buffers_.size(); |
| for (size_t i = 0; i < buffers_.size(); ++i) { |
| if (buffers_[i]->release_fence.get() == handle) { |
| index = i; |
| break; |
| } |
| } |
| ASSERT_LT(index, buffers_.size()); |
| buffers_[index]->release_fence = {}; |
| buffers_[index]->release_fence_watch_controller.StopWatchingZxHandle(); |
| num_used_buffers_--; |
| |
| if (wait_free_buffer_run_loop_) |
| wait_free_buffer_run_loop_->Quit(); |
| } |
| FakeCameraDevice::FakeCameraDevice() = default; |
| FakeCameraDevice::~FakeCameraDevice() = default; |
| |
| void FakeCameraDevice::Bind( |
| fidl::InterfaceRequest<fuchsia::camera3::Device> request) { |
| bindings_.AddBinding(this, std::move(request)); |
| } |
| |
| void FakeCameraDevice::SetGetIdentifierHandler( |
| base::RepeatingCallback<void(GetIdentifierCallback)> |
| get_identifier_handler) { |
| get_identifier_handler_ = std::move(get_identifier_handler); |
| } |
| |
| void FakeCameraDevice::GetIdentifier(GetIdentifierCallback callback) { |
| if (get_identifier_handler_) { |
| get_identifier_handler_.Run(std::move(callback)); |
| return; |
| } |
| |
| callback("Fake Camera"); |
| } |
| |
| void FakeCameraDevice::GetConfigurations(GetConfigurationsCallback callback) { |
| std::vector<fuchsia::camera3::Configuration> configurations(1); |
| configurations[0].streams.resize(1); |
| configurations[0].streams[0].frame_rate.numerator = 30; |
| configurations[0].streams[0].frame_rate.denominator = 1; |
| configurations[0].streams[0].image_format.pixel_format.type = |
| fuchsia::sysmem::PixelFormatType::NV12; |
| configurations[0].streams[0].image_format.coded_width = 640; |
| configurations[0].streams[0].image_format.coded_height = 480; |
| configurations[0].streams[0].image_format.bytes_per_row = 640; |
| callback(std::move(configurations)); |
| } |
| |
| void FakeCameraDevice::ConnectToStream( |
| uint32_t index, |
| fidl::InterfaceRequest<fuchsia::camera3::Stream> request) { |
| EXPECT_EQ(index, 0U); |
| stream_.Bind(std::move(request)); |
| } |
| |
| void FakeCameraDevice::NotImplemented_(const std::string& name) { |
| ADD_FAILURE() << "NotImplemented_: " << name; |
| } |
| |
| FakeCameraDeviceWatcher::FakeCameraDeviceWatcher( |
| sys::OutgoingDirectory* outgoing_directory) { |
| zx_status_t status = |
| outgoing_directory->AddPublicService<fuchsia::camera3::DeviceWatcher>( |
| [this]( |
| fidl::InterfaceRequest<fuchsia::camera3::DeviceWatcher> request) { |
| auto client = std::make_unique<Client>(this); |
| |
| // Queue events for all existing devices. |
| for (auto& device : devices_) { |
| fuchsia::camera3::WatchDevicesEvent event; |
| event.set_added(device.first); |
| client->QueueEvent(std::move(event)); |
| } |
| |
| bindings_.AddBinding(std::move(client), std::move(request)); |
| }); |
| ZX_CHECK(status == ZX_OK, status) << "AddPublicService failed"; |
| |
| devices_.insert( |
| std::make_pair(next_device_id_++, std::make_unique<FakeCameraDevice>())); |
| } |
| |
| FakeCameraDeviceWatcher::~FakeCameraDeviceWatcher() = default; |
| |
| void FakeCameraDeviceWatcher::DisconnectClients() { |
| bindings_.CloseAll(); |
| } |
| |
| std::unique_ptr<FakeCameraDevice> FakeCameraDeviceWatcher::RemoveDevice( |
| uint64_t device_id) { |
| auto device_it = devices_.find(device_id); |
| DCHECK(device_it != devices_.end()); |
| |
| // Queue an event for each client to inform about the device removal. |
| for (auto& binding : bindings_.bindings()) { |
| fuchsia::camera3::WatchDevicesEvent event; |
| event.set_removed(device_id); |
| binding->impl()->QueueEvent(std::move(event)); |
| } |
| |
| std::unique_ptr<FakeCameraDevice> device = std::move(device_it->second); |
| devices_.erase(device_it); |
| |
| return device; |
| } |
| |
| FakeCameraDeviceWatcher::Client::Client(FakeCameraDeviceWatcher* device_watcher) |
| : device_watcher_(device_watcher) {} |
| FakeCameraDeviceWatcher::Client::~Client() {} |
| |
| void FakeCameraDeviceWatcher::Client::QueueEvent( |
| fuchsia::camera3::WatchDevicesEvent event) { |
| event_queue_.push_back(std::move(event)); |
| |
| if (watch_devices_callback_) { |
| watch_devices_callback_(std::move(event_queue_)); |
| event_queue_.clear(); |
| watch_devices_callback_ = {}; |
| } |
| } |
| |
| void FakeCameraDeviceWatcher::Client::WatchDevices( |
| WatchDevicesCallback callback) { |
| DCHECK(!watch_devices_callback_); |
| |
| if (initial_list_sent_ && event_queue_.empty()) { |
| watch_devices_callback_ = std::move(callback); |
| return; |
| } |
| |
| callback(std::move(event_queue_)); |
| event_queue_.clear(); |
| initial_list_sent_ = true; |
| } |
| |
| void FakeCameraDeviceWatcher::Client::ConnectToDevice( |
| uint64_t id, |
| fidl::InterfaceRequest<fuchsia::camera3::Device> request) { |
| auto it = device_watcher_->devices().find(id); |
| if (it == device_watcher_->devices().end()) |
| return; |
| it->second->Bind(std::move(request)); |
| } |
| |
| void FakeCameraDeviceWatcher::Client::NotImplemented_(const std::string& name) { |
| ADD_FAILURE() << "NotImplemented_: " << name; |
| } |
| |
| } // namespace media |