diff --git a/packages/camera/.gitignore b/packages/camera/.gitignore new file mode 100644 index 0000000..e9dc58d --- /dev/null +++ b/packages/camera/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md new file mode 100644 index 0000000..30c14fa --- /dev/null +++ b/packages/camera/CHANGELOG.md @@ -0,0 +1,2 @@ +## 0.1.0 +* First draft version. diff --git a/packages/camera/LICENSE b/packages/camera/LICENSE new file mode 100644 index 0000000..454d129 --- /dev/null +++ b/packages/camera/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2021 Sony Group Corporation. All rights reserved. +Copyright (c) 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the names of the copyright holders nor the names of the + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/camera/README.md b/packages/camera/README.md new file mode 100644 index 0000000..4ecb235 --- /dev/null +++ b/packages/camera/README.md @@ -0,0 +1,47 @@ +# camera_elinux + +The implementation of the camera plugin for flutter elinux. APIs are designed to be API compatible with the the official [`camera`](https://github.com/flutter/plugins/tree/master/packages/camera). + +## Required libraries + +This plugin uses [GStreamer](https://gstreamer.freedesktop.org/) internally. + +```Shell +$ sudo apt install libglib2.0-dev +$ sudo apt install libgstreamer1.0-dev +# Install as needed. +$ sudo apt libgstreamer-plugins-base1.0-dev \ + gstreamer1.0-plugins-base gstreamer1.0-plugins-good \ + gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav +``` + +## Usage + +### pubspec.yaml +```yaml +dependencies: + camera: ^0.8.1+7 + camera_elinux: + git: + url: https://github.com/sony/flutter-elinux-plugins.git + path: packages/camera + ref: main +``` + +### Source code +Import `camera` in your Dart code: +```dart +import 'package:camera/camera.dart'; +``` + +## Troubleshoting + +If you get the following error: +```Shell +Wrong JPEG library version: library is 62, caller expects 80 +``` + +, try the following: +```Shell +sudo mv /usr/lib/x86_64-linux-gnu/gstreamer-1.0/libgstjpeg.so /usr/lib/x86_64-linux-gnu/gstreamer-1.0/libgstjpeg.so.org +``` diff --git a/packages/camera/elinux/.gitignore b/packages/camera/elinux/.gitignore new file mode 100644 index 0000000..f68e1be --- /dev/null +++ b/packages/camera/elinux/.gitignore @@ -0,0 +1 @@ +flutter/ diff --git a/packages/camera/elinux/CMakeLists.txt b/packages/camera/elinux/CMakeLists.txt new file mode 100644 index 0000000..0369976 --- /dev/null +++ b/packages/camera/elinux/CMakeLists.txt @@ -0,0 +1,47 @@ +cmake_minimum_required(VERSION 3.15) +set(PROJECT_NAME "camera_elinux") +project(${PROJECT_NAME} LANGUAGES CXX) + +# This value is used when generating builds using this plugin, so it must +# not be changed +set(PLUGIN_NAME "camera_elinux_plugin") + +find_package(PkgConfig) +pkg_check_modules(GLIB REQUIRED glib-2.0) +pkg_check_modules(GSTREAMER REQUIRED gstreamer-1.0) + +add_library(${PLUGIN_NAME} SHARED + "camera_elinux_plugin.cc" + "gst_camera.cc" + "types/exposure_mode.cc" + "types/focus_mode.cc" + "types/orientation.cc" + "method_channel/method_channel_camera.cc" + "method_channel/method_channel_device.cc" +) +apply_standard_settings(${PLUGIN_NAME}) +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) + +target_include_directories(${PLUGIN_NAME} + PRIVATE + "./" + ${GLIB_INCLUDE_DIRS} + ${GSTREAMER_INCLUDE_DIRS} +) + +target_link_libraries(${PLUGIN_NAME} + PRIVATE + ${GLIB_LIBRARIES} + ${GSTREAMER_LIBRARIES} +) + +# List of absolute paths to libraries that should be bundled with the plugin +set(camera_elinux_bundled_libraries + "" + PARENT_SCOPE +) diff --git a/packages/camera/elinux/camera_elinux_plugin.cc b/packages/camera/elinux/camera_elinux_plugin.cc new file mode 100644 index 0000000..8892b04 --- /dev/null +++ b/packages/camera/elinux/camera_elinux_plugin.cc @@ -0,0 +1,340 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "include/camera_elinux/camera_elinux_plugin.h" + +#include +#include +#include + +#include + +#include "camera_stream_handler_impl.h" +#include "events/camera_initialized_event.h" +#include "gst_camera.h" +#include "messages/messages.h" +#include "method_channel/method_channel_camera.h" +#include "method_channel/method_channel_device.h" + +namespace { +constexpr char kCameraChannelName[] = "plugins.flutter.io/camera"; + +constexpr char kCameraChannelApiAvailableCameras[] = "availableCameras"; +constexpr char kCameraChannelApiCreate[] = "create"; +constexpr char kCameraChannelApiInitialize[] = "initialize"; +constexpr char kCameraChannelApiTakePicture[] = "takePicture"; +constexpr char kCameraChannelApiPrepareForVideoRecording[] = + "prepareForVideoRecording"; +constexpr char kCameraChannelApiStartVideoRecording[] = "startVideoRecording"; +constexpr char kCameraChannelApiStopVideoRecording[] = "stopVideoRecording"; +constexpr char kCameraChannelApiPauseVideoRecording[] = "pauseVideoRecording"; +constexpr char kCameraChannelApiResumeVideoRecording[] = "resumeVideoRecording"; +constexpr char kCameraChannelApiSetFlashMode[] = "setFlashMode"; +constexpr char kCameraChannelApiSetExposureMode[] = "setExposureMode"; +constexpr char kCameraChannelApiSetExposurePoint[] = "setExposurePoint"; +constexpr char kCameraChannelApiGetMinExposureOffset[] = "getMinExposureOffset"; +constexpr char kCameraChannelApiGetMaxExposureOffset[] = "getMaxExposureOffset"; +constexpr char kCameraChannelApiGetExposureOffsetStepSize[] = + "getExposureOffsetStepSize"; +constexpr char kCameraChannelApiSetExposureOffset[] = "setExposureOffset"; +constexpr char kCameraChannelApiSetFocusMode[] = "setFocusMode"; +constexpr char kCameraChannelApiSetFocusPoint[] = "setFocusPoint"; +constexpr char kCameraChannelApiStartImageStream[] = "startImageStream"; +constexpr char kCameraChannelApiStopImageStream[] = "stopImageStream"; +constexpr char kCameraChannelApiGetMaxZoomLevel[] = "getMaxZoomLevel"; +constexpr char kCameraChannelApiGetMinZoomLevel[] = "getMinZoomLevel"; +constexpr char kCameraChannelApiSetZoomLevel[] = "setZoomLevel"; +constexpr char kCameraChannelApiLockCaptureOrientation[] = + "lockCaptureOrientation"; +constexpr char kCameraChannelApiUnlockCaptureOrientation[] = + "unlockCaptureOrientation"; +constexpr char kCameraChannelApiDispose[] = "dispose"; + +class CameraPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar); + + CameraPlugin(flutter::PluginRegistrar* plugin_registrar, + flutter::TextureRegistrar* texture_registrar) + : plugin_registrar_(plugin_registrar), + texture_registrar_(texture_registrar) { + GstCamera::GstLibraryLoad(); + } + virtual ~CameraPlugin() { + if (camera_) { + camera_->Stop(); + camera_ = nullptr; + } + GstCamera::GstLibraryUnload(); + } + + private: + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + + void HandleAvailableCamerasCall( + std::unique_ptr> result); + void HandleCreateCall( + const flutter::EncodableValue* message, + std::unique_ptr> result); + void HandleInitializeCall( + const flutter::EncodableValue* message, + std::unique_ptr> result); + void HandleGetMaxZoomLevelCall( + const flutter::EncodableValue* message, + std::unique_ptr> result); + void HandleGetMinZoomLevelCall( + const flutter::EncodableValue* message, + std::unique_ptr> result); + void HandleSetZoomLevelCall( + const flutter::EncodableValue* message, + std::unique_ptr> result); + void HandleLockCaptureOrientationCall( + const flutter::EncodableValue* message, + std::unique_ptr> result); + void HandleDisposeCall( + const flutter::EncodableValue* message, + std::unique_ptr> result); + + flutter::PluginRegistrar* plugin_registrar_; + flutter::TextureRegistrar* texture_registrar_; + + std::unique_ptr buffer_; + std::unique_ptr texture_; + std::unique_ptr camera_ = nullptr; + int64_t texture_id_; + std::unique_ptr method_channel_camera_; + std::unique_ptr method_channel_device_; +}; + +// static +void CameraPlugin::RegisterWithRegistrar(flutter::PluginRegistrar* registrar) { + auto plugin = + std::make_unique(registrar, registrar->texture_registrar()); + auto channel = + std::make_unique>( + registrar->messenger(), kCameraChannelName, + &flutter::StandardMethodCodec::GetInstance()); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto& call, auto result) { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); +} + +void CameraPlugin::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + const std::string& method_name = method_call.method_name(); + + if (!method_name.compare(kCameraChannelApiAvailableCameras)) { + HandleAvailableCamerasCall(std::move(result)); + } else if (!method_name.compare(kCameraChannelApiCreate)) { + HandleCreateCall(method_call.arguments(), std::move(result)); + } else if (!method_name.compare(kCameraChannelApiInitialize)) { + HandleInitializeCall(method_call.arguments(), std::move(result)); + } else if (!method_name.compare(kCameraChannelApiTakePicture)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiPrepareForVideoRecording)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiStartVideoRecording)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiStopVideoRecording)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiPauseVideoRecording)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiResumeVideoRecording)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiSetFlashMode)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiSetExposureMode)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiSetExposurePoint)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiGetMinExposureOffset)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiGetMaxExposureOffset)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiGetExposureOffsetStepSize)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiSetExposureOffset)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiSetFocusMode)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiSetFocusPoint)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiStartImageStream)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiStopImageStream)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiGetMaxZoomLevel)) { + HandleGetMaxZoomLevelCall(method_call.arguments(), std::move(result)); + } else if (!method_name.compare(kCameraChannelApiGetMinZoomLevel)) { + HandleGetMinZoomLevelCall(method_call.arguments(), std::move(result)); + } else if (!method_name.compare(kCameraChannelApiSetZoomLevel)) { + HandleSetZoomLevelCall(method_call.arguments(), std::move(result)); + } else if (!method_name.compare(kCameraChannelApiLockCaptureOrientation)) { + HandleLockCaptureOrientationCall(method_call.arguments(), + std::move(result)); + } else if (!method_name.compare(kCameraChannelApiUnlockCaptureOrientation)) { + result->NotImplemented(); + } else if (!method_name.compare(kCameraChannelApiDispose)) { + HandleDisposeCall(method_call.arguments(), std::move(result)); + } else { + result->NotImplemented(); + } +} + +void CameraPlugin::HandleAvailableCamerasCall( + std::unique_ptr> result) { + flutter::EncodableList cameras; + + // TODO: add multi camera support. + for (int i = 0; i < 1; i++) { + AvailableCamerasMessage camera; + camera.SetName("camera" + std::to_string(i)); + camera.SetSensorOrientation(0); + camera.SetLensFacing("back"); + cameras.push_back(camera.ToMap()); + } + + result->Success(flutter::EncodableValue(cameras)); +} + +void CameraPlugin::HandleCreateCall( + const flutter::EncodableValue* message, + std::unique_ptr> result) { + // auto meta = CreateMessage::FromMap(message); + + buffer_ = std::make_unique(); + texture_ = + std::make_unique(flutter::PixelBufferTexture( + [host = this](size_t width, + size_t height) -> const FlutterDesktopPixelBuffer* { + host->buffer_->width = host->camera_->GetPreviewWidth(); + host->buffer_->height = host->camera_->GetPreviewHeight(); + host->buffer_->buffer = host->camera_->GetPreviewFrameBuffer(); + return host->buffer_.get(); + })); + auto texture_id = texture_registrar_->RegisterTexture(texture_.get()); + auto stream_handler = std::make_unique( + // OnNotifyInitialized + []() {}, + // OnNotifyFrameDecoded + [texture_id, host = this]() { + host->texture_registrar_->MarkTextureFrameAvailable(texture_id); + }, + // OnNotifyCompleted + []() {}); + + camera_ = std::make_unique(std::move(stream_handler)); + texture_id_ = texture_id; + + flutter::EncodableMap reply; + reply[flutter::EncodableValue("cameraId")] = + flutter::EncodableValue(texture_id); + + result->Success(flutter::EncodableValue(reply)); +} + +void CameraPlugin::HandleInitializeCall( + const flutter::EncodableValue* message, + std::unique_ptr> result) { + camera_->Play(); + double preview_width = camera_->GetPreviewWidth(); + double preview_height = camera_->GetPreviewHeight(); + + { + method_channel_camera_ = + std::make_unique(plugin_registrar_, texture_id_); + + CameraInitializedEvent message; + message.SetPreviewWidth(preview_width); + message.SetPreviewHeight(preview_height); + message.SetFocusMode(FocusMode::kAuto); + message.SetExposureMode(ExposureMode::kAuto); + message.SetFocusPointSupported(false); + message.SetExposurePointSupported(false); + + method_channel_camera_->SendInitializedEvent(message); + } + + { + method_channel_device_ = + std::make_unique(plugin_registrar_); + auto orientation = DeviceOrientation::kLandscapeRight; + + method_channel_device_->SendDeviceOrientationChangeEvent(orientation); + } + result->Success(); +} + +void CameraPlugin::HandleGetMaxZoomLevelCall( + const flutter::EncodableValue* message, + std::unique_ptr> result) { + if (!camera_) { + result->Error("Not found an active camera", + "Check for creating a camera device"); + return; + } + result->Success(flutter::EncodableValue(camera_->GetMaxZoomLevel())); +} + +void CameraPlugin::HandleGetMinZoomLevelCall( + const flutter::EncodableValue* message, + std::unique_ptr> result) { + if (!camera_) { + result->Error("Not found an active camera", + "Check for creating a camera device"); + return; + } + result->Success(flutter::EncodableValue(camera_->GetMinZoomLevel())); +} + +void CameraPlugin::HandleSetZoomLevelCall( + const flutter::EncodableValue* message, + std::unique_ptr> result) { + if (!camera_) { + result->Error("Not found an active camera", + "Check for creating a camera device"); + return; + } + + auto meta = ZoomLevelMessage::FromMap(*message); + if (camera_->SetZoomLevel(meta.GetZoom())) { + result->Success(); + } else { + result->Error("Failed to change the zoom level", "Check the zoom level"); + } +} + +void CameraPlugin::HandleLockCaptureOrientationCall( + const flutter::EncodableValue* message, + std::unique_ptr> result) { + result->NotImplemented(); +} + +void CameraPlugin::HandleDisposeCall( + const flutter::EncodableValue* message, + std::unique_ptr> result) { + // TODO: add multi camera support. + if (camera_) { + camera_->Stop(); + camera_ = nullptr; + texture_registrar_->UnregisterTexture(texture_id_); + } + result->Success(); +} + +} // namespace + +void CameraElinuxPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + CameraPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/packages/camera/elinux/camera_stream_handler.h b/packages/camera/elinux/camera_stream_handler.h new file mode 100644 index 0000000..f7c828e --- /dev/null +++ b/packages/camera/elinux/camera_stream_handler.h @@ -0,0 +1,32 @@ +// Copyright 2021 Sony Group Corporation. 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_ELINUX_CAMERA_STREAM_HANDLER_H_ +#define PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_STREAM_HANDLER_H_ + +class CameraStreamHandler { + public: + CameraStreamHandler() = default; + virtual ~CameraStreamHandler() = default; + + // Prevent copying. + CameraStreamHandler(CameraStreamHandler const&) = delete; + CameraStreamHandler& operator=(CameraStreamHandler const&) = delete; + + // Notifies the completion of initializing the video player. + void OnNotifyInitialized() { OnNotifyInitializedInternal(); } + + // Notifies the completion of decoding a video frame. + void OnNotifyFrameDecoded() { OnNotifyFrameDecodedInternal(); } + + // Notifies the completion of playing a video. + void OnNotifyCompleted() { OnNotifyCompletedInternal(); } + + protected: + virtual void OnNotifyInitializedInternal() = 0; + virtual void OnNotifyFrameDecodedInternal() = 0; + virtual void OnNotifyCompletedInternal() = 0; +}; + +#endif // PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_STREAM_HANDLER_H_ diff --git a/packages/camera/elinux/camera_stream_handler_impl.h b/packages/camera/elinux/camera_stream_handler_impl.h new file mode 100644 index 0000000..55e5377 --- /dev/null +++ b/packages/camera/elinux/camera_stream_handler_impl.h @@ -0,0 +1,57 @@ +// Copyright 2021 Sony Group Corporation. 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_ELINUX_CAMERA_STREAM_HANDLER_IMPL_H_ +#define PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_STREAM_HANDLER_IMPL_H_ + +#include + +#include "camera_stream_handler.h" + +class CameraStreamHandlerImpl : public CameraStreamHandler { + public: + using OnNotifyInitialized = std::function; + using OnNotifyFrameDecoded = std::function; + using OnNotifyCompleted = std::function; + + CameraStreamHandlerImpl(OnNotifyInitialized on_notify_initialized, + OnNotifyFrameDecoded on_notify_frame_decoded, + OnNotifyCompleted on_notify_completed) + : on_notify_initialized_(on_notify_initialized), + on_notify_frame_decoded_(on_notify_frame_decoded), + on_notify_completed_(on_notify_completed) {} + virtual ~CameraStreamHandlerImpl() = default; + + // Prevent copying. + CameraStreamHandlerImpl(CameraStreamHandlerImpl const&) = delete; + CameraStreamHandlerImpl& operator=(CameraStreamHandlerImpl const&) = delete; + + protected: + // |CameraStreamHandler| + void OnNotifyInitializedInternal() { + if (on_notify_initialized_) { + on_notify_initialized_(); + } + } + + // |CameraStreamHandler| + void OnNotifyFrameDecodedInternal() { + if (on_notify_frame_decoded_) { + on_notify_frame_decoded_(); + } + } + + // |CameraStreamHandler| + void OnNotifyCompletedInternal() { + if (on_notify_completed_) { + on_notify_completed_(); + } + } + + OnNotifyInitialized on_notify_initialized_; + OnNotifyFrameDecoded on_notify_frame_decoded_; + OnNotifyCompleted on_notify_completed_; +}; + +#endif // PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_STREAM_HANDLER_IMPL_H_ diff --git a/packages/camera/elinux/events/camera_initialized_event.h b/packages/camera/elinux/events/camera_initialized_event.h new file mode 100644 index 0000000..244c143 --- /dev/null +++ b/packages/camera/elinux/events/camera_initialized_event.h @@ -0,0 +1,128 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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_ELINUX_CAMERA_EVENT_CAMERA_INITIALIZED_EVENT_H_ +#define PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_EVENT_CAMERA_INITIALIZED_EVENT_H_ + +#include +#include + +#include +#include + +#include "types/exposure_mode.h" +#include "types/focus_mode.h" + +// See: +// flutter/plugins/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart +class CameraInitializedEvent { + public: + CameraInitializedEvent() = default; + ~CameraInitializedEvent() = default; + + // Prevent copying. + CameraInitializedEvent(CameraInitializedEvent const&) = default; + CameraInitializedEvent& operator=(CameraInitializedEvent const&) = default; + + void SetPreviewWidth(const double& width) { preview_width_ = width; } + double GetPreviewWidth() const { return preview_width_; } + + void SetPreviewHeight(const double& height) { preview_height_ = height; } + double GetPreviewHeight() const { return preview_height_; } + + void SetFocusMode(const FocusMode& focus_mode) { focus_mode_ = focus_mode; } + FocusMode GetFocusMode() const { return focus_mode_; } + + void SetExposureMode(const ExposureMode& exposure_mode) { + exposure_mode_ = exposure_mode; + } + ExposureMode GetExposureMode() const { return exposure_mode_; } + + void SetFocusPointSupported(const bool& supported) { + focus_point_supported_ = supported; + } + bool GetFocusPointSupported() const { return focus_point_supported_; } + + void SetExposurePointSupported(const bool& supported) { + exposure_point_supported_ = supported; + } + bool GetExposurePointSupported() const { return exposure_point_supported_; } + + flutter::EncodableValue ToMap() { + flutter::EncodableMap map = { + {flutter::EncodableValue("previewWidth"), + flutter::EncodableValue(preview_width_)}, + {flutter::EncodableValue("previewHeight"), + flutter::EncodableValue(preview_height_)}, + {flutter::EncodableValue("focusMode"), + flutter::EncodableValue(SerializeFocusMode(focus_mode_))}, + {flutter::EncodableValue("exposureMode"), + flutter::EncodableValue(SerializeExposureMode(exposure_mode_))}, + {flutter::EncodableValue("focusPointSupported"), + flutter::EncodableValue(focus_point_supported_)}, + {flutter::EncodableValue("exposurePointSupported"), + flutter::EncodableValue(exposure_point_supported_)}, + }; + return flutter::EncodableValue(map); + } + + static CameraInitializedEvent FromMap(const flutter::EncodableValue& value) { + CameraInitializedEvent message; + if (std::holds_alternative(value)) { + auto map = std::get(value); + + flutter::EncodableValue& preview_width = + map[flutter::EncodableValue("previewWidth")]; + if (std::holds_alternative(preview_width)) { + message.SetPreviewWidth(std::get(preview_width)); + } + + flutter::EncodableValue& preview_height = + map[flutter::EncodableValue("previewHeight")]; + if (std::holds_alternative(preview_height)) { + message.SetPreviewHeight(std::get(preview_height)); + } + + flutter::EncodableValue& focus_mode = + map[flutter::EncodableValue("focusMode")]; + if (std::holds_alternative(focus_mode)) { + message.SetFocusMode( + DeserializeFocusMode(std::get(focus_mode))); + } + + flutter::EncodableValue& exposure_mode = + map[flutter::EncodableValue("exposureMode")]; + if (std::holds_alternative(exposure_mode)) { + message.SetExposureMode( + DeserializeExposureMode(std::get(exposure_mode))); + } + + flutter::EncodableValue& focus_point_supported = + map[flutter::EncodableValue("focusPointSupported")]; + if (std::holds_alternative(focus_point_supported)) { + message.SetFocusPointSupported(std::get(focus_point_supported)); + } + + flutter::EncodableValue& exposure_point_supported = + map[flutter::EncodableValue("exposurePointSupported")]; + if (std::holds_alternative(exposure_point_supported)) { + message.SetExposurePointSupported( + std::get(exposure_point_supported)); + } + } + + return message; + } + + private: + double preview_width_; + double preview_height_; + FocusMode focus_mode_; + ExposureMode exposure_mode_; + bool focus_point_supported_; + bool exposure_point_supported_; +}; + +#endif // PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_EVENT_CAMERA_INITIALIZED_EVENT_H_ diff --git a/packages/camera/elinux/gst_camera.cc b/packages/camera/elinux/gst_camera.cc new file mode 100644 index 0000000..8440c30 --- /dev/null +++ b/packages/camera/elinux/gst_camera.cc @@ -0,0 +1,309 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "gst_camera.h" + +#include + +GstCamera::GstCamera(std::unique_ptr handler) + : stream_handler_(std::move(handler)) { + gst_.pipeline = nullptr; + gst_.camerabin = nullptr; + gst_.video_convert = nullptr; + gst_.video_sink = nullptr; + gst_.output = nullptr; + gst_.bus = nullptr; + gst_.buffer = nullptr; + + if (!CreatePipeline()) { + std::cerr << "Failed to create a pipeline" << std::endl; + DestroyPipeline(); + return; + } + + // Prerolls before getting information from the pipeline. + Preroll(); + + GetZoomMaxMinSize(max_zoom_level_, min_zoom_level_); +} + +GstCamera::~GstCamera() { + Stop(); + DestroyPipeline(); +} + +// static +void GstCamera::GstLibraryLoad() { gst_init(NULL, NULL); } + +// static +void GstCamera::GstLibraryUnload() { gst_deinit(); } + +bool GstCamera::Play() { + auto result = gst_element_set_state(gst_.pipeline, GST_STATE_PLAYING); + if (result == GST_STATE_CHANGE_FAILURE) { + std::cerr << "Failed to change the state to PLAYING" << std::endl; + return false; + } + + // Waits until the state becomes GST_STATE_PLAYING. + if (result == GST_STATE_CHANGE_ASYNC) { + GstState state; + result = + gst_element_get_state(gst_.pipeline, &state, NULL, GST_CLOCK_TIME_NONE); + if (result == GST_STATE_CHANGE_FAILURE) { + std::cerr << "Failed to get the current state" << std::endl; + } + } + + return true; +} + +bool GstCamera::Pause() { + if (gst_element_set_state(gst_.pipeline, GST_STATE_PAUSED) == + GST_STATE_CHANGE_FAILURE) { + std::cerr << "Failed to change the state to PAUSED" << std::endl; + return false; + } + return true; +} + +bool GstCamera::Stop() { + if (gst_element_set_state(gst_.pipeline, GST_STATE_READY) == + GST_STATE_CHANGE_FAILURE) { + std::cerr << "Failed to change the state to READY" << std::endl; + return false; + } + return true; +} + +bool GstCamera::SetZoomLevel(float zoom) { + if (zoom_level_ == zoom) { + return true; + } + if (max_zoom_level_ < zoom) { + std::cerr << "zoom level(" << zoom << ") is over the max-zoom level(" + << max_zoom_level_ << ")" << std::endl; + return false; + } + if (min_zoom_level_ > zoom) { + std::cerr << "zoom level(" << zoom << ") is under the min-zoom level(" + << min_zoom_level_ << ")" << std::endl; + return false; + } + + g_object_set(gst_.camerabin, "zoom", zoom, NULL); + zoom_level_ = zoom; + return true; +} + +const uint8_t* GstCamera::GetPreviewFrameBuffer() { + std::shared_lock lock(mutex_buffer_); + if (!gst_.buffer) { + return nullptr; + } + + const uint32_t pixel_bytes = width_ * height_ * 4; + gst_buffer_extract(gst_.buffer, 0, pixels_.get(), pixel_bytes); + return reinterpret_cast(pixels_.get()); +} + +// Creats a camra pipeline using camerabin. +// $ gst-launch-1.0 camerabin viewfinder-sink="videoconvert ! +// video/x-raw,format=RGBA ! fakesink" +bool GstCamera::CreatePipeline() { + gst_.pipeline = gst_pipeline_new("pipeline"); + if (!gst_.pipeline) { + std::cerr << "Failed to create a pipeline" << std::endl; + return false; + } + gst_.camerabin = gst_element_factory_make("camerabin", "camerabin"); + if (!gst_.camerabin) { + std::cerr << "Failed to create a source" << std::endl; + return false; + } + gst_.video_convert = gst_element_factory_make("videoconvert", "videoconvert"); + if (!gst_.video_convert) { + std::cerr << "Failed to create a videoconvert" << std::endl; + return false; + } + gst_.video_sink = gst_element_factory_make("fakesink", "videosink"); + if (!gst_.video_sink) { + std::cerr << "Failed to create a videosink" << std::endl; + return false; + } + gst_.output = gst_bin_new("output"); + if (!gst_.output) { + std::cerr << "Failed to create an output" << std::endl; + return false; + } + gst_.bus = gst_pipeline_get_bus(GST_PIPELINE(gst_.pipeline)); + if (!gst_.bus) { + std::cerr << "Failed to create a bus" << std::endl; + return false; + } + gst_bus_set_sync_handler(gst_.bus, (GstBusSyncHandler)HandleGstMessage, this, + NULL); + + // Sets properties to fakesink to get the callback of a decoded frame. + g_object_set(G_OBJECT(gst_.video_sink), "sync", TRUE, "qos", FALSE, NULL); + g_object_set(G_OBJECT(gst_.video_sink), "signal-handoffs", TRUE, NULL); + g_signal_connect(G_OBJECT(gst_.video_sink), "handoff", + G_CALLBACK(HandoffHandler), this); + gst_bin_add_many(GST_BIN(gst_.output), gst_.video_convert, gst_.video_sink, + NULL); + + // Adds caps to the converter to convert the color format to RGBA. + auto* caps = gst_caps_from_string("video/x-raw,format=RGBA"); + auto link_ok = + gst_element_link_filtered(gst_.video_convert, gst_.video_sink, caps); + gst_caps_unref(caps); + if (!link_ok) { + std::cerr << "Failed to link elements" << std::endl; + return false; + } + + auto* sinkpad = gst_element_get_static_pad(gst_.video_convert, "sink"); + auto* ghost_sinkpad = gst_ghost_pad_new("sink", sinkpad); + gst_pad_set_active(ghost_sinkpad, TRUE); + gst_element_add_pad(gst_.output, ghost_sinkpad); + + // Sets properties to camerabin. + g_object_set(gst_.camerabin, "viewfinder-sink", gst_.output, NULL); + gst_bin_add_many(GST_BIN(gst_.pipeline), gst_.camerabin, NULL); + + return true; +} + +void GstCamera::Preroll() { + if (!gst_.camerabin) { + return; + } + + auto result = gst_element_set_state(gst_.pipeline, GST_STATE_PAUSED); + if (result == GST_STATE_CHANGE_FAILURE) { + std::cerr << "Failed to change the state to PAUSED" << std::endl; + return; + } + + // Waits until the state becomes GST_STATE_PAUSED. + if (result == GST_STATE_CHANGE_ASYNC) { + GstState state; + result = + gst_element_get_state(gst_.pipeline, &state, NULL, GST_CLOCK_TIME_NONE); + if (result == GST_STATE_CHANGE_FAILURE) { + std::cerr << "Failed to get the current state" << std::endl; + } + } +} + +void GstCamera::DestroyPipeline() { + if (gst_.video_sink) { + g_object_set(G_OBJECT(gst_.video_sink), "signal-handoffs", FALSE, NULL); + } + + if (gst_.pipeline) { + gst_element_set_state(gst_.pipeline, GST_STATE_NULL); + } + + if (gst_.buffer) { + gst_buffer_unref(gst_.buffer); + gst_.buffer = nullptr; + } + + if (gst_.bus) { + gst_object_unref(gst_.bus); + gst_.bus = nullptr; + } + + if (gst_.pipeline) { + gst_object_unref(gst_.pipeline); + gst_.pipeline = nullptr; + } + + if (gst_.camerabin) { + gst_.camerabin = nullptr; + } + + if (gst_.output) { + gst_.output = nullptr; + } + + if (gst_.video_sink) { + gst_.video_sink = nullptr; + } + + if (gst_.video_convert) { + gst_.video_convert = nullptr; + } +} + +void GstCamera::GetZoomMaxMinSize(float& max, float& min) { + if (!gst_.pipeline || !gst_.camerabin) { + std::cerr << "The pileline hasn't initialized yet."; + return; + } + + g_object_get(gst_.camerabin, "max-zoom", &max, NULL); + min = 1.0; +} + +// static +void GstCamera::HandoffHandler(GstElement* fakesink, GstBuffer* buf, + GstPad* new_pad, gpointer user_data) { + auto* self = reinterpret_cast(user_data); + auto* caps = gst_pad_get_current_caps(new_pad); + auto* structure = gst_caps_get_structure(caps, 0); + + int width; + int height; + gst_structure_get_int(structure, "width", &width); + gst_structure_get_int(structure, "height", &height); + if (width != self->width_ || height != self->height_) { + self->width_ = width; + self->height_ = height; + self->pixels_.reset(new uint32_t[width * height]); + std::cout << "Pixel buffer size: width = " << width + << ", height = " << height << std::endl; + } + + std::lock_guard lock(self->mutex_buffer_); + if (self->gst_.buffer) { + gst_buffer_unref(self->gst_.buffer); + self->gst_.buffer = nullptr; + } + self->gst_.buffer = gst_buffer_ref(buf); + self->stream_handler_->OnNotifyFrameDecoded(); +} + +// static +gboolean GstCamera::HandleGstMessage(GstBus* bus, GstMessage* message, + gpointer user_data) { + switch (GST_MESSAGE_TYPE(message)) { + case GST_MESSAGE_WARNING: { + gchar* debug; + GError* error; + gst_message_parse_warning(message, &error, &debug); + g_printerr("WARNING from element %s: %s\n", GST_OBJECT_NAME(message->src), + error->message); + g_printerr("Warning details: %s\n", debug); + g_free(debug); + g_error_free(error); + break; + } + case GST_MESSAGE_ERROR: { + gchar* debug; + GError* error; + gst_message_parse_error(message, &error, &debug); + g_printerr("ERROR from element %s: %s\n", GST_OBJECT_NAME(message->src), + error->message); + g_printerr("Error details: %s\n", debug); + g_free(debug); + g_error_free(error); + break; + } + default: + break; + } + return TRUE; +} diff --git a/packages/camera/elinux/gst_camera.h b/packages/camera/elinux/gst_camera.h new file mode 100644 index 0000000..5cf5048 --- /dev/null +++ b/packages/camera/elinux/gst_camera.h @@ -0,0 +1,68 @@ +// Copyright 2021 Sony Group Corporation. 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_ELINUX_GST_CAMERA_H_ +#define PACKAGES_CAMERA_CAMERA_ELINUX_GST_CAMERA_H_ + +#include + +#include +#include +#include + +#include "camera_stream_handler.h" + +class GstCamera { + public: + GstCamera(std::unique_ptr handler); + ~GstCamera(); + + static void GstLibraryLoad(); + static void GstLibraryUnload(); + + bool Play(); + bool Pause(); + bool Stop(); + + bool SetZoomLevel(float zoom); + float GetMaxZoomLevel() const { return max_zoom_level_; }; + float GetMinZoomLevel() const { return min_zoom_level_; }; + + const uint8_t* GetPreviewFrameBuffer(); + int32_t GetPreviewWidth() const { return width_; }; + int32_t GetPreviewHeight() const { return height_; }; + + private: + struct GstCameraElements { + GstElement* pipeline; + GstElement* camerabin; + GstElement* video_convert; + GstElement* video_sink; + GstElement* output; + GstBus* bus; + GstBuffer* buffer; + }; + + static void HandoffHandler(GstElement* fakesink, GstBuffer* buf, + GstPad* new_pad, gpointer user_data); + static gboolean HandleGstMessage(GstBus* bus, GstMessage* message, + gpointer user_data); + + bool CreatePipeline(); + void DestroyPipeline(); + void Preroll(); + void GetZoomMaxMinSize(float& max, float& min); + + GstCameraElements gst_; + std::unique_ptr pixels_; + int32_t width_ = -1; + int32_t height_ = -1; + std::shared_mutex mutex_buffer_; + std::unique_ptr stream_handler_; + float max_zoom_level_; + float min_zoom_level_; + float zoom_level_ = 1.0f; +}; + +#endif // PACKAGES_CAMERA_CAMERA_ELINUX_GST_CAMERA_H_ diff --git a/packages/camera/elinux/include/camera_elinux/camera_elinux_plugin.h b/packages/camera/elinux/include/camera_elinux/camera_elinux_plugin.h new file mode 100644 index 0000000..12bf40f --- /dev/null +++ b/packages/camera/elinux/include/camera_elinux/camera_elinux_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_CAMERA_CAMERA_ELINUX_PLUGIN_H_ +#define FLUTTER_PLUGIN_CAMERA_CAMERA_ELINUX_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define FLUTTER_PLUGIN_EXPORT +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void CameraElinuxPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_CAMERA_CAMERA_ELINUX_PLUGIN_H_ diff --git a/packages/camera/elinux/messages/available_cameras_message.h b/packages/camera/elinux/messages/available_cameras_message.h new file mode 100644 index 0000000..7e20046 --- /dev/null +++ b/packages/camera/elinux/messages/available_cameras_message.h @@ -0,0 +1,79 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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_ELINUX_MESSAGES_AVAILABLE_CAMERAS_MESSAGE_H_ +#define PACKAGES_CAMERA_CAMERA_ELINUX_MESSAGES_AVAILABLE_CAMERAS_MESSAGE_H_ + +#include +#include + +#include +#include + +class AvailableCamerasMessage { + public: + AvailableCamerasMessage() = default; + ~AvailableCamerasMessage() = default; + + // Prevent copying. + AvailableCamerasMessage(AvailableCamerasMessage const&) = default; + AvailableCamerasMessage& operator=(AvailableCamerasMessage const&) = default; + + void SetName(const std::string& name) { name_ = name; } + std::string GetName() const { return name_; } + + void SetSensorOrientation(const int& sensor_orientation) { + sensor_orientation_ = sensor_orientation; + } + int GetSensorOrientation() const { return sensor_orientation_; } + + void SetLensFacing(const std::string& lens_facing) { + lens_facing_ = lens_facing; + } + std::string GetLensFacing() const { return lens_facing_; } + + flutter::EncodableValue ToMap() { + flutter::EncodableMap map = { + {flutter::EncodableValue("name"), flutter::EncodableValue(name_)}, + {flutter::EncodableValue("sensorOrientation"), + flutter::EncodableValue(sensor_orientation_)}, + {flutter::EncodableValue("lensFacing"), + flutter::EncodableValue(lens_facing_)}}; + return flutter::EncodableValue(map); + } + + static AvailableCamerasMessage FromMap(const flutter::EncodableValue& value) { + AvailableCamerasMessage message; + if (std::holds_alternative(value)) { + auto map = std::get(value); + + flutter::EncodableValue& name = map[flutter::EncodableValue("name")]; + if (std::holds_alternative(name)) { + message.SetName(std::get(name)); + } + + flutter::EncodableValue& sensor_orientation = + map[flutter::EncodableValue("sensorOrientation")]; + if (std::holds_alternative(sensor_orientation)) { + message.SetSensorOrientation(std::get(sensor_orientation)); + } + + flutter::EncodableValue& lens_facing = + map[flutter::EncodableValue("lensFacing")]; + if (std::holds_alternative(lens_facing)) { + message.SetLensFacing(std::get(lens_facing)); + } + } + + return message; + } + + private: + std::string name_; + int sensor_orientation_; + std::string lens_facing_; +}; + +#endif // PACKAGES_CAMERA_CAMERA_ELINUX_MESSAGES_AVAILABLE_CAMERAS_MESSAGE_H_ diff --git a/packages/camera/elinux/messages/messages.h b/packages/camera/elinux/messages/messages.h new file mode 100644 index 0000000..337039d --- /dev/null +++ b/packages/camera/elinux/messages/messages.h @@ -0,0 +1,13 @@ +// 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_ELINUX_MESSAGES_MESSAGES_H_ +#define PACKAGES_CAMERA_CAMERA_ELINUX_MESSAGES_MESSAGES_H_ + +#include "available_cameras_message.h" +#include "orientation_message.h" +#include "texture_message.h" +#include "zoom_level_message.h" + +#endif // PACKAGES_CAMERA_CAMERA_ELINUX_MESSAGES_MESSAGES_H_ diff --git a/packages/camera/elinux/messages/orientation_message.h b/packages/camera/elinux/messages/orientation_message.h new file mode 100644 index 0000000..a0d51b4 --- /dev/null +++ b/packages/camera/elinux/messages/orientation_message.h @@ -0,0 +1,57 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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_ELINUX_MESSAGES_ORIENTATION_MESSAGE_H_ +#define PACKAGES_CAMERA_CAMERA_ELINUX_MESSAGES_ORIENTATION_MESSAGE_H_ + +#include +#include + +#include +#include + +#include "types/orientation.h" + +class OrientationMessage { + public: + OrientationMessage() = default; + ~OrientationMessage() = default; + + // Prevent copying. + OrientationMessage(OrientationMessage const&) = default; + OrientationMessage& operator=(OrientationMessage const&) = default; + + void SetOrientation(DeviceOrientation orientation) { + orientation_ = orientation; + } + DeviceOrientation GetOrientation() const { return orientation_; } + + flutter::EncodableValue ToMap() { + flutter::EncodableMap map = { + {flutter::EncodableValue("orientation"), + flutter::EncodableValue(SerializeDeviceOrientation(orientation_))}}; + return flutter::EncodableValue(map); + } + + static OrientationMessage FromMap(const flutter::EncodableValue& value) { + OrientationMessage message; + if (std::holds_alternative(value)) { + auto map = std::get(value); + + flutter::EncodableValue& orientation = + map[flutter::EncodableValue("orientation")]; + if (std::holds_alternative(orientation)) { + message.SetOrientation( + DeserializeDeviceOrientation(std::get(orientation))); + } + } + return message; + } + + private: + DeviceOrientation orientation_; +}; + +#endif // PACKAGES_CAMERA_CAMERA_ELINUX_MESSAGES_ORIENTATION_MESSAGE_H_ diff --git a/packages/camera/elinux/messages/texture_message.h b/packages/camera/elinux/messages/texture_message.h new file mode 100644 index 0000000..33e2a18 --- /dev/null +++ b/packages/camera/elinux/messages/texture_message.h @@ -0,0 +1,50 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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_ELINUX_MESSAGES_TEXTURE_MESSAGE_H_ +#define PACKAGES_CAMERA_CAMERA_ELINUX_MESSAGES_TEXTURE_MESSAGE_H_ + +#include +#include + +class TextureMessage { + public: + TextureMessage() = default; + ~TextureMessage() = default; + + // Prevent copying. + TextureMessage(TextureMessage const&) = default; + TextureMessage& operator=(TextureMessage const&) = default; + + void SetTextureId(int64_t texture_id) { texture_id_ = texture_id; } + + int64_t GetTextureId() const { return texture_id_; } + + flutter::EncodableValue ToMap() { + flutter::EncodableMap map = {{flutter::EncodableValue("textureId"), + flutter::EncodableValue(texture_id_)}}; + return flutter::EncodableValue(map); + } + + static TextureMessage FromMap(const flutter::EncodableValue& value) { + TextureMessage message; + if (std::holds_alternative(value)) { + auto map = std::get(value); + + flutter::EncodableValue& texture_id = + map[flutter::EncodableValue("textureId")]; + if (std::holds_alternative(texture_id) || + std::holds_alternative(texture_id)) { + message.SetTextureId(texture_id.LongValue()); + } + } + return message; + } + + private: + int64_t texture_id_ = 0; +}; + +#endif // PACKAGES_CAMERA_CAMERA_ELINUX_MESSAGES_TEXTURE_MESSAGE_H_ diff --git a/packages/camera/elinux/messages/zoom_level_message.h b/packages/camera/elinux/messages/zoom_level_message.h new file mode 100644 index 0000000..11cb354 --- /dev/null +++ b/packages/camera/elinux/messages/zoom_level_message.h @@ -0,0 +1,49 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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_ELINUX_MESSAGES_ZOOM_LEVEL_MESSAGE_H_ +#define PACKAGES_CAMERA_CAMERA_ELINUX_MESSAGES_ZOOM_LEVEL_MESSAGE_H_ + +#include +#include + +#include + +class ZoomLevelMessage { + public: + ZoomLevelMessage() = default; + ~ZoomLevelMessage() = default; + + // Prevent copying. + ZoomLevelMessage(ZoomLevelMessage const&) = default; + ZoomLevelMessage& operator=(ZoomLevelMessage const&) = default; + + void SetZoom(double zoom) { zoom_ = zoom; } + double GetZoom() const { return zoom_; } + + flutter::EncodableValue ToMap() { + flutter::EncodableMap map = { + {flutter::EncodableValue("zoom"), flutter::EncodableValue(zoom_)}}; + return flutter::EncodableValue(map); + } + + static ZoomLevelMessage FromMap(const flutter::EncodableValue& value) { + ZoomLevelMessage message; + if (std::holds_alternative(value)) { + auto map = std::get(value); + + flutter::EncodableValue& zoom = map[flutter::EncodableValue("zoom")]; + if (std::holds_alternative(zoom)) { + message.SetZoom(std::get(zoom)); + } + } + return message; + } + + private: + double zoom_; +}; + +#endif // PACKAGES_CAMERA_CAMERA_ELINUX_MESSAGES_ZOOM_LEVEL_MESSAGE_H_ diff --git a/packages/camera/elinux/method_channel/method_channel_camera.cc b/packages/camera/elinux/method_channel/method_channel_camera.cc new file mode 100644 index 0000000..36e0a50 --- /dev/null +++ b/packages/camera/elinux/method_channel/method_channel_camera.cc @@ -0,0 +1,33 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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 "method_channel/method_channel_camera.h" + +#include + +namespace { +constexpr char kChannelName[] = "flutter.io/cameraPlugin/camera"; +constexpr char kChannelMethodInitialized[] = "initialized"; +}; // namespace + +MethodChannelCamera::MethodChannelCamera(flutter::PluginRegistrar* registrar, + int64_t camera_id) { + std::string channel_name = kChannelName + std::to_string(camera_id); + channel_ = std::make_unique>( + registrar->messenger(), channel_name.c_str(), + &flutter::StandardMethodCodec::GetInstance()); +} + +void MethodChannelCamera::SendInitializedEvent( + CameraInitializedEvent& message) { + auto value = std::make_unique(message.ToMap()); + Send(kChannelMethodInitialized, std::move(value)); +} + +void MethodChannelCamera::Send( + const std::string& method, + std::unique_ptr&& arguments) { + channel_->InvokeMethod(method, std::move(arguments)); +} diff --git a/packages/camera/elinux/method_channel/method_channel_camera.h b/packages/camera/elinux/method_channel/method_channel_camera.h new file mode 100644 index 0000000..cb5854a --- /dev/null +++ b/packages/camera/elinux/method_channel/method_channel_camera.h @@ -0,0 +1,37 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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_ELINUX_CAMERA_METHOD_CHANNEL_METHOD_CHANNEL_CAMERA_H_ +#define PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_METHOD_CHANNEL_METHOD_CHANNEL_CAMERA_H_ + +#include +#include +#include + +#include + +#include "events/camera_initialized_event.h" + +enum class CameraEventType { + kError, + kCameraClosing, + kInitialized, +}; + +class MethodChannelCamera { + public: + MethodChannelCamera(flutter::PluginRegistrar* registrar, int64_t camera_id); + ~MethodChannelCamera() = default; + + void SendInitializedEvent(CameraInitializedEvent& message); + + private: + void Send(const std::string& method, + std::unique_ptr&& arguments); + + std::unique_ptr> channel_; +}; + +#endif // PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_METHOD_CHANNEL_METHOD_CHANNEL_CAMERA_H_ diff --git a/packages/camera/elinux/method_channel/method_channel_device.cc b/packages/camera/elinux/method_channel/method_channel_device.cc new file mode 100644 index 0000000..8dbc0a9 --- /dev/null +++ b/packages/camera/elinux/method_channel/method_channel_device.cc @@ -0,0 +1,37 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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 "method_channel/method_channel_device.h" + +#include + +namespace { +constexpr char kChannelName[] = "flutter.io/cameraPlugin/device"; +constexpr char kChannelMethodInitialized[] = "initialized"; + +constexpr char kOrientation[] = "orientation"; +}; // namespace + +MethodChannelDevice::MethodChannelDevice(flutter::PluginRegistrar* registrar) { + channel_ = std::make_unique>( + registrar->messenger(), kChannelName, + &flutter::StandardMethodCodec::GetInstance()); +} + +void MethodChannelDevice::SendDeviceOrientationChangeEvent( + const DeviceOrientation& orientation) { + flutter::EncodableMap mp; + mp[flutter::EncodableValue(kOrientation)] = + flutter::EncodableValue(SerializeDeviceOrientation(orientation)); + auto value = std::make_unique(mp); + + Send(kChannelMethodInitialized, std::move(value)); +} + +void MethodChannelDevice::Send( + const std::string& method, + std::unique_ptr&& arguments) { + channel_->InvokeMethod(method, std::move(arguments)); +} diff --git a/packages/camera/elinux/method_channel/method_channel_device.h b/packages/camera/elinux/method_channel/method_channel_device.h new file mode 100644 index 0000000..02533fd --- /dev/null +++ b/packages/camera/elinux/method_channel/method_channel_device.h @@ -0,0 +1,31 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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_ELINUX_CAMERA_METHOD_CHANNEL_METHOD_CHANNEL_DEVICE_H_ +#define PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_METHOD_CHANNEL_METHOD_CHANNEL_DEVICE_H_ + +#include +#include +#include + +#include + +#include "types/orientation.h" + +class MethodChannelDevice { + public: + MethodChannelDevice(flutter::PluginRegistrar* registrar); + ~MethodChannelDevice() = default; + + void SendDeviceOrientationChangeEvent(const DeviceOrientation& orientation); + + private: + void Send(const std::string& method, + std::unique_ptr&& arguments); + + std::unique_ptr> channel_; +}; + +#endif // PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_METHOD_CHANNEL_METHOD_CHANNEL_DEVICE_H_ diff --git a/packages/camera/elinux/types/exposure_mode.cc b/packages/camera/elinux/types/exposure_mode.cc new file mode 100644 index 0000000..3f24166 --- /dev/null +++ b/packages/camera/elinux/types/exposure_mode.cc @@ -0,0 +1,29 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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 "types/exposure_mode.h" + +std::string SerializeExposureMode(ExposureMode exposure_mode) { + switch (exposure_mode) { + case ExposureMode::kLocked: + return "locked"; + case ExposureMode::kAuto: + return "auto"; + default: + return "auto"; + std::cerr << "Unknown ExposureMode value" << std::endl; + } +} + +ExposureMode DeserializeExposureMode(std::string str) { + if (!str.compare("locked")) { + return ExposureMode::kLocked; + } + if (!str.compare("auto")) { + return ExposureMode::kAuto; + } + std::cerr << str.c_str() << " is not a valid ExposureMode value" << std::endl; + return ExposureMode::kAuto; +} diff --git a/packages/camera/elinux/types/exposure_mode.h b/packages/camera/elinux/types/exposure_mode.h new file mode 100644 index 0000000..7abb457 --- /dev/null +++ b/packages/camera/elinux/types/exposure_mode.h @@ -0,0 +1,22 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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_ELINUX_CAMERA_TYPES_EXPOSURE_MODE_H_ +#define PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_TYPES_EXPOSURE_MODE_H_ + +#include +#include + +// See: +// flutter/plugins/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart +enum class ExposureMode { + kAuto, + kLocked, +}; + +std::string SerializeExposureMode(ExposureMode exposure_mode); +ExposureMode DeserializeExposureMode(std::string str); + +#endif // PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_TYPES_EXPOSURE_MODE_H_ diff --git a/packages/camera/elinux/types/focus_mode.cc b/packages/camera/elinux/types/focus_mode.cc new file mode 100644 index 0000000..3ba1711 --- /dev/null +++ b/packages/camera/elinux/types/focus_mode.cc @@ -0,0 +1,29 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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 "types/focus_mode.h" + +std::string SerializeFocusMode(FocusMode focus_mode) { + switch (focus_mode) { + case FocusMode::kLocked: + return "locked"; + case FocusMode::kAuto: + return "auto"; + default: + std::cerr << "Unknown FocusMode value" << std::endl; + return "auto"; + } +} + +FocusMode DeserializeFocusMode(std::string str) { + if (!str.compare("locked")) { + return FocusMode::kLocked; + } + if (!str.compare("auto")) { + return FocusMode::kAuto; + } + std::cerr << str.c_str() << " is not a valid FocusMode value" << std::endl; + return FocusMode::kAuto; +} diff --git a/packages/camera/elinux/types/focus_mode.h b/packages/camera/elinux/types/focus_mode.h new file mode 100644 index 0000000..cc5b6e4 --- /dev/null +++ b/packages/camera/elinux/types/focus_mode.h @@ -0,0 +1,22 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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_ELINUX_CAMERA_TYPES_FOCUS_MODE_H_ +#define PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_TYPES_FOCUS_MODE_H_ + +#include +#include + +// See: +// flutter/plugins/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart +enum class FocusMode { + kAuto, + kLocked, +}; + +std::string SerializeFocusMode(FocusMode focus_mode); +FocusMode DeserializeFocusMode(std::string str); + +#endif // PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_TYPES_FOCUS_MODE_H_ diff --git a/packages/camera/elinux/types/orientation.cc b/packages/camera/elinux/types/orientation.cc new file mode 100644 index 0000000..bf54f32 --- /dev/null +++ b/packages/camera/elinux/types/orientation.cc @@ -0,0 +1,40 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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 "types/orientation.h" + +std::string SerializeDeviceOrientation(DeviceOrientation orientation) { + switch (orientation) { + case DeviceOrientation::kPortraitUp: + return "portraitUp"; + case DeviceOrientation::kLandscapeLeft: + return "landscapeLeft"; + case DeviceOrientation::kPortraitDown: + return "portraitDown"; + case DeviceOrientation::kLandscapeRight: + return "landscapeRight"; + default: + std::cerr << "Unknown DeviceOrientation value" << std::endl; + return "landscapeLeft"; + } +} + +DeviceOrientation DeserializeDeviceOrientation(std::string str) { + if (!str.compare("portraitUp")) { + return DeviceOrientation::kPortraitUp; + } + if (!str.compare("landscapeLeft")) { + return DeviceOrientation::kLandscapeLeft; + } + if (!str.compare("portraitDown")) { + return DeviceOrientation::kPortraitDown; + } + if (!str.compare("landscapeRight")) { + return DeviceOrientation::kLandscapeRight; + } + std::cerr << str.c_str() << " is not a valid DeviceOrientation value" + << std::endl; + return DeviceOrientation::kLandscapeLeft; +} diff --git a/packages/camera/elinux/types/orientation.h b/packages/camera/elinux/types/orientation.h new file mode 100644 index 0000000..891ebea --- /dev/null +++ b/packages/camera/elinux/types/orientation.h @@ -0,0 +1,24 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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_ELINUX_CAMERA_TYPES_ORIENTATION_H_ +#define PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_TYPES_ORIENTATION_H_ + +#include +#include + +// See: [DeviceOrientation] in +// flutter/packages/flutter/lib/src/services/system_chrome.dart +enum class DeviceOrientation { + kPortraitUp, + kLandscapeLeft, + kPortraitDown, + kLandscapeRight, +}; + +std::string SerializeDeviceOrientation(DeviceOrientation orientation); +DeviceOrientation DeserializeDeviceOrientation(std::string str); + +#endif // PACKAGES_CAMERA_CAMERA_ELINUX_CAMERA_TYPES_ORIENTATION_H_ diff --git a/packages/camera/example/elinux/.gitignore b/packages/camera/example/elinux/.gitignore new file mode 100644 index 0000000..229c109 --- /dev/null +++ b/packages/camera/example/elinux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral/ diff --git a/packages/camera/example/elinux/CMakeLists.txt b/packages/camera/example/elinux/CMakeLists.txt new file mode 100644 index 0000000..94678a5 --- /dev/null +++ b/packages/camera/example/elinux/CMakeLists.txt @@ -0,0 +1,110 @@ +cmake_minimum_required(VERSION 3.15) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "camera_example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + + # Basically we use this include when we got the following error: + # fatal error: 'bits/c++config.h' file not found + if(FLUTTER_TARGET_PLATFORM_SYSROOT) + include_directories(SYSTEM ${FLUTTER_SYSTEM_INCLUDE_DIRECTORIES}) + endif() +endif() + +# Configure build options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Configure build option to target backend. +if (NOT FLUTTER_TARGET_BACKEND_TYPE) + set(FLUTTER_TARGET_BACKEND_TYPE "wayland" CACHE + STRING "Flutter target backend type" FORCE) + set_property(CACHE FLUTTER_TARGET_BACKEND_TYPE PROPERTY STRINGS + "wayland" "gbm" "eglstream" "x11") +endif() + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +install(FILES "${FLUTTER_EMBEDDER_LIBRARY}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/camera/example/elinux/flutter/CMakeLists.txt b/packages/camera/example/elinux/flutter/CMakeLists.txt new file mode 100644 index 0000000..f141a3c --- /dev/null +++ b/packages/camera/example/elinux/flutter/CMakeLists.txt @@ -0,0 +1,108 @@ +cmake_minimum_required(VERSION 3.15) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_engine.so") +if(FLUTTER_TARGET_BACKEND_TYPE MATCHES "gbm") + set(FLUTTER_EMBEDDER_LIBRARY "${EPHEMERAL_DIR}/libflutter_elinux_gbm.so") +elseif(FLUTTER_TARGET_BACKEND_TYPE MATCHES "eglstream") + set(FLUTTER_EMBEDDER_LIBRARY "${EPHEMERAL_DIR}/libflutter_elinux_eglstream.so") +elseif(FLUTTER_TARGET_BACKEND_TYPE MATCHES "x11") + set(FLUTTER_EMBEDDER_LIBRARY "${EPHEMERAL_DIR}/libflutter_elinux_x11.so") +else() + set(FLUTTER_EMBEDDER_LIBRARY "${EPHEMERAL_DIR}/libflutter_elinux_wayland.so") +endif() + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_EMBEDDER_LIBRARY ${FLUTTER_EMBEDDER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/elinux/" PARENT_SCOPE) +set(AOT_LIBRARY "${EPHEMERAL_DIR}/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_plugin_registrar.h" + "flutter_messenger.h" + "flutter_texture_registrar.h" + "flutter_elinux.h" + "flutter_platform_views.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE "${FLUTTER_EMBEDDER_LIBRARY}") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list_prepend(CPP_WRAPPER_SOURCES_CORE "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list_prepend(CPP_WRAPPER_SOURCES_PLUGIN "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list_prepend(CPP_WRAPPER_SOURCES_APP "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + "${FLUTTER_EMBEDDER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/camera/example/elinux/flutter/generated_plugin_registrant.h b/packages/camera/example/elinux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..a31c23c --- /dev/null +++ b/packages/camera/example/elinux/flutter/generated_plugin_registrant.h @@ -0,0 +1,13 @@ +// +// Generated file. Do not edit. +// + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/camera/example/elinux/flutter/generated_plugins.cmake b/packages/camera/example/elinux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2495ffb --- /dev/null +++ b/packages/camera/example/elinux/flutter/generated_plugins.cmake @@ -0,0 +1,17 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + camera_elinux + video_player_elinux +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/elinux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) diff --git a/packages/camera/example/elinux/flutter/main.dart b/packages/camera/example/elinux/flutter/main.dart new file mode 100644 index 0000000..31a7a2e --- /dev/null +++ b/packages/camera/example/elinux/flutter/main.dart @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// +// @dart=2.12 + +import 'package:camera_example/main.dart' as entrypoint; +import 'generated_plugin_registrant.dart'; + +Future main() async { + registerPlugins(); + entrypoint.main(); +} diff --git a/packages/camera/example/elinux/runner/CMakeLists.txt b/packages/camera/example/elinux/runner/CMakeLists.txt new file mode 100644 index 0000000..d15d5ca --- /dev/null +++ b/packages/camera/example/elinux/runner/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.15) +project(runner LANGUAGES CXX) + +if(FLUTTER_TARGET_BACKEND_TYPE MATCHES "gbm") + add_definitions(-DFLUTTER_TARGET_BACKEND_GBM) +elseif(FLUTTER_TARGET_BACKEND_TYPE MATCHES "eglstream") + add_definitions(-DFLUTTER_TARGET_BACKEND_EGLSTREAM) +elseif(FLUTTER_TARGET_BACKEND_TYPE MATCHES "x11") + add_definitions(-DFLUTTER_TARGET_BACKEND_X11) +else() + add_definitions(-DFLUTTER_TARGET_BACKEND_WAYLAND) +endif() + +add_executable(${BINARY_NAME} + "flutter_window.cc" + "main.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) +apply_standard_settings(${BINARY_NAME}) +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/camera/example/elinux/runner/command_options.h b/packages/camera/example/elinux/runner/command_options.h new file mode 100644 index 0000000..977cdc8 --- /dev/null +++ b/packages/camera/example/elinux/runner/command_options.h @@ -0,0 +1,367 @@ +// Copyright 2021 Sony Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMMAND_OPTIONS_ +#define COMMAND_OPTIONS_ + +#include +#include +#include +#include +#include +#include +#include + +// todo: Supports other types besides int, string. + +namespace commandline { + +namespace { +constexpr char kOptionStyleNormal[] = "--"; +constexpr char kOptionStyleShort[] = "-"; +constexpr char kOptionValueForHelpMessage[] = "="; +} // namespace + +class Exception : public std::exception { + public: + Exception(const std::string& msg) : msg_(msg) {} + ~Exception() throw() {} + + const char* what() const throw() { return msg_.c_str(); } + + private: + std::string msg_; +}; + +class CommandOptions { + public: + CommandOptions() = default; + ~CommandOptions() = default; + + void AddWithoutValue(const std::string& name, const std::string& short_name, + const std::string& description, bool required) { + Add(name, short_name, description, "", + ReaderString(), required, false); + } + + void AddInt(const std::string& name, const std::string& short_name, + const std::string& description, const int& default_value, + bool required) { + Add(name, short_name, description, default_value, + ReaderInt(), required, true); + } + + void AddString(const std::string& name, const std::string& short_name, + const std::string& description, + const std::string& default_value, bool required) { + Add(name, short_name, description, default_value, + ReaderString(), required, true); + } + + template + void Add(const std::string& name, const std::string& short_name, + const std::string& description, const T default_value, + F reader = F(), bool required = true, bool required_value = true) { + if (options_.find(name) != options_.end()) { + std::cerr << "Already registered option: " << name << std::endl; + return; + } + + if (lut_short_options_.find(short_name) != lut_short_options_.end()) { + std::cerr << short_name << "is already registered" << std::endl; + return; + } + lut_short_options_[short_name] = name; + + options_[name] = std::make_unique>( + name, short_name, description, default_value, reader, required, + required_value); + + // register to show help message. + registration_order_options_.push_back(options_[name].get()); + } + + bool Exist(const std::string& name) { + auto itr = options_.find(name); + return itr != options_.end() && itr->second->HasValue(); + } + + template + const T& GetValue(const std::string& name) { + auto itr = options_.find(name); + if (itr == options_.end()) { + throw Exception("Not found: " + name); + } + + auto* option_value = dynamic_cast*>(itr->second.get()); + if (!option_value) { + throw Exception("Type mismatch: " + name); + } + return option_value->GetValue(); + } + + bool Parse(int argc, const char* const* argv) { + if (argc < 1) { + errors_.push_back("No options"); + return false; + } + + command_name_ = argv[0]; + for (auto i = 1; i < argc; i++) { + const std::string arg(argv[i]); + + // normal options: e.g. --bundle=/data/sample/bundle --fullscreen + if (arg.length() > 2 && + arg.substr(0, 2).compare(kOptionStyleNormal) == 0) { + const size_t option_value_len = arg.find("=") != std::string::npos + ? (arg.length() - arg.find("=")) + : 0; + const bool has_value = option_value_len != 0; + std::string option_name = + arg.substr(2, arg.length() - 2 - option_value_len); + + if (options_.find(option_name) == options_.end()) { + errors_.push_back("Not found option: " + option_name); + continue; + } + + if (!has_value && options_[option_name]->IsRequiredValue()) { + errors_.push_back(option_name + " requres an option value"); + continue; + } + + if (has_value && !options_[option_name]->IsRequiredValue()) { + errors_.push_back(option_name + " doesn't requres an option value"); + continue; + } + + if (has_value) { + SetOptionValue(option_name, arg.substr(arg.find("=") + 1)); + } else { + SetOption(option_name); + } + } + // short options: e.g. -f /foo/file.txt -h 640 -abc + else if (arg.length() > 1 && + arg.substr(0, 1).compare(kOptionStyleShort) == 0) { + for (size_t j = 1; j < arg.length(); j++) { + const std::string option_name{argv[i][j]}; + + if (lut_short_options_.find(option_name) == + lut_short_options_.end()) { + errors_.push_back("Not found short option: " + option_name); + break; + } + + if (j == arg.length() - 1 && + options_[lut_short_options_[option_name]]->IsRequiredValue()) { + if (i == argc - 1) { + errors_.push_back("Invalid format option: " + option_name); + break; + } + SetOptionValue(lut_short_options_[option_name], argv[++i]); + } else { + SetOption(lut_short_options_[option_name]); + } + } + } else { + errors_.push_back("Invalid format option: " + arg); + } + } + + for (size_t i = 0; i < registration_order_options_.size(); i++) { + if (registration_order_options_[i]->IsRequired() && + !registration_order_options_[i]->HasValue()) { + errors_.push_back( + std::string(registration_order_options_[i]->GetName()) + + " option is mandatory."); + } + } + + return errors_.size() == 0; + } + + std::string GetError() { return errors_.size() > 0 ? errors_[0] : ""; } + + std::vector& GetErrors() { return errors_; } + + std::string ShowHelp() { + std::ostringstream ostream; + + ostream << "Usage: " << command_name_ << " "; + for (size_t i = 0; i < registration_order_options_.size(); i++) { + if (registration_order_options_[i]->IsRequired()) { + ostream << registration_order_options_[i]->GetHelpShortMessage() << " "; + } + } + ostream << std::endl; + + ostream << "Global options:" << std::endl; + size_t max_name_len = 0; + for (size_t i = 0; i < registration_order_options_.size(); i++) { + max_name_len = std::max( + max_name_len, registration_order_options_[i]->GetName().length()); + } + + for (size_t i = 0; i < registration_order_options_.size(); i++) { + if (!registration_order_options_[i]->GetShortName().empty()) { + ostream << kOptionStyleShort + << registration_order_options_[i]->GetShortName() << ", "; + } else { + ostream << std::string(4, ' '); + } + + size_t index_adjust = 0; + constexpr int kSpacerNum = 5; + auto need_value = registration_order_options_[i]->IsRequiredValue(); + ostream << kOptionStyleNormal + << registration_order_options_[i]->GetName(); + if (need_value) { + ostream << kOptionValueForHelpMessage; + index_adjust += std::string(kOptionValueForHelpMessage).length(); + } + ostream << std::string( + max_name_len + kSpacerNum - index_adjust - + registration_order_options_[i]->GetName().length(), + ' '); + ostream << registration_order_options_[i]->GetDescription() << std::endl; + } + + return ostream.str(); + } + + private: + struct ReaderInt { + int operator()(const std::string& value) { return std::stoi(value); } + }; + + struct ReaderString { + std::string operator()(const std::string& value) { return value; } + }; + + class Option { + public: + Option(const std::string& name, const std::string& short_name, + const std::string& description, bool required, bool required_value) + : name_(name), + short_name_(short_name), + description_(description), + is_required_(required), + is_required_value_(required_value), + value_set_(false){}; + virtual ~Option() = default; + + const std::string& GetName() const { return name_; }; + + const std::string& GetShortName() const { return short_name_; }; + + const std::string& GetDescription() const { return description_; }; + + const std::string GetHelpShortMessage() const { + std::string message = kOptionStyleNormal + name_; + if (is_required_value_) { + message += kOptionValueForHelpMessage; + } + return message; + } + + bool IsRequired() const { return is_required_; }; + + bool IsRequiredValue() const { return is_required_value_; }; + + void Set() { value_set_ = true; }; + + virtual bool SetValue(const std::string& value) = 0; + + virtual bool HasValue() const = 0; + + protected: + std::string name_; + std::string short_name_; + std::string description_; + bool is_required_; + bool is_required_value_; + bool value_set_; + }; + + template + class OptionValue : public Option { + public: + OptionValue(const std::string& name, const std::string& short_name, + const std::string& description, const T& default_value, + bool required, bool required_value) + : Option(name, short_name, description, required, required_value), + default_value_(default_value), + value_(default_value){}; + virtual ~OptionValue() = default; + + bool SetValue(const std::string& value) { + value_ = Read(value); + value_set_ = true; + return true; + } + + bool HasValue() const { return value_set_; } + + const T& GetValue() const { return value_; } + + protected: + virtual T Read(const std::string& s) = 0; + + T default_value_; + T value_; + }; + + template + class OptionValueReader : public OptionValue { + public: + OptionValueReader(const std::string& name, const std::string& short_name, + const std::string& description, const T default_value, + F reader, bool required, bool required_value) + : OptionValue(name, short_name, description, default_value, required, + required_value), + reader_(reader) {} + ~OptionValueReader() = default; + + private: + T Read(const std::string& value) { return reader_(value); } + + F reader_; + }; + + bool SetOption(const std::string& name) { + auto itr = options_.find(name); + if (itr == options_.end()) { + errors_.push_back("Unknown option: " + name); + return false; + } + + itr->second->Set(); + return true; + } + + bool SetOptionValue(const std::string& name, const std::string& value) { + auto itr = options_.find(name); + if (itr == options_.end()) { + errors_.push_back("Unknown option: " + name); + return false; + } + + if (!itr->second->SetValue(value)) { + errors_.push_back("Invalid option value: " + name + " = " + value); + return false; + } + return true; + } + + std::string command_name_; + std::unordered_map> options_; + std::unordered_map lut_short_options_; + std::vector registration_order_options_; + std::vector errors_; +}; + +} // namespace commandline + +#endif // COMMAND_OPTIONS_ diff --git a/packages/camera/example/elinux/runner/flutter_embedder_options.h b/packages/camera/example/elinux/runner/flutter_embedder_options.h new file mode 100644 index 0000000..636fdda --- /dev/null +++ b/packages/camera/example/elinux/runner/flutter_embedder_options.h @@ -0,0 +1,103 @@ +// Copyright 2021 Sony Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_EMBEDDER_OPTIONS_ +#define FLUTTER_EMBEDDER_OPTIONS_ + +#include + +#include + +#include "command_options.h" + +class FlutterEmbedderOptions { + public: + FlutterEmbedderOptions() { + options_.AddString("bundle", "b", "Path to Flutter app bundle", "./", + false); + options_.AddWithoutValue("no-cursor", "n", "No mouse cursor/pointer", + false); +#if defined(FLUTTER_TARGET_BACKEND_GBM) || \ + defined(FLUTTER_TARGET_BACKEND_EGLSTREAM) + // no more options. +#elif defined(FLUTTER_TARGET_BACKEND_X11) + options_.AddWithoutValue("fullscreen", "f", "Always full-screen display", + false); + options_.AddInt("width", "w", "Flutter app window width", 1280, false); + options_.AddInt("height", "h", "Flutter app window height", 720, false); +#else // FLUTTER_TARGET_BACKEND_WAYLAND + options_.AddWithoutValue("onscreen-keyboard", "k", + "Enable on-screen keyboard", false); + options_.AddWithoutValue("window-decoration", "d", + "Enable window decorations", false); + options_.AddWithoutValue("fullscreen", "f", "Always full-screen display", + false); + options_.AddInt("width", "w", "Flutter app window width", 1280, false); + options_.AddInt("height", "h", "Flutter app window height", 720, false); +#endif + } + ~FlutterEmbedderOptions() = default; + + bool Parse(int argc, char** argv) { + if (!options_.Parse(argc, argv)) { + std::cerr << options_.GetError() << std::endl; + std::cout << options_.ShowHelp(); + return false; + } + + bundle_path_ = options_.GetValue("bundle"); + use_mouse_cursor_ = !options_.Exist("no-cursor"); + +#if defined(FLUTTER_TARGET_BACKEND_GBM) || \ + defined(FLUTTER_TARGET_BACKEND_EGLSTREAM) + use_onscreen_keyboard_ = false; + use_window_decoration_ = false; + window_view_mode_ = flutter::FlutterViewController::ViewMode::kFullscreen; +#elif defined(FLUTTER_TARGET_BACKEND_X11) + use_onscreen_keyboard_ = false; + use_window_decoration_ = false; + window_view_mode_ = + options_.Exist("fullscreen") + ? flutter::FlutterViewController::ViewMode::kFullscreen + : flutter::FlutterViewController::ViewMode::kNormal; + window_width_ = options_.GetValue("width"); + window_height_ = options_.GetValue("height"); +#else // FLUTTER_TARGET_BACKEND_WAYLAND + use_onscreen_keyboard_ = options_.Exist("onscreen-keyboard"); + use_window_decoration_ = options_.Exist("window-decoration"); + window_view_mode_ = + options_.Exist("fullscreen") + ? flutter::FlutterViewController::ViewMode::kFullscreen + : flutter::FlutterViewController::ViewMode::kNormal; + window_width_ = options_.GetValue("width"); + window_height_ = options_.GetValue("height"); +#endif + + return true; + } + + std::string BundlePath() const { return bundle_path_; } + bool IsUseMouseCursor() const { return use_mouse_cursor_; } + bool IsUseOnscreenKeyboard() const { return use_onscreen_keyboard_; } + bool IsUseWindowDecoraation() const { return use_window_decoration_; } + flutter::FlutterViewController::ViewMode WindowViewMode() const { + return window_view_mode_; + } + int WindowWidth() const { return window_width_; } + int WindowHeight() const { return window_height_; } + + private: + commandline::CommandOptions options_; + + std::string bundle_path_; + bool use_mouse_cursor_ = true; + bool use_onscreen_keyboard_ = false; + bool use_window_decoration_ = false; + flutter::FlutterViewController::ViewMode window_view_mode_ = + flutter::FlutterViewController::ViewMode::kNormal; + int window_width_ = 1280; + int window_height_ = 720; +}; + +#endif // FLUTTER_EMBEDDER_OPTIONS_ diff --git a/packages/camera/example/elinux/runner/flutter_window.cc b/packages/camera/example/elinux/runner/flutter_window.cc new file mode 100644 index 0000000..0c5b639 --- /dev/null +++ b/packages/camera/example/elinux/runner/flutter_window.cc @@ -0,0 +1,79 @@ +// Copyright 2021 Sony Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter_window.h" + +#include +#include +#include +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow( + const flutter::FlutterViewController::ViewProperties view_properties, + const flutter::DartProject project) + : view_properties_(view_properties), project_(project) {} + +bool FlutterWindow::OnCreate() { + flutter_view_controller_ = std::make_unique( + view_properties_, project_); + + // Ensure that basic setup of the controller was successful. + if (!flutter_view_controller_->engine() || + !flutter_view_controller_->view()) { + return false; + } + + // Register Flutter plugins. + RegisterPlugins(flutter_view_controller_->engine()); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_view_controller_) { + flutter_view_controller_ = nullptr; + } +} + +void FlutterWindow::Run() { + // Main loop. + auto next_flutter_event_time = + std::chrono::steady_clock::time_point::clock::now(); + while (flutter_view_controller_->view()->DispatchEvent()) { + // Wait until the next event. + { + auto wait_duration = + std::max(std::chrono::nanoseconds(0), + next_flutter_event_time - + std::chrono::steady_clock::time_point::clock::now()); + std::this_thread::sleep_for( + std::chrono::duration_cast(wait_duration)); + } + + // Processes any pending events in the Flutter engine, and returns the + // number of nanoseconds until the next scheduled event (or max, if none). + auto wait_duration = flutter_view_controller_->engine()->ProcessMessages(); + { + auto next_event_time = std::chrono::steady_clock::time_point::max(); + if (wait_duration != std::chrono::nanoseconds::max()) { + next_event_time = + std::min(next_event_time, + std::chrono::steady_clock::time_point::clock::now() + + wait_duration); + } else { + // Wait for the next frame if no events. + auto frame_rate = flutter_view_controller_->view()->GetFrameRate(); + next_event_time = std::min( + next_event_time, + std::chrono::steady_clock::time_point::clock::now() + + std::chrono::milliseconds( + static_cast(std::trunc(1000000.0 / frame_rate)))); + } + next_flutter_event_time = + std::max(next_flutter_event_time, next_event_time); + } + } +} diff --git a/packages/camera/example/elinux/runner/flutter_window.h b/packages/camera/example/elinux/runner/flutter_window.h new file mode 100644 index 0000000..20b9cb8 --- /dev/null +++ b/packages/camera/example/elinux/runner/flutter_window.h @@ -0,0 +1,34 @@ +// Copyright 2021 Sony Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_WINDOW_ +#define FLUTTER_WINDOW_ + +#include +#include + +#include + +class FlutterWindow { + public: + explicit FlutterWindow( + const flutter::FlutterViewController::ViewProperties view_properties, + const flutter::DartProject project); + ~FlutterWindow() = default; + + // Prevent copying. + FlutterWindow(FlutterWindow const&) = delete; + FlutterWindow& operator=(FlutterWindow const&) = delete; + + bool OnCreate(); + void OnDestroy(); + void Run(); + + private: + flutter::FlutterViewController::ViewProperties view_properties_; + flutter::DartProject project_; + std::unique_ptr flutter_view_controller_; +}; + +#endif // FLUTTER_WINDOW_ diff --git a/packages/camera/example/elinux/runner/main.cc b/packages/camera/example/elinux/runner/main.cc new file mode 100644 index 0000000..92a6513 --- /dev/null +++ b/packages/camera/example/elinux/runner/main.cc @@ -0,0 +1,45 @@ +// Copyright 2021 Sony Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include + +#include +#include +#include + +#include "flutter_embedder_options.h" +#include "flutter_window.h" + +int main(int argc, char** argv) { + FlutterEmbedderOptions options; + if (!options.Parse(argc, argv)) { + return 0; + } + + // Creates the Flutter project. + const auto bundle_path = options.BundlePath(); + const std::wstring fl_path(bundle_path.begin(), bundle_path.end()); + flutter::DartProject project(fl_path); + auto command_line_arguments = std::vector(); + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + flutter::FlutterViewController::ViewProperties view_properties = {}; + view_properties.width = options.WindowWidth(); + view_properties.height = options.WindowHeight(); + view_properties.view_mode = options.WindowViewMode(); + view_properties.use_mouse_cursor = options.IsUseMouseCursor(); + view_properties.use_onscreen_keyboard = options.IsUseOnscreenKeyboard(); + view_properties.use_window_decoration = options.IsUseWindowDecoraation(); + + // The Flutter instance hosted by this window. + FlutterWindow window(view_properties, project); + if (!window.OnCreate()) { + return 0; + } + window.Run(); + window.OnDestroy(); + + return 0; +} diff --git a/packages/camera/example/integration_test/camera_test.dart b/packages/camera/example/integration_test/camera_test.dart new file mode 100644 index 0000000..00a1714 --- /dev/null +++ b/packages/camera/example/integration_test/camera_test.dart @@ -0,0 +1,235 @@ +// 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:async'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:camera/camera.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:video_player/video_player.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + late Directory testDir; + + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + final Directory extDir = await getTemporaryDirectory(); + testDir = await Directory('${extDir.path}/test').create(recursive: true); + }); + + tearDownAll(() async { + await testDir.delete(recursive: true); + }); + + final Map presetExpectedSizes = + { + ResolutionPreset.low: + Platform.isAndroid ? const Size(240, 320) : const Size(288, 352), + ResolutionPreset.medium: + Platform.isAndroid ? const Size(480, 720) : const Size(480, 640), + ResolutionPreset.high: const Size(720, 1280), + ResolutionPreset.veryHigh: const Size(1080, 1920), + ResolutionPreset.ultraHigh: const Size(2160, 3840), + // Don't bother checking for max here since it could be anything. + }; + + /// Verify that [actual] has dimensions that are at least as large as + /// [expectedSize]. Allows for a mismatch in portrait vs landscape. Returns + /// whether the dimensions exactly match. + bool assertExpectedDimensions(Size expectedSize, Size actual) { + expect(actual.shortestSide, lessThanOrEqualTo(expectedSize.shortestSide)); + expect(actual.longestSide, lessThanOrEqualTo(expectedSize.longestSide)); + return actual.shortestSide == expectedSize.shortestSide && + actual.longestSide == expectedSize.longestSide; + } + + // This tests that the capture is no bigger than the preset, since we have + // automatic code to fall back to smaller sizes when we need to. Returns + // whether the image is exactly the desired resolution. + Future testCaptureImageResolution( + CameraController controller, ResolutionPreset preset) async { + final Size expectedSize = presetExpectedSizes[preset]!; + print( + 'Capturing photo at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); + + // Take Picture + final file = await controller.takePicture(); + + // Load picture + final File fileImage = File(file.path); + final Image image = await decodeImageFromList(fileImage.readAsBytesSync()); + + // Verify image dimensions are as expected + expect(image, isNotNull); + return assertExpectedDimensions( + expectedSize, Size(image.height.toDouble(), image.width.toDouble())); + } + + testWidgets('Capture specific image resolutions', + (WidgetTester tester) async { + final List cameras = await availableCameras(); + if (cameras.isEmpty) { + return; + } + for (CameraDescription cameraDescription in cameras) { + bool previousPresetExactlySupported = true; + for (MapEntry preset + in presetExpectedSizes.entries) { + final CameraController controller = + CameraController(cameraDescription, preset.key); + await controller.initialize(); + final bool presetExactlySupported = + await testCaptureImageResolution(controller, preset.key); + assert(!(!previousPresetExactlySupported && presetExactlySupported), + 'The camera took higher resolution pictures at a lower resolution.'); + previousPresetExactlySupported = presetExactlySupported; + await controller.dispose(); + } + } + }, skip: !Platform.isAndroid); + + // This tests that the capture is no bigger than the preset, since we have + // automatic code to fall back to smaller sizes when we need to. Returns + // whether the image is exactly the desired resolution. + Future testCaptureVideoResolution( + CameraController controller, ResolutionPreset preset) async { + final Size expectedSize = presetExpectedSizes[preset]!; + print( + 'Capturing video at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); + + // Take Video + await controller.startVideoRecording(); + sleep(const Duration(milliseconds: 300)); + final file = await controller.stopVideoRecording(); + + // Load video metadata + final File videoFile = File(file.path); + final VideoPlayerController videoController = + VideoPlayerController.file(videoFile); + await videoController.initialize(); + final Size video = videoController.value.size; + + // Verify image dimensions are as expected + expect(video, isNotNull); + return assertExpectedDimensions( + expectedSize, Size(video.height, video.width)); + } + + testWidgets('Capture specific video resolutions', + (WidgetTester tester) async { + final List cameras = await availableCameras(); + if (cameras.isEmpty) { + return; + } + for (CameraDescription cameraDescription in cameras) { + bool previousPresetExactlySupported = true; + for (MapEntry preset + in presetExpectedSizes.entries) { + final CameraController controller = + CameraController(cameraDescription, preset.key); + await controller.initialize(); + await controller.prepareForVideoRecording(); + final bool presetExactlySupported = + await testCaptureVideoResolution(controller, preset.key); + assert(!(!previousPresetExactlySupported && presetExactlySupported), + 'The camera took higher resolution pictures at a lower resolution.'); + previousPresetExactlySupported = presetExactlySupported; + await controller.dispose(); + } + } + }, skip: !Platform.isAndroid); + + testWidgets('Pause and resume video recording', (WidgetTester tester) async { + final List cameras = await availableCameras(); + if (cameras.isEmpty) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + await controller.prepareForVideoRecording(); + + int startPause; + int timePaused = 0; + + await controller.startVideoRecording(); + final int recordingStart = DateTime.now().millisecondsSinceEpoch; + sleep(const Duration(milliseconds: 500)); + + await controller.pauseVideoRecording(); + startPause = DateTime.now().millisecondsSinceEpoch; + sleep(const Duration(milliseconds: 500)); + await controller.resumeVideoRecording(); + timePaused += DateTime.now().millisecondsSinceEpoch - startPause; + + sleep(const Duration(milliseconds: 500)); + + await controller.pauseVideoRecording(); + startPause = DateTime.now().millisecondsSinceEpoch; + sleep(const Duration(milliseconds: 500)); + await controller.resumeVideoRecording(); + timePaused += DateTime.now().millisecondsSinceEpoch - startPause; + + sleep(const Duration(milliseconds: 500)); + + final file = await controller.stopVideoRecording(); + final int recordingTime = + DateTime.now().millisecondsSinceEpoch - recordingStart; + + final File videoFile = File(file.path); + final VideoPlayerController videoController = VideoPlayerController.file( + videoFile, + ); + await videoController.initialize(); + final int duration = videoController.value.duration.inMilliseconds; + await videoController.dispose(); + + expect(duration, lessThan(recordingTime - timePaused)); + }, skip: !Platform.isAndroid); + + testWidgets( + 'Android image streaming', + (WidgetTester tester) async { + final List cameras = await availableCameras(); + if (cameras.isEmpty) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + bool _isDetecting = false; + + await controller.startImageStream((CameraImage image) { + if (_isDetecting) return; + + _isDetecting = true; + + expectLater(image, isNotNull).whenComplete(() => _isDetecting = false); + }); + + expect(controller.value.isStreamingImages, true); + + sleep(const Duration(milliseconds: 500)); + + await controller.stopImageStream(); + await controller.dispose(); + }, + skip: !Platform.isAndroid, + ); +} diff --git a/packages/camera/example/lib/main.dart b/packages/camera/example/lib/main.dart new file mode 100644 index 0000000..3d1f990 --- /dev/null +++ b/packages/camera/example/lib/main.dart @@ -0,0 +1,971 @@ +// 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. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:io'; + +import 'package:camera/camera.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +class CameraExampleHome extends StatefulWidget { + @override + _CameraExampleHomeState createState() { + return _CameraExampleHomeState(); + } +} + +/// Returns a suitable camera icon for [direction]. +IconData getCameraLensIcon(CameraLensDirection direction) { + switch (direction) { + case CameraLensDirection.back: + return Icons.camera_rear; + case CameraLensDirection.front: + return Icons.camera_front; + case CameraLensDirection.external: + return Icons.camera; + default: + throw ArgumentError('Unknown lens direction'); + } +} + +void logError(String code, String? message) { + if (message != null) { + print('Error: $code\nError Message: $message'); + } else { + print('Error: $code'); + } +} + +class _CameraExampleHomeState extends State + with WidgetsBindingObserver, TickerProviderStateMixin { + CameraController? controller; + XFile? imageFile; + XFile? videoFile; + VideoPlayerController? videoController; + VoidCallback? videoPlayerListener; + bool enableAudio = true; + double _minAvailableExposureOffset = 0.0; + double _maxAvailableExposureOffset = 0.0; + double _currentExposureOffset = 0.0; + late AnimationController _flashModeControlRowAnimationController; + late Animation _flashModeControlRowAnimation; + late AnimationController _exposureModeControlRowAnimationController; + late Animation _exposureModeControlRowAnimation; + late AnimationController _focusModeControlRowAnimationController; + late Animation _focusModeControlRowAnimation; + double _minAvailableZoom = 1.0; + double _maxAvailableZoom = 1.0; + double _currentScale = 1.0; + double _baseScale = 1.0; + + // Counting pointers (number of user fingers on screen) + int _pointers = 0; + + @override + void initState() { + super.initState(); + _ambiguate(WidgetsBinding.instance)?.addObserver(this); + + _flashModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _flashModeControlRowAnimation = CurvedAnimation( + parent: _flashModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + _exposureModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _exposureModeControlRowAnimation = CurvedAnimation( + parent: _exposureModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + _focusModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _focusModeControlRowAnimation = CurvedAnimation( + parent: _focusModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + } + + @override + void dispose() { + _ambiguate(WidgetsBinding.instance)?.removeObserver(this); + _flashModeControlRowAnimationController.dispose(); + _exposureModeControlRowAnimationController.dispose(); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + final CameraController? cameraController = controller; + + // App state changed before we got the chance to initialize. + if (cameraController == null || !cameraController.value.isInitialized) { + return; + } + + if (state == AppLifecycleState.inactive) { + cameraController.dispose(); + } else if (state == AppLifecycleState.resumed) { + onNewCameraSelected(cameraController.description); + } + } + + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: const Text('Camera example'), + ), + body: Column( + children: [ + Expanded( + child: Container( + child: Padding( + padding: const EdgeInsets.all(1.0), + child: Center( + child: _cameraPreviewWidget(), + ), + ), + decoration: BoxDecoration( + color: Colors.black, + border: Border.all( + color: + controller != null && controller!.value.isRecordingVideo + ? Colors.redAccent + : Colors.grey, + width: 3.0, + ), + ), + ), + ), + _captureControlRowWidget(), + _modeControlRowWidget(), + Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _cameraTogglesRowWidget(), + _thumbnailWidget(), + ], + ), + ), + ], + ), + ); + } + + /// Display the preview from the camera (or a message if the preview is not available). + Widget _cameraPreviewWidget() { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + return const Text( + 'Tap a camera', + style: TextStyle( + color: Colors.white, + fontSize: 24.0, + fontWeight: FontWeight.w900, + ), + ); + } else { + return Listener( + onPointerDown: (_) => _pointers++, + onPointerUp: (_) => _pointers--, + child: CameraPreview( + controller!, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onScaleStart: _handleScaleStart, + onScaleUpdate: _handleScaleUpdate, + onTapDown: (details) => onViewFinderTap(details, constraints), + ); + }), + ), + ); + } + } + + void _handleScaleStart(ScaleStartDetails details) { + _baseScale = _currentScale; + } + + Future _handleScaleUpdate(ScaleUpdateDetails details) async { + // When there are not exactly two fingers on screen don't scale + if (controller == null || _pointers != 2) { + return; + } + + _currentScale = (_baseScale * details.scale) + .clamp(_minAvailableZoom, _maxAvailableZoom); + + await controller!.setZoomLevel(_currentScale); + } + + /// Display the thumbnail of the captured image or video. + Widget _thumbnailWidget() { + final VideoPlayerController? localVideoController = videoController; + + return Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + localVideoController == null && imageFile == null + ? Container() + : SizedBox( + child: (localVideoController == null) + ? Image.file(File(imageFile!.path)) + : Container( + child: Center( + child: AspectRatio( + aspectRatio: + localVideoController.value.size != null + ? localVideoController + .value.aspectRatio + : 1.0, + child: VideoPlayer(localVideoController)), + ), + decoration: BoxDecoration( + border: Border.all(color: Colors.pink)), + ), + width: 64.0, + height: 64.0, + ), + ], + ), + ), + ); + } + + /// Display a bar with buttons to change the flash and exposure modes + Widget _modeControlRowWidget() { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + IconButton( + icon: Icon(Icons.flash_on), + color: Colors.blue, + onPressed: controller != null ? onFlashModeButtonPressed : null, + ), + IconButton( + icon: Icon(Icons.exposure), + color: Colors.blue, + onPressed: + controller != null ? onExposureModeButtonPressed : null, + ), + IconButton( + icon: Icon(Icons.filter_center_focus), + color: Colors.blue, + onPressed: controller != null ? onFocusModeButtonPressed : null, + ), + IconButton( + icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), + color: Colors.blue, + onPressed: controller != null ? onAudioModeButtonPressed : null, + ), + IconButton( + icon: Icon(controller?.value.isCaptureOrientationLocked ?? false + ? Icons.screen_lock_rotation + : Icons.screen_rotation), + color: Colors.blue, + onPressed: controller != null + ? onCaptureOrientationLockButtonPressed + : null, + ), + ], + ), + _flashModeControlRowWidget(), + _exposureModeControlRowWidget(), + _focusModeControlRowWidget(), + ], + ); + } + + Widget _flashModeControlRowWidget() { + return SizeTransition( + sizeFactor: _flashModeControlRowAnimation, + child: ClipRect( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + IconButton( + icon: Icon(Icons.flash_off), + color: controller?.value.flashMode == FlashMode.off + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.off) + : null, + ), + IconButton( + icon: Icon(Icons.flash_auto), + color: controller?.value.flashMode == FlashMode.auto + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.auto) + : null, + ), + IconButton( + icon: Icon(Icons.flash_on), + color: controller?.value.flashMode == FlashMode.always + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.always) + : null, + ), + IconButton( + icon: Icon(Icons.highlight), + color: controller?.value.flashMode == FlashMode.torch + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.torch) + : null, + ), + ], + ), + ), + ); + } + + Widget _exposureModeControlRowWidget() { + final ButtonStyle styleAuto = TextButton.styleFrom( + primary: controller?.value.exposureMode == ExposureMode.auto + ? Colors.orange + : Colors.blue, + ); + final ButtonStyle styleLocked = TextButton.styleFrom( + primary: controller?.value.exposureMode == ExposureMode.locked + ? Colors.orange + : Colors.blue, + ); + + return SizeTransition( + sizeFactor: _exposureModeControlRowAnimation, + child: ClipRect( + child: Container( + color: Colors.grey.shade50, + child: Column( + children: [ + Center( + child: Text("Exposure Mode"), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + TextButton( + child: Text('AUTO'), + style: styleAuto, + onPressed: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.auto) + : null, + onLongPress: () { + if (controller != null) { + controller!.setExposurePoint(null); + showInSnackBar('Resetting exposure point'); + } + }, + ), + TextButton( + child: Text('LOCKED'), + style: styleLocked, + onPressed: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.locked) + : null, + ), + ], + ), + Center( + child: Text("Exposure Offset"), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + Text(_minAvailableExposureOffset.toString()), + Slider( + value: _currentExposureOffset, + min: _minAvailableExposureOffset, + max: _maxAvailableExposureOffset, + label: _currentExposureOffset.toString(), + onChanged: _minAvailableExposureOffset == + _maxAvailableExposureOffset + ? null + : setExposureOffset, + ), + Text(_maxAvailableExposureOffset.toString()), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _focusModeControlRowWidget() { + final ButtonStyle styleAuto = TextButton.styleFrom( + primary: controller?.value.focusMode == FocusMode.auto + ? Colors.orange + : Colors.blue, + ); + final ButtonStyle styleLocked = TextButton.styleFrom( + primary: controller?.value.focusMode == FocusMode.locked + ? Colors.orange + : Colors.blue, + ); + + return SizeTransition( + sizeFactor: _focusModeControlRowAnimation, + child: ClipRect( + child: Container( + color: Colors.grey.shade50, + child: Column( + children: [ + Center( + child: Text("Focus Mode"), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + TextButton( + child: Text('AUTO'), + style: styleAuto, + onPressed: controller != null + ? () => onSetFocusModeButtonPressed(FocusMode.auto) + : null, + onLongPress: () { + if (controller != null) controller!.setFocusPoint(null); + showInSnackBar('Resetting focus point'); + }, + ), + TextButton( + child: Text('LOCKED'), + style: styleLocked, + onPressed: controller != null + ? () => onSetFocusModeButtonPressed(FocusMode.locked) + : null, + ), + ], + ), + ], + ), + ), + ), + ); + } + + /// Display the control bar with buttons to take pictures and record videos. + Widget _captureControlRowWidget() { + final CameraController? cameraController = controller; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + IconButton( + icon: const Icon(Icons.camera_alt), + color: Colors.blue, + onPressed: cameraController != null && + cameraController.value.isInitialized && + !cameraController.value.isRecordingVideo + ? onTakePictureButtonPressed + : null, + ), + IconButton( + icon: const Icon(Icons.videocam), + color: Colors.blue, + onPressed: cameraController != null && + cameraController.value.isInitialized && + !cameraController.value.isRecordingVideo + ? onVideoRecordButtonPressed + : null, + ), + IconButton( + icon: cameraController != null && + cameraController.value.isRecordingPaused + ? Icon(Icons.play_arrow) + : Icon(Icons.pause), + color: Colors.blue, + onPressed: cameraController != null && + cameraController.value.isInitialized && + cameraController.value.isRecordingVideo + ? (cameraController.value.isRecordingPaused) + ? onResumeButtonPressed + : onPauseButtonPressed + : null, + ), + IconButton( + icon: const Icon(Icons.stop), + color: Colors.red, + onPressed: cameraController != null && + cameraController.value.isInitialized && + cameraController.value.isRecordingVideo + ? onStopButtonPressed + : null, + ) + ], + ); + } + + /// Display a row of toggle to select the camera (or a message if no camera is available). + Widget _cameraTogglesRowWidget() { + final List toggles = []; + + final onChanged = (CameraDescription? description) { + if (description == null) { + return; + } + + onNewCameraSelected(description); + }; + + if (cameras.isEmpty) { + return const Text('No camera found'); + } else { + for (CameraDescription cameraDescription in cameras) { + toggles.add( + SizedBox( + width: 90.0, + child: RadioListTile( + title: Icon(getCameraLensIcon(cameraDescription.lensDirection)), + groupValue: controller?.description, + value: cameraDescription, + onChanged: + controller != null && controller!.value.isRecordingVideo + ? null + : onChanged, + ), + ), + ); + } + } + + return Row(children: toggles); + } + + String timestamp() => DateTime.now().millisecondsSinceEpoch.toString(); + + void showInSnackBar(String message) { + // ignore: deprecated_member_use + _scaffoldKey.currentState?.showSnackBar(SnackBar(content: Text(message))); + } + + void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { + if (controller == null) { + return; + } + + final CameraController cameraController = controller!; + + final offset = Offset( + details.localPosition.dx / constraints.maxWidth, + details.localPosition.dy / constraints.maxHeight, + ); + cameraController.setExposurePoint(offset); + cameraController.setFocusPoint(offset); + } + + void onNewCameraSelected(CameraDescription cameraDescription) async { + if (controller != null) { + await controller!.dispose(); + } + final CameraController cameraController = CameraController( + cameraDescription, + ResolutionPreset.medium, + enableAudio: enableAudio, + imageFormatGroup: ImageFormatGroup.jpeg, + ); + controller = cameraController; + + // If the controller is updated then update the UI. + cameraController.addListener(() { + if (mounted) setState(() {}); + if (cameraController.value.hasError) { + showInSnackBar( + 'Camera error ${cameraController.value.errorDescription}'); + } + }); + + try { + await cameraController.initialize(); + await Future.wait([ + cameraController + .getMinExposureOffset() + .then((value) => _minAvailableExposureOffset = value), + cameraController + .getMaxExposureOffset() + .then((value) => _maxAvailableExposureOffset = value), + cameraController + .getMaxZoomLevel() + .then((value) => _maxAvailableZoom = value), + cameraController + .getMinZoomLevel() + .then((value) => _minAvailableZoom = value), + ]); + } on CameraException catch (e) { + _showCameraException(e); + } + + if (mounted) { + setState(() {}); + } + } + + void onTakePictureButtonPressed() { + takePicture().then((XFile? file) { + if (mounted) { + setState(() { + imageFile = file; + videoController?.dispose(); + videoController = null; + }); + if (file != null) showInSnackBar('Picture saved to ${file.path}'); + } + }); + } + + void onFlashModeButtonPressed() { + if (_flashModeControlRowAnimationController.value == 1) { + _flashModeControlRowAnimationController.reverse(); + } else { + _flashModeControlRowAnimationController.forward(); + _exposureModeControlRowAnimationController.reverse(); + _focusModeControlRowAnimationController.reverse(); + } + } + + void onExposureModeButtonPressed() { + if (_exposureModeControlRowAnimationController.value == 1) { + _exposureModeControlRowAnimationController.reverse(); + } else { + _exposureModeControlRowAnimationController.forward(); + _flashModeControlRowAnimationController.reverse(); + _focusModeControlRowAnimationController.reverse(); + } + } + + void onFocusModeButtonPressed() { + if (_focusModeControlRowAnimationController.value == 1) { + _focusModeControlRowAnimationController.reverse(); + } else { + _focusModeControlRowAnimationController.forward(); + _flashModeControlRowAnimationController.reverse(); + _exposureModeControlRowAnimationController.reverse(); + } + } + + void onAudioModeButtonPressed() { + enableAudio = !enableAudio; + if (controller != null) { + onNewCameraSelected(controller!.description); + } + } + + void onCaptureOrientationLockButtonPressed() async { + if (controller != null) { + final CameraController cameraController = controller!; + if (cameraController.value.isCaptureOrientationLocked) { + await cameraController.unlockCaptureOrientation(); + showInSnackBar('Capture orientation unlocked'); + } else { + await cameraController.lockCaptureOrientation(); + showInSnackBar( + 'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}'); + } + } + } + + void onSetFlashModeButtonPressed(FlashMode mode) { + setFlashMode(mode).then((_) { + if (mounted) setState(() {}); + showInSnackBar('Flash mode set to ${mode.toString().split('.').last}'); + }); + } + + void onSetExposureModeButtonPressed(ExposureMode mode) { + setExposureMode(mode).then((_) { + if (mounted) setState(() {}); + showInSnackBar('Exposure mode set to ${mode.toString().split('.').last}'); + }); + } + + void onSetFocusModeButtonPressed(FocusMode mode) { + setFocusMode(mode).then((_) { + if (mounted) setState(() {}); + showInSnackBar('Focus mode set to ${mode.toString().split('.').last}'); + }); + } + + void onVideoRecordButtonPressed() { + startVideoRecording().then((_) { + if (mounted) setState(() {}); + }); + } + + void onStopButtonPressed() { + stopVideoRecording().then((file) { + if (mounted) setState(() {}); + if (file != null) { + showInSnackBar('Video recorded to ${file.path}'); + videoFile = file; + _startVideoPlayer(); + } + }); + } + + void onPauseButtonPressed() { + pauseVideoRecording().then((_) { + if (mounted) setState(() {}); + showInSnackBar('Video recording paused'); + }); + } + + void onResumeButtonPressed() { + resumeVideoRecording().then((_) { + if (mounted) setState(() {}); + showInSnackBar('Video recording resumed'); + }); + } + + Future startVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return; + } + + if (cameraController.value.isRecordingVideo) { + // A recording is already started, do nothing. + return; + } + + try { + await cameraController.startVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + return; + } + } + + Future stopVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isRecordingVideo) { + return null; + } + + try { + return cameraController.stopVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + + Future pauseVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isRecordingVideo) { + return null; + } + + try { + await cameraController.pauseVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future resumeVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isRecordingVideo) { + return null; + } + + try { + await cameraController.resumeVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setFlashMode(FlashMode mode) async { + if (controller == null) { + return; + } + + try { + await controller!.setFlashMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setExposureMode(ExposureMode mode) async { + if (controller == null) { + return; + } + + try { + await controller!.setExposureMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setExposureOffset(double offset) async { + if (controller == null) { + return; + } + + setState(() { + _currentExposureOffset = offset; + }); + try { + offset = await controller!.setExposureOffset(offset); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setFocusMode(FocusMode mode) async { + if (controller == null) { + return; + } + + try { + await controller!.setFocusMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future _startVideoPlayer() async { + if (videoFile == null) { + return; + } + + final VideoPlayerController vController = + VideoPlayerController.file(File(videoFile!.path)); + videoPlayerListener = () { + if (videoController != null && videoController!.value.size != null) { + // Refreshing the state to update video player with the correct ratio. + if (mounted) setState(() {}); + videoController!.removeListener(videoPlayerListener!); + } + }; + vController.addListener(videoPlayerListener!); + await vController.setLooping(true); + await vController.initialize(); + await videoController?.dispose(); + if (mounted) { + setState(() { + imageFile = null; + videoController = vController; + }); + } + await vController.play(); + } + + Future takePicture() async { + final CameraController? cameraController = controller; + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return null; + } + + if (cameraController.value.isTakingPicture) { + // A capture is already pending, do nothing. + return null; + } + + try { + XFile file = await cameraController.takePicture(); + return file; + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + + void _showCameraException(CameraException e) { + logError(e.code, e.description); + showInSnackBar('Error: ${e.code}\n${e.description}'); + } +} + +class CameraApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + scrollBehavior: MyCustomScrollBehavior(), + home: CameraExampleHome(), + ); + } +} + +class MyCustomScrollBehavior extends MaterialScrollBehavior { + // Override behavior methods and getters like dragDevices + @override + Set get dragDevices => { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }; +} + +List cameras = []; + +Future main() async { + // Fetch the available cameras before initializing the app. + try { + WidgetsFlutterBinding.ensureInitialized(); + cameras = await availableCameras(); + } on CameraException catch (e) { + logError(e.code, e.description); + } + runApp(CameraApp()); +} + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; diff --git a/packages/camera/example/pubspec.yaml b/packages/camera/example/pubspec.yaml new file mode 100644 index 0000000..75850fb --- /dev/null +++ b/packages/camera/example/pubspec.yaml @@ -0,0 +1,32 @@ +name: camera_example +description: Demonstrates how to use the camera plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.22.0" + +dependencies: + camera: ^0.8.1+7 + camera_elinux: + path: ../ + flutter: + sdk: flutter + path_provider: ^2.0.0 + path_provider_elinux: + path: ../../path_provider + video_player: ^2.1.4 + video_player_elinux: + path: ../../video_player + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true diff --git a/packages/camera/example/test_driver/integration_test.dart b/packages/camera/example/test_driver/integration_test.dart new file mode 100644 index 0000000..dedb253 --- /dev/null +++ b/packages/camera/example/test_driver/integration_test.dart @@ -0,0 +1,64 @@ +// 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:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_driver/flutter_driver.dart'; + +const String _examplePackage = 'io.flutter.plugins.cameraexample'; + +Future main() async { + if (!(Platform.isLinux || Platform.isMacOS)) { + print('This test must be run on a POSIX host. Skipping...'); + exit(0); + } + final bool adbExists = + Process.runSync('which', ['adb']).exitCode == 0; + if (!adbExists) { + print('This test needs ADB to exist on the \$PATH. Skipping...'); + exit(0); + } + print('Granting camera permissions...'); + Process.runSync('adb', [ + 'shell', + 'pm', + 'grant', + _examplePackage, + 'android.permission.CAMERA' + ]); + Process.runSync('adb', [ + 'shell', + 'pm', + 'grant', + _examplePackage, + 'android.permission.RECORD_AUDIO' + ]); + print('Starting test.'); + final FlutterDriver driver = await FlutterDriver.connect(); + final String data = await driver.requestData( + null, + timeout: const Duration(minutes: 1), + ); + await driver.close(); + print('Test finished. Revoking camera permissions...'); + Process.runSync('adb', [ + 'shell', + 'pm', + 'revoke', + _examplePackage, + 'android.permission.CAMERA' + ]); + Process.runSync('adb', [ + 'shell', + 'pm', + 'revoke', + _examplePackage, + 'android.permission.RECORD_AUDIO' + ]); + + final Map result = jsonDecode(data); + exit(result['result'] == 'true' ? 0 : 1); +} diff --git a/packages/camera/pubspec.yaml b/packages/camera/pubspec.yaml new file mode 100644 index 0000000..404a5a2 --- /dev/null +++ b/packages/camera/pubspec.yaml @@ -0,0 +1,21 @@ +name: camera_elinux +description: A Flutter plugin for getting information about and controlling the + camera on eLinux. Supports previewing the camera feed, capturing images, capturing video, + and streaming image buffers to dart. +version: 0.1.0 +homepage: https://github.com/sony/flutter-elinux-plugins +repository: https://github.com/sony/flutter-elinux-plugins/tree/main/packages/camera + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + +flutter: + plugin: + platforms: + elinux: + pluginClass: CameraElinuxPlugin