diff --git a/packages/camera/camera_windows/CHANGELOG.md b/packages/camera/camera_windows/CHANGELOG.md index 4be03155c2d..ffb03962340 100644 --- a/packages/camera/camera_windows/CHANGELOG.md +++ b/packages/camera/camera_windows/CHANGELOG.md @@ -1,9 +1,11 @@ -## NEXT +## 0.2.7+1 +* Restores Support for streaming frames. * Updates minimum supported SDK version to Flutter 3.27/Dart 3.6. ## 0.2.6+2 +* Restores support for streaming frames. * Fixes compile errors under strict standards mode. * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. diff --git a/packages/camera/camera_windows/lib/camera_windows.dart b/packages/camera/camera_windows/lib/camera_windows.dart index 523b41b6975..56b29e77d2a 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 'src/type_conversion.dart'; /// An implementation of [CameraPlatform] for Windows. class CameraWindows extends CameraPlatform { @@ -234,6 +235,40 @@ class CameraWindows extends CameraPlatform { 'resumeVideoRecording() is not supported due to Win32 API limitations.'); } + @override + bool supportsImageStreaming() => true; + + @override + Stream onStreamedFrameAvailable(int cameraId, + {CameraImageStreamOptions? options}) { + late StreamController controller; + StreamSubscription? subscription; + + controller = StreamController( + onListen: () async { + final String eventChannelName = + await _hostApi.startImageStream(cameraId); + final EventChannel imageStreamChannel = + EventChannel(eventChannelName); + subscription = imageStreamChannel.receiveBroadcastStream().listen( + (dynamic image) => controller.add( + cameraImageFromPlatformData(image as Map))); + }, + onPause: _onFrameStreamPauseResume, + onResume: _onFrameStreamPauseResume, + onCancel: () async { + // Cancelling the subscription stops the image capture on the native side. + await subscription?.cancel(); + }); + + return controller.stream; + } + + 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 a351d6d9cee..0d815c7c95e 100644 --- a/packages/camera/camera_windows/lib/src/messages.g.dart +++ b/packages/camera/camera_windows/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.6.0), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -108,6 +108,43 @@ class PlatformSize { } } +/// A representation of frame data from the camera preview stream. +class PlatformFrameData { + PlatformFrameData({ + required this.data, + required this.width, + required this.height, + required this.length, + }); + + List data; + + int width; + + int height; + + int length; + + Object encode() { + return [ + data, + width, + height, + length, + ]; + } + + static PlatformFrameData decode(Object result) { + result as List; + return PlatformFrameData( + data: (result[0] as List?)!.cast(), + width: result[1]! as int, + height: result[2]! as int, + length: result[3]! as int, + ); + } +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -124,6 +161,9 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PlatformSize) { buffer.putUint8(131); writeValue(buffer, value.encode()); + } else if (value is PlatformFrameData) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -139,6 +179,8 @@ class _PigeonCodec extends StandardMessageCodec { return PlatformMediaSettings.decode(readValue(buffer)!); case 131: return PlatformSize.decode(readValue(buffer)!); + case 132: + return PlatformFrameData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -362,6 +404,38 @@ class CameraApi { } } + /// Starts the image stream for the given camera. + /// Returns the name of the [EventChannel] used to deliver the images. + /// Cancelling the subscription to the channel stops the capture. + 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 if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as String?)!; + } + } + /// 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/src/type_conversion.dart b/packages/camera/camera_windows/lib/src/type_conversion.dart new file mode 100644 index 00000000000..ed24a752341 --- /dev/null +++ b/packages/camera/camera_windows/lib/src/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 cb70021a0ef..f8eb1fde2c9 100644 --- a/packages/camera/camera_windows/pigeons/messages.dart +++ b/packages/camera/camera_windows/pigeons/messages.dart @@ -40,6 +40,21 @@ class PlatformSize { final double height; } +/// A representation of frame data from the camera preview stream. +class PlatformFrameData { + PlatformFrameData({ + required this.data, + required this.width, + required this.height, + required this.length, + }); + + final List data; + final int width; + final int height; + final int length; +} + @HostApi() abstract class CameraApi { /// Returns the names of all of the available capture devices. @@ -70,6 +85,11 @@ abstract class CameraApi { @async String stopVideoRecording(int cameraId); + /// Starts the image stream for the given camera. + /// Returns the name of the [EventChannel] used to deliver the images. + /// Cancelling the subscription to the channel stops the capture. + String startImageStream(int cameraId); + /// Starts the preview stream for the given camera. @async void pausePreview(int cameraId); diff --git a/packages/camera/camera_windows/pubspec.yaml b/packages/camera/camera_windows/pubspec.yaml index 284f053cd91..f937c5f5be9 100644 --- a/packages/camera/camera_windows/pubspec.yaml +++ b/packages/camera/camera_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_windows description: A Flutter plugin for getting information about and controlling the camera on Windows. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.2.6+2 +version: 0.2.7+1 environment: sdk: ^3.6.0 @@ -17,7 +17,7 @@ flutter: dartPluginClass: CameraWindows dependencies: - camera_platform_interface: ^2.6.0 + camera_platform_interface: ^2.9.0 cross_file: ^0.3.1 flutter: sdk: flutter diff --git a/packages/camera/camera_windows/test/camera_windows_test.dart b/packages/camera/camera_windows/test/camera_windows_test.dart index 7d21af5f782..ccbfcc1c61f 100644 --- a/packages/camera/camera_windows/test/camera_windows_test.dart +++ b/packages/camera/camera_windows/test/camera_windows_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:async/async.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_windows/camera_windows.dart'; @@ -181,6 +183,11 @@ void main() { verify(mockApi.dispose(captureAny)); expect(verification.captured[0], cameraId); }); + + test('Should report support for image streaming', () async { + final CameraWindows plugin = CameraWindows(api: MockCameraApi()); + expect(plugin.supportsImageStreaming(), true); + }); }); group('Event Tests', () { @@ -548,6 +555,15 @@ void main() { verify(mockApi.resumePreview(captureAny)); expect(verification.captured[0], cameraId); }); + + test('Should start the image stream when it is subscribed', () async { + final Stream stream = + plugin.onStreamedFrameAvailable(cameraId); + + await stream.listen((CameraImageData frame) {}).cancel(); + + verify(mockApi.startImageStream(cameraId)); + }); }); }); } diff --git a/packages/camera/camera_windows/test/camera_windows_test.mocks.dart b/packages/camera/camera_windows/test/camera_windows_test.mocks.dart index 7e22e173918..a7f873fb427 100644 --- a/packages/camera/camera_windows/test/camera_windows_test.mocks.dart +++ b/packages/camera/camera_windows/test/camera_windows_test.mocks.dart @@ -166,24 +166,15 @@ class MockCameraApi extends _i1.Mock implements _i2.CameraApi { ) as _i4.Future); @override - _i4.Future startImageStream(int? cameraId) => (super.noSuchMethod( + _i4.Future startImageStream(int? cameraId) => (super.noSuchMethod( Invocation.method( #startImageStream, [cameraId], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - _i4.Future stopImageStream(int? cameraId) => (super.noSuchMethod( - Invocation.method( - #stopImageStream, - [cameraId], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i4.Future.value('imageStream/$cameraId'), + returnValueForMissingStub: + _i4.Future.value('imageStream/$cameraId'), + ) as _i4.Future); @override _i4.Future pausePreview(int? cameraId) => (super.noSuchMethod( diff --git a/packages/camera/camera_windows/windows/CMakeLists.txt b/packages/camera/camera_windows/windows/CMakeLists.txt index e209a22ec0f..ef607578de3 100644 --- a/packages/camera/camera_windows/windows/CMakeLists.txt +++ b/packages/camera/camera_windows/windows/CMakeLists.txt @@ -33,6 +33,9 @@ list(APPEND PLUGIN_SOURCES "texture_handler.h" "texture_handler.cpp" "com_heap_ptr.h" + "task_runner.h" + "task_runner_window.h" + "task_runner_window.cpp" ) add_library(${PLUGIN_NAME} SHARED @@ -84,6 +87,7 @@ add_executable(${TEST_RUNNER} test/camera_plugin_test.cpp test/camera_test.cpp test/capture_controller_test.cpp + test/task_runner_window_test.cpp ${PLUGIN_SOURCES} ) apply_standard_settings(${TEST_RUNNER}) diff --git a/packages/camera/camera_windows/windows/camera.cpp b/packages/camera/camera_windows/windows/camera.cpp index 13bf3b3efae..b6d0e7ffc5f 100644 --- a/packages/camera/camera_windows/windows/camera.cpp +++ b/packages/camera/camera_windows/windows/camera.cpp @@ -39,24 +39,26 @@ CameraImpl::~CameraImpl() { bool CameraImpl::InitCamera(flutter::TextureRegistrar* texture_registrar, flutter::BinaryMessenger* messenger, - const PlatformMediaSettings& media_settings) { + const PlatformMediaSettings& media_settings, + std::unique_ptr task_runner) { auto capture_controller_factory = std::make_unique(); return InitCamera(std::move(capture_controller_factory), texture_registrar, - messenger, media_settings); + messenger, media_settings, task_runner); } bool CameraImpl::InitCamera( std::unique_ptr capture_controller_factory, flutter::TextureRegistrar* texture_registrar, flutter::BinaryMessenger* messenger, - const PlatformMediaSettings& media_settings) { + const PlatformMediaSettings& media_settings, + std::unique_ptr task_runner) { assert(!device_id_.empty()); messenger_ = messenger; capture_controller_ = capture_controller_factory->CreateCaptureController(this); return capture_controller_->InitCaptureDevice(texture_registrar, device_id_, - media_settings); + media_settings, task_runner); } bool CameraImpl::AddPendingVoidResult( diff --git a/packages/camera/camera_windows/windows/camera.h b/packages/camera/camera_windows/windows/camera.h index 44e9fcde588..7b4198703b0 100644 --- a/packages/camera/camera_windows/windows/camera.h +++ b/packages/camera/camera_windows/windows/camera.h @@ -85,7 +85,8 @@ class Camera : public CaptureControllerListener { // Returns false if initialization fails. virtual bool InitCamera(flutter::TextureRegistrar* texture_registrar, flutter::BinaryMessenger* messenger, - const PlatformMediaSettings& media_settings) = 0; + const PlatformMediaSettings& media_settings, + std::unique_ptr task_runner) = 0; }; // Concrete implementation of the |Camera| interface. @@ -151,7 +152,8 @@ class CameraImpl : public Camera { } bool InitCamera(flutter::TextureRegistrar* texture_registrar, flutter::BinaryMessenger* messenger, - const PlatformMediaSettings& media_settings) override; + const PlatformMediaSettings& media_settings, + std::unique_ptr task_runner) override; // Initializes the camera and its associated capture controller. // @@ -163,7 +165,8 @@ class CameraImpl : public Camera { std::unique_ptr capture_controller_factory, flutter::TextureRegistrar* texture_registrar, flutter::BinaryMessenger* messenger, - const PlatformMediaSettings& media_settings); + const PlatformMediaSettings& media_settings, + std::unique_ptr task_runner); private: // A generic type for any pending asyncronous result. @@ -230,6 +233,7 @@ class CameraImpl : public Camera { std::unique_ptr capture_controller_; std::unique_ptr event_api_; flutter::BinaryMessenger* messenger_ = nullptr; + std::unique_ptr task_runner_; int64_t camera_id_ = -1; std::string device_id_; }; diff --git a/packages/camera/camera_windows/windows/camera_plugin.cpp b/packages/camera/camera_windows/windows/camera_plugin.cpp index 089aa28c722..d939139e471 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 @@ -17,11 +19,13 @@ #include #include #include +#include #include "capture_device_info.h" #include "com_heap_ptr.h" #include "messages.g.h" #include "string_utils.h" +#include "task_runner_window.h" namespace camera_windows { using flutter::EncodableList; @@ -131,7 +135,8 @@ CameraPlugin::CameraPlugin(flutter::TextureRegistrar* texture_registrar, flutter::BinaryMessenger* messenger) : texture_registrar_(texture_registrar), messenger_(messenger), - camera_factory_(std::make_unique()) {} + camera_factory_(std::make_unique()), + task_runner_(std::make_unique()) {} CameraPlugin::CameraPlugin(flutter::TextureRegistrar* texture_registrar, flutter::BinaryMessenger* messenger, @@ -223,8 +228,8 @@ void CameraPlugin::Create(const std::string& camera_name, if (camera->AddPendingIntResult(PendingResultType::kCreateCamera, std::move(result))) { - bool initialized = - camera->InitCamera(texture_registrar_, messenger_, settings); + bool initialized = camera->InitCamera(texture_registrar_, messenger_, + settings, task_runner_); if (initialized) { cameras_.push_back(std::move(camera)); } @@ -341,6 +346,44 @@ void CameraPlugin::StopVideoRecording( } } +ErrorOr CameraPlugin::StartImageStream(int64_t camera_id) { + Camera* camera = GetCameraByCameraId(camera_id); + if (!camera) { + return FlutterError("camera_error", "Camera not created"); + } + + CaptureController* cc = camera->GetCaptureController(); + assert(cc); + + if (cc->IsStreaming()) { + return FlutterError("camera_error", + "Images from camera are already streaming"); + } + + std::ostringstream event_channel_name; + event_channel_name << "plugins.flutter.io/camera_windows/imageStream/" + << camera_id; + + auto frame_event_channel = + flutter::EventChannel(messenger_, event_channel_name.str(), + &flutter::StandardMethodCodec::GetInstance()); + + auto event_channel_handler = + std::make_unique>( + [cc](auto arguments, auto events) { + cc->StartImageStream(std::move(events)); + return nullptr; + }, + [cc](auto arguments) { + cc->StopImageStream(); + return nullptr; + }); + + frame_event_channel.SetStreamHandler(std::move(event_channel_handler)); + + return event_channel_name.str(); +} + 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 422f5c92b36..6a2eb98b1f2 100644 --- a/packages/camera/camera_windows/windows/camera_plugin.h +++ b/packages/camera/camera_windows/windows/camera_plugin.h @@ -16,6 +16,7 @@ #include "capture_controller.h" #include "capture_controller_listener.h" #include "messages.g.h" +#include "task_runner.h" namespace camera_windows { using flutter::MethodResult; @@ -68,6 +69,7 @@ class CameraPlugin : public flutter::Plugin, void StopVideoRecording( int64_t camera_id, std::function reply)> result) override; + ErrorOr StartImageStream(int64_t camera_id) override; void TakePicture( int64_t camera_id, std::function reply)> result) override; @@ -93,6 +95,7 @@ class CameraPlugin : public flutter::Plugin, flutter::TextureRegistrar* texture_registrar_; flutter::BinaryMessenger* messenger_; std::vector> cameras_; + std::unique_ptr task_runner_; friend class camera_windows::test::MockCameraPlugin; }; diff --git a/packages/camera/camera_windows/windows/capture_controller.cpp b/packages/camera/camera_windows/windows/capture_controller.cpp index 44675daebec..85f79a9e43b 100644 --- a/packages/camera/camera_windows/windows/capture_controller.cpp +++ b/packages/camera/camera_windows/windows/capture_controller.cpp @@ -5,13 +5,17 @@ #include "capture_controller.h" #include +#include +#include #include #include #include #include +#include #include "com_heap_ptr.h" +#include "messages.g.h" #include "photo_handler.h" #include "preview_handler.h" #include "record_handler.h" @@ -300,7 +304,8 @@ void CaptureControllerImpl::ResetCaptureController() { bool CaptureControllerImpl::InitCaptureDevice( flutter::TextureRegistrar* texture_registrar, const std::string& device_id, - const PlatformMediaSettings& media_settings) { + const PlatformMediaSettings& media_settings, + std::unique_ptr task_runner) { assert(capture_controller_listener_); if (IsInitialized()) { @@ -317,6 +322,7 @@ bool CaptureControllerImpl::InitCaptureDevice( media_settings_ = media_settings; texture_registrar_ = texture_registrar; video_device_id_ = device_id; + task_runner_ = task_runner; // MFStartup must be called before using Media Foundation. if (!media_foundation_started_) { @@ -555,6 +561,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 @@ -860,9 +876,39 @@ void CaptureControllerImpl::OnRecordStopped(CameraResult result, // Implements CaptureEngineObserver::UpdateBuffer. bool CaptureControllerImpl::UpdateBuffer(uint8_t* buffer, uint32_t data_length) { + using flutter::EncodableValue, flutter::EncodableMap; + if (!texture_handler_) { return false; } + if (image_stream_sink_) { + // Create a strongly-typed frame data object using Pigeon + flutter::EncodableList data_list; + std::vector buffer_data(buffer, buffer + data_length); + for (uint8_t byte : buffer_data) { + data_list.push_back(flutter::EncodableValue(static_cast(byte))); + } + + PlatformFrameData frame_data(data_list, + static_cast(preview_frame_width_), + static_cast(preview_frame_height_), + static_cast(data_length)); + + // Use CameraApi::GetCodec() to properly encode the Pigeon-defined class + // This ensures the frame data is serialized using the same codec as other + // CameraApi messages + flutter::EncodableValue encoded_frame = + CameraApi::GetCodec().EncodeMessage(frame_data.ToEncodableList()); + + task_runner_->EnqueueTask([weak_sink = std::weak_ptr(image_stream_sink_), + encoded_frame = std::move(encoded_frame)]() { + std::shared_ptr> sink = + weak_sink.lock(); + if (sink) { + sink->Success(encoded_frame); + } + }); + } 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 2280cb3b93f..497e973f15e 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 @@ -24,6 +25,7 @@ #include "photo_handler.h" #include "preview_handler.h" #include "record_handler.h" +#include "task_runner.h" #include "texture_handler.h" namespace camera_windows { @@ -67,9 +69,12 @@ class CaptureController { // device_id: A string that holds information of camera device id to // be captured. // media_settings: Settings controlling capture behavior. - virtual bool InitCaptureDevice( - TextureRegistrar* texture_registrar, const std::string& device_id, - const PlatformMediaSettings& media_settings) = 0; + // task_runner: A task runner for posting image frames via a platform + // thread. + virtual bool InitCaptureDevice(TextureRegistrar* texture_registrar, + const std::string& device_id, + const PlatformMediaSettings& media_settings, + std::unique_ptr task_runner) = 0; // Returns preview frame width virtual uint32_t GetPreviewWidth() const = 0; @@ -92,6 +97,16 @@ 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; + + // Returns true if an image stream is currently running. + virtual bool IsStreaming() const = 0; + // Captures a still photo. virtual void TakePicture(const std::string& file_path) = 0; }; @@ -117,7 +132,8 @@ class CaptureControllerImpl : public CaptureController, // CaptureController bool InitCaptureDevice(TextureRegistrar* texture_registrar, const std::string& device_id, - const PlatformMediaSettings& media_settings) override; + const PlatformMediaSettings& media_settings, + std::unique_ptr task_runner) override; uint32_t GetPreviewWidth() const override { return preview_frame_width_; } uint32_t GetPreviewHeight() const override { return preview_frame_height_; } void StartPreview() override; @@ -125,6 +141,13 @@ 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; + bool IsStreaming() const override { + return static_cast(image_stream_sink_); + } void TakePicture(const std::string& file_path) override; // CaptureEngineObserver @@ -219,8 +242,10 @@ class CaptureControllerImpl : public CaptureController, std::unique_ptr preview_handler_; std::unique_ptr photo_handler_; std::unique_ptr texture_handler_; + std::unique_ptr task_runner_; + std::shared_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 541ca97d018..a6cddd5b125 100644 --- a/packages/camera/camera_windows/windows/messages.g.cpp +++ b/packages/camera/camera_windows/windows/messages.g.cpp @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.6.0), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon #undef _HAS_EXCEPTIONS @@ -167,6 +167,48 @@ PlatformSize PlatformSize::FromEncodableList(const EncodableList& list) { return decoded; } +// PlatformFrameData + +PlatformFrameData::PlatformFrameData(const EncodableList& data, int64_t width, + int64_t height, int64_t length) + : data_(data), width_(width), height_(height), length_(length) {} + +const EncodableList& PlatformFrameData::data() const { return data_; } + +void PlatformFrameData::set_data(const EncodableList& value_arg) { + data_ = value_arg; +} + +int64_t PlatformFrameData::width() const { return width_; } + +void PlatformFrameData::set_width(int64_t value_arg) { width_ = value_arg; } + +int64_t PlatformFrameData::height() const { return height_; } + +void PlatformFrameData::set_height(int64_t value_arg) { height_ = value_arg; } + +int64_t PlatformFrameData::length() const { return length_; } + +void PlatformFrameData::set_length(int64_t value_arg) { length_ = value_arg; } + +EncodableList PlatformFrameData::ToEncodableList() const { + EncodableList list; + list.reserve(4); + list.push_back(EncodableValue(data_)); + list.push_back(EncodableValue(width_)); + list.push_back(EncodableValue(height_)); + list.push_back(EncodableValue(length_)); + return list; +} + +PlatformFrameData PlatformFrameData::FromEncodableList( + const EncodableList& list) { + PlatformFrameData decoded( + std::get(list[0]), std::get(list[1]), + std::get(list[2]), std::get(list[3])); + return decoded; +} + PigeonInternalCodecSerializer::PigeonInternalCodecSerializer() {} EncodableValue PigeonInternalCodecSerializer::ReadValueOfType( @@ -189,6 +231,10 @@ EncodableValue PigeonInternalCodecSerializer::ReadValueOfType( return CustomEncodableValue(PlatformSize::FromEncodableList( std::get(ReadValue(stream)))); } + case 132: { + return CustomEncodableValue(PlatformFrameData::FromEncodableList( + std::get(ReadValue(stream)))); + } default: return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); } @@ -221,6 +267,13 @@ void PigeonInternalCodecSerializer::WriteValue( stream); return; } + if (custom_value->type() == typeid(PlatformFrameData)) { + stream->WriteByte(132); + WriteValue(EncodableValue(std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } } flutter::StandardCodecSerializer::WriteValue(value, stream); } @@ -498,6 +551,41 @@ 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(); + ErrorOr output = + api->StartImageStream(camera_id_arg); + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + 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 a85c692e5b0..462947ca48d 100644 --- a/packages/camera/camera_windows/windows/messages.g.h +++ b/packages/camera/camera_windows/windows/messages.g.h @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.6.0), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon #ifndef PIGEON_MESSAGES_G_H_ @@ -140,6 +140,40 @@ class PlatformSize { double height_; }; +// A representation of frame data from the camera preview stream. +// +// Generated class from Pigeon that represents data sent in messages. +class PlatformFrameData { + public: + // Constructs an object setting all fields. + explicit PlatformFrameData(const flutter::EncodableList& data, int64_t width, + int64_t height, int64_t length); + + const flutter::EncodableList& data() const; + void set_data(const flutter::EncodableList& value_arg); + + int64_t width() const; + void set_width(int64_t value_arg); + + int64_t height() const; + void set_height(int64_t value_arg); + + int64_t length() const; + void set_length(int64_t value_arg); + + private: + static PlatformFrameData FromEncodableList( + const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class CameraApi; + friend class CameraEventApi; + friend class PigeonInternalCodecSerializer; + flutter::EncodableList data_; + int64_t width_; + int64_t height_; + int64_t length_; +}; + class PigeonInternalCodecSerializer : public flutter::StandardCodecSerializer { public: PigeonInternalCodecSerializer(); @@ -189,6 +223,10 @@ class CameraApi { virtual void StopVideoRecording( int64_t camera_id, std::function reply)> result) = 0; + // Starts the image stream for the given camera. + // Returns the name of the [EventChannel] used to deliver the images. + // Cancelling the subscription to the channel stops the capture. + virtual ErrorOr StartImageStream(int64_t camera_id) = 0; // Starts the preview stream for the given camera. virtual void PausePreview( int64_t camera_id, diff --git a/packages/camera/camera_windows/windows/task_runner.h b/packages/camera/camera_windows/windows/task_runner.h new file mode 100644 index 00000000000..fd51991d808 --- /dev/null +++ b/packages/camera/camera_windows/windows/task_runner.h @@ -0,0 +1,24 @@ +// 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. + +#ifndef PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_TASK_RUNNER_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_TASK_RUNNER_H_ + +#include + +namespace camera_windows { + +/// A closure that can be run as a task. +using TaskClosure = std::function; + +/// A task queue for scheduling functions to be executed in a different thread. +class TaskRunner { + public: + /// Schedule a function/closure to be executed as soon as possible. + virtual void EnqueueTask(TaskClosure task) = 0; +}; + +} // namespace camera_windows + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_TASK_RUNNER_H_ diff --git a/packages/camera/camera_windows/windows/task_runner_window.cpp b/packages/camera/camera_windows/windows/task_runner_window.cpp new file mode 100644 index 00000000000..2783a1e1332 --- /dev/null +++ b/packages/camera/camera_windows/windows/task_runner_window.cpp @@ -0,0 +1,105 @@ +// 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. + +#include "task_runner_window.h" + +#include +#include + +namespace camera_windows { + +TaskRunnerWindow::TaskRunnerWindow() { + WNDCLASS window_class = RegisterWindowClass(); + window_handle_.reset(CreateWindowEx(0, window_class.lpszClassName, L"", 0, 0, + 0, 0, 0, HWND_MESSAGE, nullptr, + window_class.hInstance, nullptr)); + + if (window_handle_) { + SetWindowLongPtr(window_handle_.get(), GWLP_USERDATA, + reinterpret_cast(this)); + } else { + auto error = GetLastError(); + LPWSTR message = nullptr; + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&message), 0, NULL); + OutputDebugString(message); + LocalFree(message); + } +} + +TaskRunnerWindow::~TaskRunnerWindow() { + UnregisterClass(window_class_name_.c_str(), nullptr); +} + +void TaskRunnerWindow::EnqueueTask(TaskClosure task) { + { + std::lock_guard lock(tasks_mutex_); + tasks_.push(task); + } + if (!PostMessage(window_handle_.get(), WM_NULL, 0, 0)) { + DWORD error_code = GetLastError(); + std::cerr << "Failed to post message to main thread; error_code: " + << error_code << std::endl; + } +} + +void TaskRunnerWindow::ProcessTasks() { + // Even though it would usually be sufficient to process only a single task + // whenever a message is received, if the message queue happens to be full, + // there may have been fewer messages received than tasks in the queue. + for (;;) { + TaskClosure task; + { + std::lock_guard lock(tasks_mutex_); + if (tasks_.empty()) break; + task = tasks_.front(); + tasks_.pop(); + } + task(); + } +} + +WNDCLASS TaskRunnerWindow::RegisterWindowClass() { + window_class_name_ = L"FlutterPluginCameraWindowsTaskRunnerWindow"; + + WNDCLASS window_class{}; + window_class.hCursor = nullptr; + window_class.lpszClassName = window_class_name_.c_str(); + window_class.style = 0; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = nullptr; + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = WndProc; + RegisterClass(&window_class); + return window_class; +} + +LRESULT +TaskRunnerWindow::HandleMessage(UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_NULL: + ProcessTasks(); + return 0; + } + return DefWindowProcW(window_handle_.get(), message, wparam, lparam); +} + +LRESULT TaskRunnerWindow::WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (auto* that = reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA))) { + return that->HandleMessage(message, wparam, lparam); + } else { + return DefWindowProc(window, message, wparam, lparam); + } +} + +} // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/task_runner_window.h b/packages/camera/camera_windows/windows/task_runner_window.h new file mode 100644 index 00000000000..1ab220ffe0d --- /dev/null +++ b/packages/camera/camera_windows/windows/task_runner_window.h @@ -0,0 +1,61 @@ +// 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. + +#ifndef PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_TASK_RUNNER_WINDOW_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_TASK_RUNNER_WINDOW_H_ + +#include + +#include +#include +#include +#include +#include + +#include "task_runner.h" +#include "unique_hwnd.h" + +namespace camera_windows { + +/// Hidden HWND responsible for processing camera tasks on main thread. +/// Adapted from Flutter Engine, see: +/// https://github.com/flutter/flutter/issues/134346#issuecomment-2141023146 +/// and: +/// https://github.com/flutter/engine/blob/d7c0bcfe7a30408b0722c9d47d8b0b1e4cdb9c81/shell/platform/windows/task_runner_window.h +class TaskRunnerWindow : public TaskRunner { + public: + virtual void EnqueueTask(TaskClosure task); + + /// Creates a hidden window and registers a callback that runs + /// enqueued tasks whenever a message is received. + TaskRunnerWindow(); + + /// Destroys the hidden window. + ~TaskRunnerWindow(); + + private: + void ProcessTasks(); + + WNDCLASS RegisterWindowClass(); + + LRESULT + HandleMessage(UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept; + + static LRESULT CALLBACK WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + UniqueHWND window_handle_; + std::wstring window_class_name_; + std::mutex tasks_mutex_; + std::queue tasks_; + + // Prevent copying. + TaskRunnerWindow(TaskRunnerWindow const&) = delete; + TaskRunnerWindow& operator=(TaskRunnerWindow const&) = delete; +}; +} // namespace camera_windows + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_TASK_RUNNER_WINDOW_H_ 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 2680ae26c0e..0b2c2c392a7 100644 --- a/packages/camera/camera_windows/windows/test/camera_plugin_test.cpp +++ b/packages/camera/camera_windows/windows/test/camera_plugin_test.cpp @@ -27,7 +27,9 @@ using ::testing::_; using ::testing::DoAll; using ::testing::EndsWith; using ::testing::Eq; +using ::testing::Optional; using ::testing::Pointee; +using ::testing::Property; using ::testing::Return; void MockInitCamera(MockCamera* camera, bool success) { @@ -52,7 +54,8 @@ void MockInitCamera(MockCamera* camera, bool success) { .Times(1) .WillOnce([camera, success](flutter::TextureRegistrar* texture_registrar, flutter::BinaryMessenger* messenger, - const PlatformMediaSettings& media_settings) { + const PlatformMediaSettings& media_settings, + std::unique_ptr task_runner) { assert(camera->pending_int_result_); if (success) { camera->pending_int_result_(1); @@ -351,6 +354,82 @@ 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(); + + std::unique_ptr texture_registrar = + std::make_unique(); + std::unique_ptr messenger = + 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, GetCaptureController) + .Times(1) + .WillOnce( + [cam = camera.get()]() { return cam->capture_controller_.get(); }); + + EXPECT_CALL(*capture_controller, IsStreaming).WillRepeatedly(Return(false)); + + EXPECT_CALL(*messenger, SetMessageHandler).Times(1); + + camera->camera_id_ = mock_camera_id; + camera->capture_controller_ = std::move(capture_controller); + + MockCameraPlugin plugin(texture_registrar.get(), messenger.get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + auto result = plugin.StartImageStream(mock_camera_id); + EXPECT_FALSE(result.has_error()); +} + +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)); + + auto result = plugin.StartImageStream(missing_camera_id); + EXPECT_TRUE(result.has_error()); +} + TEST(CameraPlugin, InitializeHandlerErrorOnInvalidCameraId) { int64_t mock_camera_id = 1234; int64_t missing_camera_id = 5678; diff --git a/packages/camera/camera_windows/windows/test/camera_test.cpp b/packages/camera/camera_windows/windows/test/camera_test.cpp index 3aa199345c2..19f87ce130c 100644 --- a/packages/camera/camera_windows/windows/test/camera_test.cpp +++ b/packages/camera/camera_windows/windows/test/camera_test.cpp @@ -52,10 +52,11 @@ TEST(Camera, InitCameraCreatesCaptureController) { PlatformMediaSettings media_settings(PlatformResolutionPreset::kMax, false); // Init camera with mock capture controller factory - bool result = camera->InitCamera( - std::move(capture_controller_factory), - std::make_unique().get(), - std::make_unique().get(), media_settings); + bool result = + camera->InitCamera(std::move(capture_controller_factory), + std::make_unique().get(), + std::make_unique().get(), + media_settings, std::make_shared()); EXPECT_TRUE(result); EXPECT_TRUE(camera->GetCaptureController() != nullptr); } @@ -84,10 +85,11 @@ TEST(Camera, InitCameraReportsFailure) { PlatformMediaSettings media_settings(PlatformResolutionPreset::kMax, false); // Init camera with mock capture controller factory - bool result = camera->InitCamera( - std::move(capture_controller_factory), - std::make_unique().get(), - std::make_unique().get(), media_settings); + bool result = + camera->InitCamera(std::move(capture_controller_factory), + std::make_unique().get(), + std::make_unique().get(), + media_settings, std::make_shared()); EXPECT_FALSE(result); EXPECT_TRUE(camera->GetCaptureController() != nullptr); } diff --git a/packages/camera/camera_windows/windows/test/capture_controller_test.cpp b/packages/camera/camera_windows/windows/test/capture_controller_test.cpp index 98e775845a6..67ce473689d 100644 --- a/packages/camera/camera_windows/windows/test/capture_controller_test.cpp +++ b/packages/camera/camera_windows/windows/test/capture_controller_test.cpp @@ -33,6 +33,7 @@ void MockInitCaptureController( CaptureControllerImpl* capture_controller, MockTextureRegistrar* texture_registrar, MockCaptureEngine* engine, MockCamera* camera, int64_t mock_texture_id, + std::unique_ptr task_runner, const PlatformMediaSettings media_settings = PlatformMediaSettings(PlatformResolutionPreset::kMax, true)) { ComPtr video_source = new MockMediaSource(); @@ -62,7 +63,7 @@ void MockInitCaptureController( EXPECT_CALL(*engine, Initialize).Times(1); bool result = capture_controller->InitCaptureDevice( - texture_registrar, MOCK_DEVICE_ID, media_settings); + texture_registrar, MOCK_DEVICE_ID, media_settings, task_runner); EXPECT_TRUE(result); @@ -237,12 +238,14 @@ TEST(CaptureController, std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Init capture controller with mocks and tests MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); capture_controller = nullptr; camera = nullptr; @@ -258,19 +261,21 @@ TEST(CaptureController, InitCaptureEngineCanOnlyBeCalledOnce) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Init capture controller once with mocks and tests MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); // Init capture controller a second time. EXPECT_CALL(*camera, OnCreateCaptureEngineFailed).Times(1); bool result = capture_controller->InitCaptureDevice( texture_registrar.get(), MOCK_DEVICE_ID, - PlatformMediaSettings(PlatformResolutionPreset::kMax, true)); + PlatformMediaSettings(PlatformResolutionPreset::kMax, true), task_runner); EXPECT_FALSE(result); @@ -288,6 +293,7 @@ TEST(CaptureController, InitCaptureEngineReportsFailure) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); ComPtr video_source = new MockMediaSource(); ComPtr audio_source = new MockMediaSource(); @@ -312,7 +318,7 @@ TEST(CaptureController, InitCaptureEngineReportsFailure) { bool result = capture_controller->InitCaptureDevice( texture_registrar.get(), MOCK_DEVICE_ID, - PlatformMediaSettings(PlatformResolutionPreset::kMax, true)); + PlatformMediaSettings(PlatformResolutionPreset::kMax, true), task_runner); EXPECT_FALSE(result); EXPECT_FALSE(engine->initialized_); @@ -331,6 +337,7 @@ TEST(CaptureController, InitCaptureEngineReportsAccessDenied) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); ComPtr video_source = new MockMediaSource(); ComPtr audio_source = new MockMediaSource(); @@ -357,7 +364,7 @@ TEST(CaptureController, InitCaptureEngineReportsAccessDenied) { bool result = capture_controller->InitCaptureDevice( texture_registrar.get(), MOCK_DEVICE_ID, - PlatformMediaSettings(PlatformResolutionPreset::kMax, true)); + PlatformMediaSettings(PlatformResolutionPreset::kMax, true), task_runner); EXPECT_FALSE(result); EXPECT_FALSE(engine->initialized_); @@ -376,11 +383,13 @@ TEST(CaptureController, ReportsInitializedErrorEvent) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); EXPECT_CALL(*camera, OnCreateCaptureEngineFailed( Eq(CameraResult::kError), @@ -405,11 +414,13 @@ TEST(CaptureController, ReportsInitializedAccessDeniedEvent) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); EXPECT_CALL(*camera, OnCreateCaptureEngineFailed( Eq(CameraResult::kAccessDenied), @@ -434,11 +445,12 @@ TEST(CaptureController, ReportsCaptureEngineErrorEvent) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); - + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); EXPECT_CALL(*(camera.get()), OnCaptureError(Eq(CameraResult::kError), Eq("Unspecified error"))) @@ -461,11 +473,12 @@ TEST(CaptureController, ReportsCaptureEngineAccessDeniedEvent) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); - + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); EXPECT_CALL(*(camera.get()), OnCaptureError(Eq(CameraResult::kAccessDenied), Eq("Access is denied."))) @@ -488,12 +501,13 @@ TEST(CaptureController, StartPreviewStartsProcessingSamples) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); - + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr preview_sink = new MockCapturePreviewSink(); @@ -575,12 +589,14 @@ TEST(CaptureController, ReportsStartPreviewError) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); @@ -616,12 +632,14 @@ TEST(CaptureController, IgnoresStartPreviewErrorEvent) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); EXPECT_CALL(*camera, OnStartPreviewFailed).Times(0); EXPECT_CALL(*camera, OnCreateCaptureEngineSucceeded).Times(0); @@ -643,12 +661,14 @@ TEST(CaptureController, ReportsStartPreviewAccessDenied) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); @@ -682,12 +702,14 @@ TEST(CaptureController, StartRecordSuccess) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); @@ -736,6 +758,7 @@ TEST(CaptureController, StartRecordWithSettingsSuccess) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; @@ -751,7 +774,7 @@ TEST(CaptureController, StartRecordWithSettingsSuccess) { // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), engine.Get(), camera.get(), mock_texture_id, - media_settings); + task_runner, media_settings); ComPtr capture_source = new MockCaptureSource(); @@ -823,12 +846,14 @@ TEST(CaptureController, ReportsStartRecordError) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); @@ -864,12 +889,14 @@ TEST(CaptureController, ReportsStartRecordAccessDenied) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); @@ -905,12 +932,14 @@ TEST(CaptureController, ReportsStartRecordErrorEvent) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); @@ -970,12 +999,14 @@ TEST(CaptureController, ReportsStartRecordAccessDeniedEvent) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); @@ -1035,12 +1066,14 @@ TEST(CaptureController, StopRecordSuccess) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); @@ -1080,12 +1113,14 @@ TEST(CaptureController, ReportsStopRecordError) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); @@ -1124,12 +1159,14 @@ TEST(CaptureController, ReportsStopRecordAccessDenied) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); @@ -1168,12 +1205,14 @@ TEST(CaptureController, ReportsStopRecordErrorEvent) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); @@ -1209,12 +1248,14 @@ TEST(CaptureController, ReportsStopRecordAccessDeniedEvent) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); @@ -1250,12 +1291,14 @@ TEST(CaptureController, TakePictureSuccess) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to take picture MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); @@ -1292,12 +1335,14 @@ TEST(CaptureController, ReportsTakePictureError) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to take picture MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); @@ -1334,12 +1379,14 @@ TEST(CaptureController, ReportsTakePictureAccessDenied) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to take picture MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); @@ -1378,12 +1425,14 @@ TEST(CaptureController, ReportsPhotoTakenErrorEvent) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to take picture MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); @@ -1423,12 +1472,14 @@ TEST(CaptureController, ReportsPhotoTakenAccessDeniedEvent) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to take picture MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr capture_source = new MockCaptureSource(); @@ -1468,12 +1519,14 @@ TEST(CaptureController, PauseResumePreviewSuccess) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); ComPtr preview_sink = new MockCapturePreviewSink(); @@ -1505,11 +1558,14 @@ TEST(CaptureController, PausePreviewFailsIfPreviewNotStarted) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); + int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); // Pause preview fails if not started EXPECT_CALL(*camera, OnPausePreviewFailed(Eq(CameraResult::kError), @@ -1532,11 +1588,14 @@ TEST(CaptureController, ResumePreviewFailsIfPreviewNotStarted) { std::make_unique(camera.get()); std::unique_ptr texture_registrar = std::make_unique(); + auto task_runner = std::make_shared(); + int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), - engine.Get(), camera.get(), mock_texture_id); + engine.Get(), camera.get(), mock_texture_id, + task_runner); // Resume preview fails if not started. EXPECT_CALL(*camera, OnResumePreviewFailed(Eq(CameraResult::kError), @@ -1551,5 +1610,69 @@ TEST(CaptureController, ResumePreviewFailsIfPreviewNotStarted) { camera = nullptr; } +TEST(CaptureController, FrameIsDeliveredAfterStartImageStream) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + auto task_runner = std::make_shared(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to start preview + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id, + task_runner); + + auto sink = std::make_unique(); + + // Expect the sink to receive a frame after UpdateBuffer. + EXPECT_CALL(*sink, SuccessInternal(_)); + + capture_controller->StartImageStream(std::move(sink)); + capture_controller->UpdateBuffer(nullptr, 0); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; +} + +TEST(CaptureController, NoFramesAreDeliveredAfterStopImageStream) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + auto task_runner = std::make_shared(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to start preview + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id, + task_runner); + + auto sink = std::make_unique(); + + EXPECT_CALL(*sink, SuccessInternal(_)).Times(0); + + capture_controller->StartImageStream(std::move(sink)); + capture_controller->StopImageStream(); + + // Stream is stopped, the sink should not receive any frames. + capture_controller->UpdateBuffer(nullptr, 0); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; +} + } // namespace test } // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/test/mocks.h b/packages/camera/camera_windows/windows/test/mocks.h index 4a05ef82296..745b744d74f 100644 --- a/packages/camera/camera_windows/windows/test/mocks.h +++ b/packages/camera/camera_windows/windows/test/mocks.h @@ -203,7 +203,8 @@ class MockCamera : public Camera { MOCK_METHOD(bool, InitCamera, (flutter::TextureRegistrar * texture_registrar, flutter::BinaryMessenger* messenger, - const PlatformMediaSettings& media_settings), + const PlatformMediaSettings& media_settings, + std::unique_ptr task_runner), (override)); std::unique_ptr capture_controller_; @@ -236,7 +237,8 @@ class MockCaptureController : public CaptureController { MOCK_METHOD(bool, InitCaptureDevice, (flutter::TextureRegistrar * texture_registrar, const std::string& device_id, - const PlatformMediaSettings& media_settings), + const PlatformMediaSettings& media_settings, + std::unique_ptr task_runner), (override)); MOCK_METHOD(uint32_t, GetPreviewWidth, (), (const override)); @@ -248,6 +250,12 @@ 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(bool, IsStreaming, (), (const override)); MOCK_METHOD(void, TakePicture, (const std::string& file_path), (override)); }; @@ -1021,6 +1029,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, ()); @@ -1069,6 +1080,28 @@ class MockCaptureEngine : public IMFCaptureEngine { bool initialized_ = false; }; +class MockTaskRunner : public TaskRunner { + public: + MOCK_METHOD(void, EnqueueTask, (TaskClosure), (override)); +}; + +class ImmediateTaskRunner : public TaskRunner { + public: + virtual void EnqueueTask(TaskClosure task) override { task(); } +}; + +// 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 ">" #define MOCK_INVALID_CAMERA_NAME "invalid_camera_name" diff --git a/packages/camera/camera_windows/windows/test/task_runner_window_test.cpp b/packages/camera/camera_windows/windows/test/task_runner_window_test.cpp new file mode 100644 index 00000000000..67382bd8b76 --- /dev/null +++ b/packages/camera/camera_windows/windows/test/task_runner_window_test.cpp @@ -0,0 +1,34 @@ +// 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. + +#include "task_runner_window.h" + +#include + +#include + +namespace camera_windows { +namespace test { + +static void ProcessOneMessage() { + MSG msg; + GetMessage(&msg, nullptr, 0, 0); + TranslateMessage(&msg); + DispatchMessage(&msg); +} + +TEST(TaskRunnerWindow, EnqueuedTaskIsExecuted) { + TaskRunnerWindow task_runner; + + volatile bool task_completed = false; + + task_runner.EnqueueTask([&task_completed]() { task_completed = true; }); + + ProcessOneMessage(); + + EXPECT_TRUE(task_completed); +} + +} // namespace test +} // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/unique_hwnd.h b/packages/camera/camera_windows/windows/unique_hwnd.h new file mode 100644 index 00000000000..231c523bab7 --- /dev/null +++ b/packages/camera/camera_windows/windows/unique_hwnd.h @@ -0,0 +1,28 @@ +// 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. + +#ifndef PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_UNIQUE_HWND_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_UNIQUE_HWND_H_ + +#include + +#include + +namespace camera_windows { + +struct UniqueHWNDDeleter { + typedef HWND pointer; + + void operator()(HWND hwnd) { + if (hwnd) { + DestroyWindow(hwnd); + } + } +}; + +typedef std::unique_ptr UniqueHWND; + +} // namespace camera_windows + +#endif PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_UNIQUE_HWND_H_