diff --git a/packages/camera/camera_windows/lib/camera_windows.dart b/packages/camera/camera_windows/lib/camera_windows.dart index 523b41b69751..bdbfc17dcf8e 100644 --- a/packages/camera/camera_windows/lib/camera_windows.dart +++ b/packages/camera/camera_windows/lib/camera_windows.dart @@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart'; import 'package:stream_transform/stream_transform.dart'; import 'src/messages.g.dart'; +import 'type_conversion.dart'; /// An implementation of [CameraPlatform] for Windows. class CameraWindows extends CameraPlatform { @@ -32,6 +33,12 @@ class CameraWindows extends CameraPlatform { final Map hostCameraHandlers = {}; + // The stream to receive frames from the native code. + StreamSubscription? _platformImageStreamSubscription; + + // The stream for vending frames to platform interface clients. + StreamController? _frameStreamController; + /// The controller that broadcasts events coming from handleCameraMethodCall /// /// It is a `broadcast` because multiple controllers will connect to @@ -234,6 +241,57 @@ class CameraWindows extends CameraPlatform { 'resumeVideoRecording() is not supported due to Win32 API limitations.'); } + @override + Stream onStreamedFrameAvailable(int cameraId, + {CameraImageStreamOptions? options}) { + _installStreamController( + onListen: () => _onFrameStreamListen(cameraId), + onCancel: () => _onFrameStreamCancel(cameraId)); + return _frameStreamController!.stream; + } + + StreamController _installStreamController( + {void Function()? onListen, void Function()? onCancel}) { + _frameStreamController = StreamController( + onListen: onListen ?? () {}, + onPause: _onFrameStreamPauseResume, + onResume: _onFrameStreamPauseResume, + onCancel: onCancel ?? () {}, + ); + return _frameStreamController!; + } + + void _onFrameStreamListen(int cameraId) { + _startPlatformStream(cameraId); + } + + Future _startPlatformStream(int cameraId) async { + _startStreamListener(); + await _hostApi.startImageStream(cameraId); + } + + void _startStreamListener() { + const EventChannel cameraEventChannel = + EventChannel('plugins.flutter.io/camera_android/imageStream'); + _platformImageStreamSubscription = + cameraEventChannel.receiveBroadcastStream().listen((dynamic imageData) { + _frameStreamController! + .add(cameraImageFromPlatformData(imageData as Map)); + }); + } + + FutureOr _onFrameStreamCancel(int cameraId) async { + await _hostApi.stopImageStream(cameraId); + await _platformImageStreamSubscription?.cancel(); + _platformImageStreamSubscription = null; + _frameStreamController = null; + } + + void _onFrameStreamPauseResume() { + throw CameraException('InvalidCall', + 'Pause and resume are not supported for onStreamedFrameAvailable'); + } + @override Future setFlashMode(int cameraId, FlashMode mode) async { // TODO(jokerttu): Implement flash mode support, https://github.com/flutter/flutter/issues/97537. diff --git a/packages/camera/camera_windows/lib/src/messages.g.dart b/packages/camera/camera_windows/lib/src/messages.g.dart index a351d6d9cee6..9930549183bb 100644 --- a/packages/camera/camera_windows/lib/src/messages.g.dart +++ b/packages/camera/camera_windows/lib/src/messages.g.dart @@ -362,6 +362,56 @@ class CameraApi { } } + /// Starts the image stream for the given camera. + Future startImageStream(int cameraId) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.camera_windows.CameraApi.startImageStream$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([cameraId]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + /// Stops the image stream for the given camera. + Future stopImageStream(int cameraId) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.camera_windows.CameraApi.stopImageStream$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([cameraId]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + /// Starts the preview stream for the given camera. Future pausePreview(int cameraId) async { final String pigeonVar_channelName = diff --git a/packages/camera/camera_windows/lib/type_conversion.dart b/packages/camera/camera_windows/lib/type_conversion.dart new file mode 100644 index 000000000000..ed24a752341a --- /dev/null +++ b/packages/camera/camera_windows/lib/type_conversion.dart @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; + +/// Converts method channel call [data] for `receivedImageStreamData` to a +/// [CameraImageData]. +CameraImageData cameraImageFromPlatformData(Map data) { + return CameraImageData( + format: const CameraImageFormat(ImageFormatGroup.bgra8888, raw: 0), + height: data['height'] as int, + width: data['width'] as int, + lensAperture: data['lensAperture'] as double?, + sensorExposureTime: data['sensorExposureTime'] as int?, + sensorSensitivity: data['sensorSensitivity'] as double?, + planes: [ + CameraImagePlane( + bytes: data['data'] as Uint8List, + bytesPerRow: (data['width'] as int) * 4, + ) + ]); +} diff --git a/packages/camera/camera_windows/pigeons/messages.dart b/packages/camera/camera_windows/pigeons/messages.dart index cb70021a0ef1..d0970af423ac 100644 --- a/packages/camera/camera_windows/pigeons/messages.dart +++ b/packages/camera/camera_windows/pigeons/messages.dart @@ -70,6 +70,14 @@ abstract class CameraApi { @async String stopVideoRecording(int cameraId); + /// Starts the image stream for the given camera. + @async + void startImageStream(int cameraId); + + /// Stops the image stream for the given camera. + @async + void stopImageStream(int cameraId); + /// Starts the preview stream for the given camera. @async void pausePreview(int cameraId); diff --git a/packages/camera/camera_windows/windows/camera.h b/packages/camera/camera_windows/windows/camera.h index 44e9fcde5883..2a44ad2ccf62 100644 --- a/packages/camera/camera_windows/windows/camera.h +++ b/packages/camera/camera_windows/windows/camera.h @@ -22,6 +22,8 @@ enum class PendingResultType { kTakePicture, kStartRecord, kStopRecord, + kStartStream, + kStopStream, kPausePreview, kResumePreview, }; diff --git a/packages/camera/camera_windows/windows/camera_plugin.cpp b/packages/camera/camera_windows/windows/camera_plugin.cpp index 089aa28c7222..4b2abe39d6ac 100644 --- a/packages/camera/camera_windows/windows/camera_plugin.cpp +++ b/packages/camera/camera_windows/windows/camera_plugin.cpp @@ -4,6 +4,8 @@ #include "camera_plugin.h" +#include +#include #include #include #include @@ -32,6 +34,10 @@ namespace { const std::string kPictureCaptureExtension = "jpeg"; const std::string kVideoCaptureExtension = "mp4"; +constexpr char kFrameEventChannelName[] = + "plugins.flutter.io/camera_android/imageStream"; + +std::unique_ptr> event_sink; // Builds CaptureDeviceInfo object from given device holding device name and id. std::unique_ptr GetDeviceInfo(IMFActivate* device) { @@ -116,12 +122,34 @@ std::optional GetFilePathForVideo() { } } // namespace +// a setter for the event sink helpful for testing. +void CameraPlugin::SetEventSink( + std::unique_ptr> events) { + event_sink = std::move(events); +} + // static void CameraPlugin::RegisterWithRegistrar( flutter::PluginRegistrarWindows* registrar) { std::unique_ptr plugin = std::make_unique( registrar->texture_registrar(), registrar->messenger()); + auto frameEventchannel = std::make_unique>( + registrar->messenger(), kFrameEventChannelName, + &flutter::StandardMethodCodec::GetInstance()); + + auto event_channel_handler = + std::make_unique>( + [plugin = plugin.get()](auto arguments, auto events) { + plugin->SetEventSink(std::move(events)); + return nullptr; + }, + [](auto arguments) { + event_sink.reset(); + return nullptr; + }); + frameEventchannel->SetStreamHandler(std::move(event_channel_handler)); + CameraApi::SetUp(registrar->messenger(), plugin.get()); registrar->AddPlugin(std::move(plugin)); @@ -341,6 +369,53 @@ void CameraPlugin::StopVideoRecording( } } +void CameraPlugin::StartImageStream( + int64_t camera_id, + std::function reply)> result) { + // check if request already exists + Camera* camera = GetCameraByCameraId(camera_id); + if (!camera) { + return result(FlutterError("camera_error", "Camera not created")); + } + if (camera->HasPendingResultByType(PendingResultType::kStartStream)) { + return result( + FlutterError("camera_error", "Pending start stream request exists")); + } + + if (!event_sink) { + return result(FlutterError("camera_error", + "Unable to make event channel from windows")); + } + + if (camera->AddPendingVoidResult(PendingResultType::kStartStream, + std::move(result))) { + CaptureController* cc = camera->GetCaptureController(); + assert(cc); + cc->StartImageStream(std::move(event_sink)); + } +} + +void CameraPlugin::StopImageStream( + int64_t camera_id, + std::function reply)> result) { + // check if request already exists + Camera* camera = GetCameraByCameraId(camera_id); + if (!camera) { + return result(FlutterError("camera_error", "Camera not created")); + } + if (camera->HasPendingResultByType(PendingResultType::kStopStream)) { + return result( + FlutterError("camera_error", "Pending stop stream request exists")); + } + + if (camera->AddPendingVoidResult(PendingResultType::kStopStream, + std::move(result))) { + CaptureController* cc = camera->GetCaptureController(); + assert(cc); + cc->StopImageStream(); + } +} + void CameraPlugin::TakePicture( int64_t camera_id, std::function reply)> result) { auto camera = GetCameraByCameraId(camera_id); diff --git a/packages/camera/camera_windows/windows/camera_plugin.h b/packages/camera/camera_windows/windows/camera_plugin.h index 422f5c92b365..f77711402bb0 100644 --- a/packages/camera/camera_windows/windows/camera_plugin.h +++ b/packages/camera/camera_windows/windows/camera_plugin.h @@ -31,6 +31,8 @@ class CameraPlugin : public flutter::Plugin, public CameraApi, public VideoCaptureDeviceEnumerator { public: + void SetEventSink( + std::unique_ptr> events); static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); CameraPlugin(flutter::TextureRegistrar* texture_registrar, @@ -68,6 +70,12 @@ class CameraPlugin : public flutter::Plugin, void StopVideoRecording( int64_t camera_id, std::function reply)> result) override; + void StartImageStream( + int64_t camera_id, + std::function reply)> result) override; + void StopImageStream( + int64_t camera_id, + std::function reply)> result) override; void TakePicture( int64_t camera_id, std::function reply)> result) override; diff --git a/packages/camera/camera_windows/windows/capture_controller.cpp b/packages/camera/camera_windows/windows/capture_controller.cpp index 44675daebec7..87c2290e3351 100644 --- a/packages/camera/camera_windows/windows/capture_controller.cpp +++ b/packages/camera/camera_windows/windows/capture_controller.cpp @@ -5,11 +5,14 @@ #include "capture_controller.h" #include +#include +#include #include #include #include #include +#include #include "com_heap_ptr.h" #include "photo_handler.h" @@ -555,6 +558,16 @@ void CaptureControllerImpl::StopRecord() { "Failed to stop video recording"); } } +void CaptureControllerImpl::StartImageStream( + std::unique_ptr> sink) { + assert(capture_controller_listener_); + image_stream_sink_ = std::move(sink); +} + +void CaptureControllerImpl::StopImageStream() { + assert(capture_controller_listener_); + image_stream_sink_.reset(); +} // Starts capturing preview frames using preview handler // After first frame is captured, OnPreviewStarted is called @@ -863,6 +876,32 @@ bool CaptureControllerImpl::UpdateBuffer(uint8_t* buffer, if (!texture_handler_) { return false; } + if (image_stream_sink_) { + // Convert the buffer data to a std::vector. + std::vector buffer_data(buffer, buffer + data_length); + + // Ensure preview_frame_height_ and preview_frame_width_ are of supported + // types. + int preview_frame_height = static_cast(preview_frame_height_); + int preview_frame_width = static_cast(preview_frame_width_); + + // Create a map to hold the buffer data and data length. + flutter::EncodableMap data_map; + data_map[flutter::EncodableValue("data")] = + flutter::EncodableValue(buffer_data); + data_map[flutter::EncodableValue("height")] = + flutter::EncodableValue(preview_frame_height); + data_map[flutter::EncodableValue("width")] = + flutter::EncodableValue(preview_frame_width); + data_map[flutter::EncodableValue("length")] = + flutter::EncodableValue(static_cast(data_length)); + + // Wrap the map in a flutter::EncodableValue. + flutter::EncodableValue encoded_value(data_map); + + // Send the encoded value through the image_stream_sink_. + image_stream_sink_->Success(encoded_value); + } return texture_handler_->UpdateBuffer(buffer, data_length); } diff --git a/packages/camera/camera_windows/windows/capture_controller.h b/packages/camera/camera_windows/windows/capture_controller.h index 2280cb3b93f9..42e8ab2547a1 100644 --- a/packages/camera/camera_windows/windows/capture_controller.h +++ b/packages/camera/camera_windows/windows/capture_controller.h @@ -6,6 +6,7 @@ #define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAPTURE_CONTROLLER_H_ #include +#include #include #include #include @@ -92,6 +93,13 @@ class CaptureController { // Stops the current video recording. virtual void StopRecord() = 0; + // Starts image streaming. + virtual void StartImageStream( + std::unique_ptr> sink) = 0; + + // Stops the current image streaming. + virtual void StopImageStream() = 0; + // Captures a still photo. virtual void TakePicture(const std::string& file_path) = 0; }; @@ -125,6 +133,10 @@ class CaptureControllerImpl : public CaptureController, void ResumePreview() override; void StartRecord(const std::string& file_path) override; void StopRecord() override; + void StartImageStream( + std::unique_ptr> sink) + override; + void StopImageStream() override; void TakePicture(const std::string& file_path) override; // CaptureEngineObserver @@ -219,8 +231,9 @@ class CaptureControllerImpl : public CaptureController, std::unique_ptr preview_handler_; std::unique_ptr photo_handler_; std::unique_ptr texture_handler_; + std::unique_ptr> + image_stream_sink_; CaptureControllerListener* capture_controller_listener_; - std::string video_device_id_; CaptureEngineState capture_engine_state_ = CaptureEngineState::kNotInitialized; diff --git a/packages/camera/camera_windows/windows/messages.g.cpp b/packages/camera/camera_windows/windows/messages.g.cpp index 541ca97d0182..724d42037480 100644 --- a/packages/camera/camera_windows/windows/messages.g.cpp +++ b/packages/camera/camera_windows/windows/messages.g.cpp @@ -498,6 +498,78 @@ void CameraApi::SetUp(flutter::BinaryMessenger* binary_messenger, channel.SetMessageHandler(nullptr); } } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.camera_windows.CameraApi.startImageStream" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_camera_id_arg = args.at(0); + if (encodable_camera_id_arg.IsNull()) { + reply(WrapError("camera_id_arg unexpectedly null.")); + return; + } + const int64_t camera_id_arg = encodable_camera_id_arg.LongValue(); + api->StartImageStream( + camera_id_arg, [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.camera_windows.CameraApi.stopImageStream" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_camera_id_arg = args.at(0); + if (encodable_camera_id_arg.IsNull()) { + reply(WrapError("camera_id_arg unexpectedly null.")); + return; + } + const int64_t camera_id_arg = encodable_camera_id_arg.LongValue(); + api->StopImageStream( + camera_id_arg, [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } { BasicMessageChannel<> channel( binary_messenger, diff --git a/packages/camera/camera_windows/windows/messages.g.h b/packages/camera/camera_windows/windows/messages.g.h index a85c692e5b03..9dcb6fab7d14 100644 --- a/packages/camera/camera_windows/windows/messages.g.h +++ b/packages/camera/camera_windows/windows/messages.g.h @@ -189,6 +189,14 @@ class CameraApi { virtual void StopVideoRecording( int64_t camera_id, std::function reply)> result) = 0; + // Starts the image stream for the given camera. + virtual void StartImageStream( + int64_t camera_id, + std::function reply)> result) = 0; + // Stops the image stream for the given camera. + virtual void StopImageStream( + int64_t camera_id, + std::function reply)> result) = 0; // Starts the preview stream for the given camera. virtual void PausePreview( int64_t camera_id, diff --git a/packages/camera/camera_windows/windows/test/camera_plugin_test.cpp b/packages/camera/camera_windows/windows/test/camera_plugin_test.cpp index 2680ae26c0e3..6bd59dbde8a4 100644 --- a/packages/camera/camera_windows/windows/test/camera_plugin_test.cpp +++ b/packages/camera/camera_windows/windows/test/camera_plugin_test.cpp @@ -351,6 +351,232 @@ TEST(CameraPlugin, InitializeHandlerCallStartPreview) { EXPECT_TRUE(result_called); } +TEST(CameraPlugin, StartImageStreamHandlerCallsStartImageStream) { + int64_t mock_camera_id = 1234; + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId(Eq(mock_camera_id))) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, + HasPendingResultByType(Eq(PendingResultType::kStartStream))) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_CALL(*camera, + AddPendingVoidResult(Eq(PendingResultType::kStartStream), _)) + .Times(1) + .WillOnce([cam = camera.get()]( + PendingResultType type, + std::function)> result) { + cam->pending_void_result_ = std::move(result); + return true; + }); + + EXPECT_CALL(*camera, GetCaptureController) + .Times(1) + .WillOnce([cam = camera.get()]() { + assert(cam->pending_void_result_); + return cam->capture_controller_.get(); + }); + + EXPECT_CALL(*capture_controller, StartImageStream) + .Times(1) + .WillOnce([cam = camera.get()]() { + assert(cam->pending_void_result_); + return cam->pending_void_result_(std::nullopt); + }); + + camera->camera_id_ = mock_camera_id; + camera->capture_controller_ = std::move(capture_controller); + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + // Set the event sink to a mocked event sink. + auto mock_event_sink = std::make_unique(); + plugin.SetEventSink(std::move(mock_event_sink)); + + bool result_called = false; + std::function)> start_image_stream_result = + [&result_called](std::optional reply) { + EXPECT_FALSE(result_called); // Ensure only one reply call. + result_called = true; + EXPECT_FALSE(reply); + }; + + plugin.StartImageStream(mock_camera_id, std::move(start_image_stream_result)); + + EXPECT_TRUE(result_called); +} + +TEST(CameraPlugin, StartImageStreamHandlerErrorOnInvalidCameraId) { + int64_t mock_camera_id = 1234; + int64_t missing_camera_id = 5678; + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, HasPendingResultByType).Times(0); + EXPECT_CALL(*camera, AddPendingVoidResult).Times(0); + EXPECT_CALL(*camera, GetCaptureController).Times(0); + EXPECT_CALL(*capture_controller, StartImageStream).Times(0); + + camera->camera_id_ = mock_camera_id; + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + bool result_called = false; + std::function)> start_image_stream_result = + [&result_called](std::optional reply) { + EXPECT_FALSE(result_called); // Ensure only one reply call. + result_called = true; + EXPECT_TRUE(reply); + }; + + plugin.StartImageStream(missing_camera_id, + std::move(start_image_stream_result)); + + EXPECT_TRUE(result_called); +} + +TEST(CameraPlugin, StopImageStreamHandlerCallsStopImageStream) { + int64_t mock_camera_id = 1234; + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId(Eq(mock_camera_id))) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, + HasPendingResultByType(Eq(PendingResultType::kStopStream))) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_CALL(*camera, + AddPendingVoidResult(Eq(PendingResultType::kStopStream), _)) + .Times(1) + .WillOnce([cam = camera.get()]( + PendingResultType type, + std::function)> result) { + cam->pending_void_result_ = std::move(result); + return true; + }); + + EXPECT_CALL(*camera, GetCaptureController) + .Times(1) + .WillOnce([cam = camera.get()]() { + assert(cam->pending_void_result_); + return cam->capture_controller_.get(); + }); + + EXPECT_CALL(*capture_controller, StopImageStream) + .Times(1) + .WillOnce([cam = camera.get()]() { + assert(cam->pending_void_result_); + return cam->pending_void_result_(std::nullopt); + }); + + camera->camera_id_ = mock_camera_id; + camera->capture_controller_ = std::move(capture_controller); + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + bool result_called = false; + std::function)> stop_image_stream_result = + [&result_called](std::optional reply) { + EXPECT_FALSE(result_called); // Ensure only one reply call. + result_called = true; + EXPECT_FALSE(reply); + }; + + plugin.StopImageStream(mock_camera_id, std::move(stop_image_stream_result)); + + EXPECT_TRUE(result_called); +} + +TEST(CameraPlugin, StopImageStreamHandlerErrorOnInvalidCameraId) { + int64_t mock_camera_id = 1234; + int64_t missing_camera_id = 5678; + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, HasPendingResultByType).Times(0); + EXPECT_CALL(*camera, AddPendingVoidResult).Times(0); + EXPECT_CALL(*camera, GetCaptureController).Times(0); + EXPECT_CALL(*capture_controller, StopImageStream).Times(0); + + camera->camera_id_ = mock_camera_id; + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + bool result_called = false; + std::function)> stop_image_stream_result = + [&result_called](std::optional reply) { + EXPECT_FALSE(result_called); // Ensure only one reply call. + result_called = true; + EXPECT_TRUE(reply); + }; + + plugin.StopImageStream(missing_camera_id, + std::move(stop_image_stream_result)); + + EXPECT_TRUE(result_called); +} + TEST(CameraPlugin, InitializeHandlerErrorOnInvalidCameraId) { int64_t mock_camera_id = 1234; int64_t missing_camera_id = 5678; diff --git a/packages/camera/camera_windows/windows/test/mocks.h b/packages/camera/camera_windows/windows/test/mocks.h index 4a05ef822969..8c279589f8c0 100644 --- a/packages/camera/camera_windows/windows/test/mocks.h +++ b/packages/camera/camera_windows/windows/test/mocks.h @@ -248,6 +248,11 @@ class MockCaptureController : public CaptureController { MOCK_METHOD(void, PausePreview, (), (override)); MOCK_METHOD(void, StartRecord, (const std::string& file_path), (override)); MOCK_METHOD(void, StopRecord, (), (override)); + MOCK_METHOD( + void, StartImageStream, + (std::unique_ptr> sink), + (override)); + MOCK_METHOD(void, StopImageStream, (), (override)); MOCK_METHOD(void, TakePicture, (const std::string& file_path), (override)); }; @@ -1021,6 +1026,9 @@ class MockCaptureEngine : public IMFCaptureEngine { MOCK_METHOD(HRESULT, StartPreview, ()); MOCK_METHOD(HRESULT, StopPreview, ()); MOCK_METHOD(HRESULT, StartRecord, ()); + MOCK_METHOD(HRESULT, StartImageStream, ()); + MOCK_METHOD(HRESULT, StopImageStream, ()); + MOCK_METHOD(HRESULT, StopRecord, (BOOL finalize, BOOL flushUnprocessedSamples)); MOCK_METHOD(HRESULT, TakePhoto, ()); @@ -1068,6 +1076,17 @@ class MockCaptureEngine : public IMFCaptureEngine { volatile ULONG ref_ = 0; bool initialized_ = false; }; +// Mock class for flutter::EventSink +class MockEventSink : public flutter::EventSink { + public: + MOCK_METHOD(void, SuccessInternal, (const flutter::EncodableValue* event), + (override)); + MOCK_METHOD(void, ErrorInternal, + (const std::string& error_code, const std::string& error_message, + const flutter::EncodableValue* error_details), + (override)); + MOCK_METHOD(void, EndOfStreamInternal, (), (override)); +}; #define MOCK_DEVICE_ID "mock_device_id" #define MOCK_CAMERA_NAME "mock_camera_name <" MOCK_DEVICE_ID ">"