add plugin camera desktop
159
plugins/camera_desktop/.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
- run: flutter pub get
|
||||||
|
- run: flutter analyze
|
||||||
|
|
||||||
|
test-dart:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
- run: flutter pub get
|
||||||
|
- run: flutter test
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
- name: Install Linux dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
clang cmake ninja-build pkg-config \
|
||||||
|
libgtk-3-dev \
|
||||||
|
libgstreamer1.0-dev \
|
||||||
|
libgstreamer-plugins-base1.0-dev \
|
||||||
|
gstreamer1.0-plugins-good \
|
||||||
|
libmpv-dev
|
||||||
|
- run: cd example && flutter build linux
|
||||||
|
|
||||||
|
build-macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
- run: cd example && flutter build macos
|
||||||
|
|
||||||
|
test-macos-native:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
- run: cd example && flutter build macos --debug
|
||||||
|
- name: Run XCTests
|
||||||
|
run: |
|
||||||
|
cd example/macos
|
||||||
|
xcodebuild test \
|
||||||
|
-workspace Runner.xcworkspace \
|
||||||
|
-scheme Runner \
|
||||||
|
-configuration Debug \
|
||||||
|
-quiet
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
- uses: ilammy/msvc-dev-cmd@v1
|
||||||
|
|
||||||
|
- name: Verify /utf-8 is set for MSVC in windows/CMakeLists.txt
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
grep -q '/utf-8' windows/CMakeLists.txt \
|
||||||
|
|| { echo "::error::windows/CMakeLists.txt is missing /utf-8"; exit 1; }
|
||||||
|
|
||||||
|
- name: Verify ci/cp936_repro.cpp covers every non-ASCII char in windows/
|
||||||
|
shell: bash
|
||||||
|
run: python ci/check_unicode_inventory.py
|
||||||
|
|
||||||
|
- name: Verify ci/cp936_repro.cpp has no UTF-8 BOM
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
bom=$(head -c 3 ci/cp936_repro.cpp | od -An -tx1 | tr -d ' \n')
|
||||||
|
if [ "$bom" = "efbbbf" ]; then
|
||||||
|
echo "::error::ci/cp936_repro.cpp must not have a BOM (MSVC would auto-detect UTF-8 and bypass /source-charset:.936)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK: no BOM"
|
||||||
|
|
||||||
|
- name: CP936 simulation without /utf-8 must fail with C4819/C2220
|
||||||
|
shell: cmd
|
||||||
|
run: |
|
||||||
|
cl /c /WX /source-charset:.936 /execution-charset:.936 /nologo ci\cp936_repro.cpp > cl.log 2>&1
|
||||||
|
set CL_EXIT=%errorlevel%
|
||||||
|
type cl.log
|
||||||
|
if %CL_EXIT% equ 0 (
|
||||||
|
echo ::error::Expected C4819/C2220 but compile succeeded, CP936 simulation is not triggering the bug
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
findstr /c:"C4819" cl.log >nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ::error::cl.exe failed but did not emit C4819, test is not reproducing the real bug
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
findstr /c:"C2220" cl.log >nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ::error::cl.exe failed but did not emit C2220, /WX promotion is not working as expected
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo OK: CP936 simulation reproduced C4819/C2220 as expected
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
# Note: MSVC refuses `/source-charset:.936` together with `/utf-8`
|
||||||
|
# (error D8016: options are incompatible). On a real Chinese Windows
|
||||||
|
# host, CP936 is NOT a flag; it's an implicit default from GetACP().
|
||||||
|
# `/utf-8` overrides that implicit default. We prove the fix in two
|
||||||
|
# independent invocations: the previous step shows CP936 is hostile
|
||||||
|
# to our bytes; this step shows `/utf-8` makes MSVC read them as UTF-8
|
||||||
|
# and compile cleanly with /WX. Together they imply that on a real
|
||||||
|
# CP936 host, adding `/utf-8` switches MSVC from CP936-mode (fail)
|
||||||
|
# to UTF-8-mode (pass).
|
||||||
|
- name: Compile with /utf-8 must succeed (proves /utf-8 resolves our chars)
|
||||||
|
shell: cmd
|
||||||
|
run: cl /c /WX /utf-8 /nologo ci\cp936_repro.cpp
|
||||||
|
|
||||||
|
- name: Build the example (generates the plugin vcxproj)
|
||||||
|
run: cd example && flutter build windows
|
||||||
|
|
||||||
|
- name: Verify /utf-8 is threaded into the generated plugin vcxproj
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
vcxproj=$(find example/build/windows -name 'camera_desktop_plugin.vcxproj' | head -n1)
|
||||||
|
if [ -z "$vcxproj" ]; then
|
||||||
|
echo "::error::camera_desktop_plugin.vcxproj not found under example/build/windows"
|
||||||
|
find example/build/windows -name '*.vcxproj' || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Inspecting: $vcxproj"
|
||||||
|
if ! grep -q '/utf-8' "$vcxproj"; then
|
||||||
|
echo "::error::/utf-8 missing from $vcxproj, CMake did not thread the flag through"
|
||||||
|
echo "--- vcxproj contents ---"
|
||||||
|
cat "$vcxproj"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK: /utf-8 present in $vcxproj"
|
||||||
36
plugins/camera_desktop/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
/pubspec.lock
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
/build/
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
# C++ test build artifacts
|
||||||
|
linux/test/build/
|
||||||
30
plugins/camera_desktop/.metadata
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "90673a4eef275d1a6692c26ac80d6d746d41a73a"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: plugin
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
- platform: linux
|
||||||
|
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
15
plugins/camera_desktop/.pubignore
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Build / tooling artifacts
|
||||||
|
.dart_tool/
|
||||||
|
build/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# Dev/CI helper scripts, not needed by package consumers.
|
||||||
|
ci/
|
||||||
|
|
||||||
|
# Example Podfile (not needed in the published tarball).
|
||||||
|
example/**/Podfile
|
||||||
|
example/**/Podfile.lock
|
||||||
|
|
||||||
|
# Internal working notes, not for the published package.
|
||||||
|
NEW_SUPPORT_ROADMAP.md
|
||||||
146
plugins/camera_desktop/CHANGELOG.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
## 1.2.1
|
||||||
|
|
||||||
|
* Fix Linux preview `Internal data stream error` (`not-negotiated`) on cameras that do not expose MJPEG, such as NV12/YUYV-only USB webcams. The MJPEG fast-path added in 1.1.7 was selected whenever its pipeline merely *parsed*, but `gst_parse_launch` succeeds even when the camera cannot produce MJPEG, so the intended raw-capture fallback never ran and initialization failed at `PLAYING`. The plugin now probes the V4L2 device (both `MJPEG` and `JPEG` pixel formats) for the target resolution before choosing the MJPEG path, and otherwise uses the always-safe raw capture path. The MJPEG pipeline pins only the resolution and lets the camera's native frame rate float, with `videorate` adapting it to the requested fps, so requesting a frame rate the camera does not expose natively in MJPEG no longer breaks negotiation.
|
||||||
|
|
||||||
|
## 1.2.0
|
||||||
|
|
||||||
|
* Speed up Windows camera initialization by completing `initialize()` as soon as the preview starts instead of blocking until the first camera frame arrives. On a typical webcam this returns control to the app in roughly 250 ms rather than about 2 seconds. The preview fills in as frames arrive, and a watchdog reports a `cameraError` if no frames are received within 8 seconds.
|
||||||
|
* Make Windows diagnostic logging opt-in and off by default so the plugin stays quiet in your app. Set the `CAMERA_DESKTOP_LOG` environment variable to any value other than `0` to capture a trace when reporting an issue. This also removes the logging overhead that slowed camera initialization, most noticeably with a debugger or IDE attached.
|
||||||
|
|
||||||
|
## 1.1.8
|
||||||
|
|
||||||
|
* Fix Windows use-after-free crashes on camera dispose: guard the preview texture against in-flight preview samples, and defer destroying the texture until Flutter's asynchronous `UnregisterTexture` completes (its raster-thread pixel-buffer callback could otherwise run after the texture was freed) (#4)
|
||||||
|
|
||||||
|
## 1.1.7
|
||||||
|
|
||||||
|
* Fix Windows crash after camera dispose by marshalling platform channel messages to the platform thread (#4)
|
||||||
|
* Fix Linux preview `not-negotiated` error on common USB webcams (#5, thanks @jvnonce)
|
||||||
|
* Fix Linux recording on systems without `x264enc` (#5, thanks @jvnonce)
|
||||||
|
* Report max frame rate across all pixel formats during Linux device enumeration (#5, thanks @jvnonce)
|
||||||
|
|
||||||
|
## 1.1.6
|
||||||
|
|
||||||
|
* Fix Swift compiler warnings: remove unused variables in DeviceEnumerator, PhotoHandler, RecordHandler, and CameraSession
|
||||||
|
|
||||||
|
## 1.1.5
|
||||||
|
|
||||||
|
* Fix SPM integration by adding missing FlutterFramework dependency to iOS and macOS Package.swift
|
||||||
|
|
||||||
|
## 1.1.4
|
||||||
|
|
||||||
|
* Update documentation
|
||||||
|
* Remove debug logging from image stream pause/resume and video recording completion
|
||||||
|
|
||||||
|
## 1.1.3
|
||||||
|
|
||||||
|
* Fix Windows build failure (C4819 / C2220) on hosts with a non-UTF-8 system code page (e.g. CP936 on Simplified Chinese Windows) by compiling the plugin with `/utf-8` under MSVC (#2)
|
||||||
|
|
||||||
|
## 1.1.2
|
||||||
|
|
||||||
|
* Fix macOS build failure on Xcode 26+ by removing unavailable `AVCaptureSessionInterruptionReasonKey` (re-introduced in 1.1.1)
|
||||||
|
* Fix Windows build failure caused by implicit `wchar_t` to `char` conversion in debug logging
|
||||||
|
|
||||||
|
## 1.1.1
|
||||||
|
|
||||||
|
* Add comprehensive diagnostic logging across all platforms (Linux, macOS, Windows)
|
||||||
|
* Log device enumeration, backend selection, pipeline construction, resolution selection, recording lifecycle, and error paths
|
||||||
|
|
||||||
|
## 1.1.0
|
||||||
|
|
||||||
|
* Add PipeWire camera portal support for Flatpak sandbox compatibility on Linux
|
||||||
|
* Automatically detect Flatpak environment and use `pipewiresrc` instead of `v4l2src`
|
||||||
|
* Request camera access via `org.freedesktop.portal.Camera` D-Bus interface
|
||||||
|
* Enumerate PipeWire camera nodes via `GstDeviceMonitor`
|
||||||
|
* Fall back to V4L2 if portal is unavailable or user denies permission
|
||||||
|
* No new build dependencies: uses GIO (D-Bus) and GStreamer APIs already linked
|
||||||
|
|
||||||
|
## 1.0.8
|
||||||
|
|
||||||
|
* Fix macOS build failure on Xcode 26+ by removing unavailable `AVCaptureSessionInterruptionReasonKey` (iOS-only API)
|
||||||
|
|
||||||
|
## 1.0.7
|
||||||
|
|
||||||
|
* Fix camera initialization failure on Intel Macs by selecting session preset after device discovery using `device.supportsSessionPreset()` with automatic fallback (1080p → 720p → high → medium)
|
||||||
|
* Make `canAddInput`/`canAddOutput` failures return a `FlutterError` instead of silently skipping, preventing blank-screen timeouts
|
||||||
|
* Increase initialization timeout from 8s to 15s for slower USB cameras
|
||||||
|
* Subscribe to `AVCaptureSessionRuntimeError`, `WasInterrupted`, and `InterruptionEnded` notifications and forward to Dart via `cameraError`
|
||||||
|
* Fix MethodChannel image stream fallback sending hardcoded `bytesPerRow` instead of actual value from `CVPixelBuffer`
|
||||||
|
* Add diagnostic logging at all critical points in macOS session setup
|
||||||
|
|
||||||
|
## 1.0.6
|
||||||
|
|
||||||
|
* Fix Xcode build warnings by declaring PrivacyInfo.xcprivacy as a resource bundle in iOS and macOS podspecs
|
||||||
|
|
||||||
|
## 1.0.5
|
||||||
|
|
||||||
|
* Fixes #1: conflict with camera_android and camera_avfoundation dependencies
|
||||||
|
|
||||||
|
## 1.0.4
|
||||||
|
|
||||||
|
* Fix macOS Swift Package Manager compatibility
|
||||||
|
|
||||||
|
## 1.0.3
|
||||||
|
|
||||||
|
* Fix hot restart FFI crash by replacing NativeCallable with polling
|
||||||
|
|
||||||
|
## 1.0.2
|
||||||
|
|
||||||
|
* Fix macOS use-after-free crash during engine teardown by making dispose synchronous/idempotent and guarding FFI callbacks
|
||||||
|
|
||||||
|
## 1.0.1
|
||||||
|
|
||||||
|
* Fix xcprivacy build warnings by declaring resource_bundles in iOS and macOS podspecs
|
||||||
|
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
First stable release of `camera_desktop`
|
||||||
|
|
||||||
|
### Platform implementations
|
||||||
|
|
||||||
|
* **macOS**, AVFoundation (`AVCaptureSession`, `AVAssetWriter`). Preview via `CVPixelBuffer` textures, H.264/AAC recording, native mirror support.
|
||||||
|
* **Windows**, Media Foundation (`IMFCaptureEngine`) with Direct3D 11 texture rendering. H.264/AAC recording via `IMFSinkWriter`.
|
||||||
|
* **Linux**, GStreamer + V4L2 (`v4l2src → videoconvert → appsink` pipeline). H.264/AAC recording with automatic encoder selection, native mirror via `videoflip`.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Live camera preview with hardware-accelerated texture rendering on all platforms
|
||||||
|
* Photo capture, video recording, and real-time image streaming
|
||||||
|
* FFI-based zero-copy frame delivery (MethodChannel fallback for compatibility)
|
||||||
|
* Configurable resolution presets, FPS (5-60), and video bitrate
|
||||||
|
* Mirror/flip control (macOS and Linux)
|
||||||
|
* Pause/resume preview
|
||||||
|
* Runtime capability querying via `getPlatformCapabilities()`
|
||||||
|
|
||||||
|
## 0.0.8
|
||||||
|
|
||||||
|
* Migrate Windows implementation to IMFCaptureEngine
|
||||||
|
|
||||||
|
## 0.0.7
|
||||||
|
|
||||||
|
* Update example app to show settings panel
|
||||||
|
|
||||||
|
## 0.0.5
|
||||||
|
|
||||||
|
* Fix C linkage on Linux
|
||||||
|
|
||||||
|
## 0.0.4
|
||||||
|
|
||||||
|
* FFI-based image stream for reduced memory copies (3→2 per frame)
|
||||||
|
* Fix macOS Swift/ObjC interop for FFI bridge
|
||||||
|
* Fix image format reporting (Linux/Windows RGBA vs macOS BGRA)
|
||||||
|
|
||||||
|
## 0.0.3
|
||||||
|
|
||||||
|
* Performance improvements
|
||||||
|
|
||||||
|
## 0.0.2
|
||||||
|
|
||||||
|
* Add setMirror API and built-in camera sorting for DeviceEnumerator
|
||||||
|
|
||||||
|
## 0.0.1
|
||||||
|
|
||||||
|
* Linux camera support via GStreamer + V4L2.
|
||||||
|
* macOS camera support via AVFoundation.
|
||||||
|
* Windows camera support via Media Foundation.
|
||||||
|
* Full `camera_platform_interface` compliance.
|
||||||
|
* Photo capture, video recording, image streaming, and live preview.
|
||||||
21
plugins/camera_desktop/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Hugo Cornellier
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
199
plugins/camera_desktop/README.md
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
<h1 align="center">camera_desktop</h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://flutter.dev"><img src="https://img.shields.io/badge/Platform-Flutter-02569B?logo=flutter" alt="Platform"></a>
|
||||||
|
<a href="https://dart.dev"><img src="https://img.shields.io/badge/language-Dart-blue" alt="Language: Dart"></a>
|
||||||
|
<br>
|
||||||
|
<a href="https://pub.dev/packages/camera_desktop"><img src="https://img.shields.io/pub/v/camera_desktop?label=pub.dev&labelColor=333940&logo=dart" alt="Pub Version"></a>
|
||||||
|
<a href="https://pub.dev/packages/camera_desktop/score"><img src="https://img.shields.io/pub/points/camera_desktop?color=2E8B57&label=pub%20points" alt="pub points"></a>
|
||||||
|
<a href="https://github.com/hugocornellier/camera_desktop/actions/workflows/ci.yml"><img src="https://github.com/hugocornellier/camera_desktop/actions/workflows/ci.yml/badge.svg" alt="Flutter CI"></a>
|
||||||
|
<a href="https://github.com/hugocornellier/camera_desktop/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-007A88.svg" alt="License"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
A Flutter camera plugin for desktop platforms. Implements
|
||||||
|
[`camera_platform_interface`](https://pub.dev/packages/camera_platform_interface)
|
||||||
|
so it works seamlessly with the standard
|
||||||
|
[`camera`](https://pub.dev/packages/camera) package and `CameraController`.
|
||||||
|
|
||||||
|
## Platform Support
|
||||||
|
|
||||||
|
| Platform | Backend | Status |
|
||||||
|
|----------|---------|--------|
|
||||||
|
| **Linux** | GStreamer + V4L2 | Included |
|
||||||
|
| **macOS** | AVFoundation | Included |
|
||||||
|
| **Windows** | Media Foundation | Included |
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Add `camera_desktop` alongside `camera` in your `pubspec.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
camera: ^0.11.0
|
||||||
|
camera_desktop: ^1.2.1
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it. All three desktop platforms are covered, no additional packages needed.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Use the standard `camera` package API:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:camera/camera.dart';
|
||||||
|
|
||||||
|
final cameras = await availableCameras();
|
||||||
|
final controller = CameraController(cameras.first, ResolutionPreset.high);
|
||||||
|
await controller.initialize();
|
||||||
|
|
||||||
|
// Preview
|
||||||
|
CameraPreview(controller);
|
||||||
|
|
||||||
|
// Capture
|
||||||
|
final file = await controller.takePicture();
|
||||||
|
|
||||||
|
// Record
|
||||||
|
await controller.startVideoRecording();
|
||||||
|
final video = await controller.stopVideoRecording();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Settings
|
||||||
|
|
||||||
|
`CameraController` (camera 0.11.x+) accepts optional `fps`, `videoBitrate`, and
|
||||||
|
`audioBitrate` parameters at construction time:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final controller = CameraController(
|
||||||
|
cameras.first,
|
||||||
|
ResolutionPreset.veryHigh,
|
||||||
|
enableAudio: true,
|
||||||
|
fps: 30,
|
||||||
|
videoBitrate: 5000000, // 5 Mbps
|
||||||
|
audioBitrate: 128000, // 128 kbps
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
These settings are applied during `initialize()`. To change them you must
|
||||||
|
`dispose()` the controller and create a new one, see [Limitations](#limitations).
|
||||||
|
|
||||||
|
## Platform-Specific Setup
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
Install GStreamer development libraries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ubuntu/Debian
|
||||||
|
sudo apt install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good
|
||||||
|
|
||||||
|
# Fedora
|
||||||
|
sudo dnf install gstreamer1-devel gstreamer1-plugins-base-devel gstreamer1-plugins-good
|
||||||
|
|
||||||
|
# Arch
|
||||||
|
sudo pacman -S gstreamer gst-plugins-base gst-plugins-good
|
||||||
|
```
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
Add camera and microphone usage descriptions to your `Info.plist`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>This app needs camera access.</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>This app needs microphone access for video recording.</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
For sandboxed apps, add to your entitlements:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<key>com.apple.security.device.camera</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.device.audio-input</key>
|
||||||
|
<true/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
No additional setup required.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
| Feature | Linux | macOS | Windows |
|
||||||
|
|---------|-------|-------|---------|
|
||||||
|
| Camera enumeration | Yes | Yes | Yes |
|
||||||
|
| Live preview | Yes | Yes | Yes |
|
||||||
|
| Photo capture | Yes | Yes | Yes |
|
||||||
|
| Video recording | Yes | Yes | Yes |
|
||||||
|
| Image streaming | Yes | Yes | Yes |
|
||||||
|
| Audio recording | Yes | Yes | Yes |
|
||||||
|
| Resolution presets | Yes | Yes | Yes |
|
||||||
|
| Custom FPS | Yes | Yes | Yes |
|
||||||
|
| Video bitrate control | Yes | Yes | Yes |
|
||||||
|
| Audio bitrate control | Yes | Yes | Yes |
|
||||||
|
| Mirror control | Yes | Yes | No (handled in Flutter) |
|
||||||
|
|
||||||
|
## Mirror / Flip Behavior
|
||||||
|
|
||||||
|
On **macOS** and **Linux**, the preview frames are mirrored at the native capture
|
||||||
|
level (like a webcam selfie view), so `buildPreview()` returns the texture as-is.
|
||||||
|
The mirror state can be toggled at runtime via `setMirror()`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:camera_desktop/camera_desktop.dart';
|
||||||
|
|
||||||
|
// Toggle mirror at runtime (macOS & Linux only)
|
||||||
|
final plugin = CameraDesktopPlugin();
|
||||||
|
await plugin.setMirror(cameraId, false); // disable mirror
|
||||||
|
await plugin.setMirror(cameraId, true); // re-enable mirror
|
||||||
|
```
|
||||||
|
|
||||||
|
On **Windows**, the native backend does not mirror, so the example app wraps the
|
||||||
|
preview in a horizontal `Transform` in Flutter:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
return Transform(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
transform: Matrix4.diagonal3Values(-1, 1, 1),
|
||||||
|
child: Texture(textureId: textureId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The same applies to video playback. Recorded files from macOS/Linux are already
|
||||||
|
mirrored, while Windows recordings need a Flutter-side flip if you want a
|
||||||
|
mirror-style playback.
|
||||||
|
|
||||||
|
## Platform Capabilities
|
||||||
|
|
||||||
|
Query what the current platform supports at runtime:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:camera_desktop/camera_desktop.dart';
|
||||||
|
|
||||||
|
final caps = await CameraDesktopPlugin().getPlatformCapabilities();
|
||||||
|
// caps['supportsMirrorControl'] == true (macOS & Linux)
|
||||||
|
// caps['supportsVideoFpsControl'] == true
|
||||||
|
// caps['supportsVideoBitrateControl'] == true
|
||||||
|
// caps['supportsAudioBitrateControl'] == true
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful when building UIs that conditionally expose controls based on the
|
||||||
|
running platform.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
Desktop cameras generally do not support mobile-oriented features:
|
||||||
|
|
||||||
|
- Flash/torch control
|
||||||
|
- Exposure/focus point selection
|
||||||
|
- Zoom (beyond 1.0x)
|
||||||
|
- Device orientation changes
|
||||||
|
- Pause/resume video recording
|
||||||
|
|
||||||
|
These methods either no-op or throw `CameraException` as appropriate.
|
||||||
|
|
||||||
|
`fps`, `videoBitrate`, and `audioBitrate` are applied at initialization and cannot
|
||||||
|
be changed on a running controller. To update them, `dispose()` the controller and
|
||||||
|
create a new one with the desired settings.
|
||||||
4
plugins/camera_desktop/analysis_options.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
48
plugins/camera_desktop/android/build.gradle
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
group = "com.hugocornellier.camera_desktop"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = "2.1.0"
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.9.1")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: "com.android.library"
|
||||||
|
apply plugin: "kotlin-android"
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.hugocornellier.camera_desktop"
|
||||||
|
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main.java.srcDirs += "src/main/kotlin"
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.hugocornellier.camera_desktop">
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.hugocornellier.camera_desktop
|
||||||
|
|
||||||
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||||
|
import io.flutter.plugin.common.MethodChannel.Result
|
||||||
|
|
||||||
|
/** CameraDesktopPlugin */
|
||||||
|
class CameraDesktopPlugin :
|
||||||
|
FlutterPlugin,
|
||||||
|
MethodCallHandler {
|
||||||
|
private lateinit var channel: MethodChannel
|
||||||
|
|
||||||
|
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "camera_desktop")
|
||||||
|
channel.setMethodCallHandler(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMethodCall(
|
||||||
|
call: MethodCall,
|
||||||
|
result: Result
|
||||||
|
) {
|
||||||
|
if (call.method == "getPlatformVersion") {
|
||||||
|
result.success("Android ${android.os.Build.VERSION.RELEASE}")
|
||||||
|
} else {
|
||||||
|
result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
channel.setMethodCallHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
74
plugins/camera_desktop/ci/check_unicode_inventory.py
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Assert that every non-ASCII character appearing in windows/*.cpp|*.h also
|
||||||
|
appears in ci/cp936_repro.cpp.
|
||||||
|
|
||||||
|
Purpose: the CP936 simulation step in CI compiles cp936_repro.cpp to prove that
|
||||||
|
/utf-8 resolves C4819 for the exact character set we use. If a new file adds a
|
||||||
|
new Unicode character (e.g. a µ in a comment) without updating the synthetic
|
||||||
|
repro, CI would silently keep passing while real Simplified-Chinese Windows
|
||||||
|
hosts would start failing again.
|
||||||
|
|
||||||
|
Run from the repo root. Exits non-zero with a clear message if drift is found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Windows runners default stdout to CP1252, which cannot encode characters
|
||||||
|
# like → or ↔. Force UTF-8 so the diagnostic prints below never raise.
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def non_ascii_chars(path: pathlib.Path) -> set[str]:
|
||||||
|
return {c for c in path.read_text(encoding="utf-8") if ord(c) > 0x7F}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
windows_sources = sorted(
|
||||||
|
list(pathlib.Path("windows").glob("*.cpp"))
|
||||||
|
+ list(pathlib.Path("windows").glob("*.h"))
|
||||||
|
)
|
||||||
|
if not windows_sources:
|
||||||
|
print("::error::No windows/*.cpp|*.h files found, run from repo root.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
real: set[str] = set()
|
||||||
|
per_file: dict[str, set[str]] = {}
|
||||||
|
for f in windows_sources:
|
||||||
|
chars = non_ascii_chars(f)
|
||||||
|
if chars:
|
||||||
|
per_file[str(f)] = chars
|
||||||
|
real |= chars
|
||||||
|
|
||||||
|
synthetic_path = pathlib.Path("ci/cp936_repro.cpp")
|
||||||
|
if not synthetic_path.exists():
|
||||||
|
print(f"::error::{synthetic_path} missing.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
synthetic = non_ascii_chars(synthetic_path)
|
||||||
|
|
||||||
|
missing = real - synthetic
|
||||||
|
if missing:
|
||||||
|
print("::error::ci/cp936_repro.cpp is missing characters used in windows/ sources.")
|
||||||
|
print("Missing:")
|
||||||
|
for c in sorted(missing):
|
||||||
|
sources = [f for f, cs in per_file.items() if c in cs]
|
||||||
|
print(f" U+{ord(c):04X} {c!r} (in: {', '.join(sources)})")
|
||||||
|
print()
|
||||||
|
print("Fix: add these characters to ci/cp936_repro.cpp so the CP936 CI")
|
||||||
|
print("simulation stays representative of the real sources.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"OK: all {len(real)} non-ASCII chars in windows/ are covered by {synthetic_path}.")
|
||||||
|
for c in sorted(real):
|
||||||
|
print(f" U+{ord(c):04X} {c}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
25
plugins/camera_desktop/ci/cp936_repro.cpp
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// Synthetic CP936-reproduction file for CI.
|
||||||
|
//
|
||||||
|
// This file deliberately contains every non-ASCII character that appears in
|
||||||
|
// windows/*.cpp and windows/*.h, so that compiling it with
|
||||||
|
// cl /c /WX /source-charset:.936 /execution-charset:.936
|
||||||
|
// reproduces the exact C4819 / C2220 failure that users see on Simplified
|
||||||
|
// Chinese Windows hosts (where GetACP() == 936 / GBK).
|
||||||
|
//
|
||||||
|
// DO NOT add a BOM to this file. With a BOM, MSVC auto-detects UTF-8 and
|
||||||
|
// ignores /source-charset:.936, which would defeat the test.
|
||||||
|
//
|
||||||
|
// If you add a new non-ASCII character anywhere under windows/, the CI step
|
||||||
|
// `ci/check_unicode_inventory.py` will fail until you add that character
|
||||||
|
// here. Keep the inventory below in sync.
|
||||||
|
//
|
||||||
|
// Covered characters (also listed explicitly so a byte-level grep for the
|
||||||
|
// UTF-8 sequences finds them here):
|
||||||
|
// U+2026 HORIZONTAL ELLIPSIS …
|
||||||
|
// U+2192 RIGHTWARDS ARROW →
|
||||||
|
// U+2194 LEFT RIGHT ARROW ↔
|
||||||
|
// U+2264 LESS-THAN OR EQUAL TO ≤
|
||||||
|
// U+2500 BOX DRAWINGS LIGHT HORIZONTAL ─
|
||||||
|
//
|
||||||
|
// No code needed: /c (compile only) is sufficient to trigger C4819 on the
|
||||||
|
// comment bytes above.
|
||||||
45
plugins/camera_desktop/example/.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
30
plugins/camera_desktop/example/.metadata
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "90673a4eef275d1a6692c26ac80d6d746d41a73a"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
- platform: macos
|
||||||
|
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
17
plugins/camera_desktop/example/README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# camera_desktop_example
|
||||||
|
|
||||||
|
Demonstrates how to use the camera_desktop plugin.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
|
||||||
|
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
||||||
28
plugins/camera_desktop/example/analysis_options.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import 'package:camera_platform_interface/camera_platform_interface.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
import 'package:camera_desktop/camera_desktop.dart';
|
||||||
|
import 'package:camera_desktop/src/image_stream_ffi.dart';
|
||||||
|
|
||||||
|
/// End-to-end proof for the orphaned-poller fix, using the REAL camera + real
|
||||||
|
/// FFI poller.
|
||||||
|
///
|
||||||
|
/// Scenario: an app starts an image stream and then disposes the camera WITHOUT
|
||||||
|
/// cancelling the stream subscription (exactly what `CameraController.dispose()`
|
||||||
|
/// does — it never stops image streams). Before the fix, the 8ms FFI poll timer
|
||||||
|
/// kept firing forever after the camera was gone. After the fix, `dispose()`
|
||||||
|
/// stops it.
|
||||||
|
///
|
||||||
|
/// We observe the real `ImageStreamFfi.pollCount` (number of poll ticks) before
|
||||||
|
/// and after `dispose()`: it must stop advancing.
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'disposing a streaming camera stops the FFI poll timer (no leak)',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final plugin = CameraPlatform.instance as CameraDesktopPlugin;
|
||||||
|
|
||||||
|
// Capture the real FFI poller created for this stream (the default
|
||||||
|
// factory is ImageStreamFfi.tryCreate, which succeeds in a real build).
|
||||||
|
ImageStreamFfi? realPoller;
|
||||||
|
final defaultFactory = plugin.imageStreamPollerFactory;
|
||||||
|
plugin.imageStreamPollerFactory = (handle) {
|
||||||
|
final poller = defaultFactory(handle);
|
||||||
|
realPoller = poller as ImageStreamFfi?;
|
||||||
|
return poller;
|
||||||
|
};
|
||||||
|
addTearDown(() => plugin.imageStreamPollerFactory = defaultFactory);
|
||||||
|
|
||||||
|
final cameras = await plugin.availableCameras();
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[poller-leak-test] cameras found: ${cameras.length}');
|
||||||
|
if (cameras.isEmpty) {
|
||||||
|
markTestSkipped('No camera available on this machine.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final cameraId = await plugin.createCameraWithSettings(
|
||||||
|
cameras.first,
|
||||||
|
const MediaSettings(resolutionPreset: ResolutionPreset.low),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await plugin.initializeCamera(cameraId);
|
||||||
|
} on CameraException catch (e) {
|
||||||
|
await plugin.dispose(cameraId);
|
||||||
|
markTestSkipped('Camera initialize failed: ${e.code} ${e.description}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start streaming. Critically: DO NOT cancel this subscription.
|
||||||
|
final sub = plugin.onStreamedFrameAvailable(cameraId).listen((_) {});
|
||||||
|
|
||||||
|
// Allow onListen (startImageStream round-trip) + several poll ticks.
|
||||||
|
await tester.pump();
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
realPoller,
|
||||||
|
isNotNull,
|
||||||
|
reason: 'FFI fast path should be active in a real macOS build',
|
||||||
|
);
|
||||||
|
final pollsWhileStreaming = realPoller!.pollCount;
|
||||||
|
expect(
|
||||||
|
pollsWhileStreaming,
|
||||||
|
greaterThan(0),
|
||||||
|
reason: 'the poll timer should be running while streaming',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dispose the camera WITHOUT cancelling the subscription — the leak path.
|
||||||
|
await plugin.dispose(cameraId);
|
||||||
|
|
||||||
|
final pollsAtDispose = realPoller!.pollCount;
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||||
|
final pollsAfterWait = realPoller!.pollCount;
|
||||||
|
|
||||||
|
// ignore: avoid_print
|
||||||
|
print(
|
||||||
|
'[poller-leak-test] polls: whileStreaming=$pollsWhileStreaming '
|
||||||
|
'atDispose=$pollsAtDispose afterWait(+600ms)=$pollsAfterWait',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
pollsAfterWait,
|
||||||
|
equals(pollsAtDispose),
|
||||||
|
reason: 'after dispose the poll timer MUST be stopped; before the fix '
|
||||||
|
'it keeps firing ~125x/sec forever',
|
||||||
|
);
|
||||||
|
|
||||||
|
await sub.cancel();
|
||||||
|
},
|
||||||
|
timeout: const Timeout(Duration(seconds: 90)),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import 'package:camera_platform_interface/camera_platform_interface.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
import 'package:camera_desktop/camera_desktop.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
testWidgets('CameraDesktopPlugin is registered as CameraPlatform', (
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
// On Linux, CameraDesktopPlugin should be auto-registered via registerWith().
|
||||||
|
expect(CameraPlatform.instance, isA<CameraDesktopPlugin>());
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('availableCameras returns a list', (WidgetTester tester) async {
|
||||||
|
final cameras = await CameraPlatform.instance.availableCameras();
|
||||||
|
// On a machine with no cameras this may be empty, but it shouldn't throw.
|
||||||
|
expect(cameras, isA<List<CameraDescription>>());
|
||||||
|
});
|
||||||
|
}
|
||||||
155
plugins/camera_desktop/example/lib/gallery_page.dart
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'photo_viewer_page.dart';
|
||||||
|
import 'recent_media.dart';
|
||||||
|
import 'video_player_page.dart';
|
||||||
|
|
||||||
|
class GalleryPage extends StatelessWidget {
|
||||||
|
const GalleryPage({super.key, required this.items});
|
||||||
|
|
||||||
|
final List<MediaEntry> items;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
'Gallery (${items.length} of ${RecentMediaStore.maxItems})',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
_buildBanner(context),
|
||||||
|
Expanded(
|
||||||
|
child: items.isEmpty ? _buildEmptyState() : _buildGrid(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBanner(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
margin: const EdgeInsets.all(12),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
size: 20,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
'STORES MOST RECENT ${RecentMediaStore.maxItems} ITEMS ONLY',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyState() {
|
||||||
|
return const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.photo_library_outlined, size: 64, color: Colors.grey),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'No photos or videos yet',
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGrid(BuildContext context) {
|
||||||
|
final width = MediaQuery.sizeOf(context).width;
|
||||||
|
final crossAxisCount = width > 900
|
||||||
|
? 4
|
||||||
|
: width > 600
|
||||||
|
? 3
|
||||||
|
: 2;
|
||||||
|
|
||||||
|
return GridView.builder(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
mainAxisSpacing: 4,
|
||||||
|
),
|
||||||
|
itemCount: items.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = items[index];
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => _onItemTap(context, item, index),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: item.isVideo
|
||||||
|
? _buildVideoThumbnail()
|
||||||
|
: _buildPhotoThumbnail(item),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onItemTap(BuildContext context, MediaEntry item, int index) {
|
||||||
|
if (item.isVideo) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => VideoPlayerPage(path: item.path)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Collect only photo entries and find the adjusted index.
|
||||||
|
final photoItems = items.where((e) => e.isPhoto).toList();
|
||||||
|
final photoIndex = photoItems.indexOf(item);
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => PhotoViewerPage(
|
||||||
|
items: photoItems,
|
||||||
|
initialIndex: photoIndex >= 0 ? photoIndex : 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVideoThumbnail() {
|
||||||
|
return Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Container(color: Colors.grey.shade800),
|
||||||
|
const Center(
|
||||||
|
child: Icon(Icons.play_circle_fill, size: 48, color: Colors.white70),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPhotoThumbnail(MediaEntry item) {
|
||||||
|
return Image.file(
|
||||||
|
File(item.path),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
cacheWidth: 300,
|
||||||
|
errorBuilder: (_, error, stackTrace) => Container(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
child: const Icon(Icons.broken_image, color: Colors.grey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
719
plugins/camera_desktop/example/lib/main.dart
Normal file
@@ -0,0 +1,719 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:camera/camera.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart';
|
||||||
|
|
||||||
|
import 'gallery_page.dart';
|
||||||
|
import 'photo_viewer_page.dart';
|
||||||
|
import 'recent_media.dart';
|
||||||
|
import 'video_player_page.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
MediaKit.ensureInitialized();
|
||||||
|
runApp(const CameraExampleApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class CameraExampleApp extends StatelessWidget {
|
||||||
|
const CameraExampleApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'Camera Desktop Example',
|
||||||
|
// Workaround for an upstream Flutter Linux freeze (flutter/flutter#153560,
|
||||||
|
// still open): the GTK/AT-SPI accessibility bridge blocks the UI thread on
|
||||||
|
// a synchronous D-Bus call while walking the semantics tree when an
|
||||||
|
// AT-SPI client is active (e.g. on settings changes here). Wrapping the
|
||||||
|
// Navigator in ExcludeSemantics prunes the app's accessibility tree,
|
||||||
|
// including overlay routes like dropdown popups, which avoids the hang.
|
||||||
|
// Trade-off: in-app screen-reader support is disabled. Remove this once
|
||||||
|
// the upstream bug is fixed if you need accessibility.
|
||||||
|
builder: (context, child) => ExcludeSemantics(child: child!),
|
||||||
|
home: const CameraExamplePage(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CaptureMode { photo, video }
|
||||||
|
|
||||||
|
const List<int> kFpsOptions = [15, 24, 30, 60];
|
||||||
|
|
||||||
|
const List<({int value, String label})> kVideoBitrateOptions = [
|
||||||
|
(value: 1000000, label: '1 Mbps'),
|
||||||
|
(value: 2500000, label: '2.5 Mbps'),
|
||||||
|
(value: 5000000, label: '5 Mbps'),
|
||||||
|
(value: 10000000, label: '10 Mbps'),
|
||||||
|
(value: 20000000, label: '20 Mbps'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const List<({int value, String label})> kAudioBitrateOptions = [
|
||||||
|
(value: 64000, label: '64 kbps'),
|
||||||
|
(value: 128000, label: '128 kbps'),
|
||||||
|
(value: 192000, label: '192 kbps'),
|
||||||
|
(value: 256000, label: '256 kbps'),
|
||||||
|
];
|
||||||
|
|
||||||
|
class CameraExamplePage extends StatefulWidget {
|
||||||
|
const CameraExamplePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CameraExamplePage> createState() => _CameraExamplePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CameraExamplePageState extends State<CameraExamplePage> {
|
||||||
|
CameraController? _controller;
|
||||||
|
List<CameraDescription> _cameras = [];
|
||||||
|
String? _errorMessage;
|
||||||
|
bool _isInitialized = false;
|
||||||
|
bool _isCapturing = false;
|
||||||
|
|
||||||
|
CaptureMode _mode = CaptureMode.photo;
|
||||||
|
bool _isRecording = false;
|
||||||
|
bool _isStoppingRecording = false;
|
||||||
|
Duration _recordingDuration = Duration.zero;
|
||||||
|
Timer? _recordingTimer;
|
||||||
|
int _nextPendingVideoId = 1;
|
||||||
|
final List<_PendingVideo> _pendingVideos = <_PendingVideo>[];
|
||||||
|
|
||||||
|
// Settings state, these drive the CameraController constructor.
|
||||||
|
int _selectedCameraIndex = 0;
|
||||||
|
ResolutionPreset _resolutionPreset = ResolutionPreset.veryHigh;
|
||||||
|
int _fps = 30;
|
||||||
|
int _videoBitrate = 5000000;
|
||||||
|
int _audioBitrate = 128000;
|
||||||
|
bool _enableAudio = true;
|
||||||
|
bool _showSettings = false;
|
||||||
|
bool _isReinitializing = false;
|
||||||
|
|
||||||
|
final RecentMediaStore _mediaStore = RecentMediaStore();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initCamera() async {
|
||||||
|
try {
|
||||||
|
_cameras = await availableCameras();
|
||||||
|
if (_cameras.isEmpty) {
|
||||||
|
setState(() => _errorMessage = 'No cameras found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _createAndInitController();
|
||||||
|
} on CameraException catch (e) {
|
||||||
|
setState(() => _errorMessage = 'Camera error: ${e.description}');
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _errorMessage = 'Error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createAndInitController() async {
|
||||||
|
final controller = CameraController(
|
||||||
|
_cameras[_selectedCameraIndex],
|
||||||
|
_resolutionPreset,
|
||||||
|
enableAudio: _enableAudio,
|
||||||
|
fps: _fps,
|
||||||
|
videoBitrate: _videoBitrate,
|
||||||
|
audioBitrate: _audioBitrate,
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.initialize();
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_controller = controller;
|
||||||
|
_isInitialized = true;
|
||||||
|
_isReinitializing = false;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _reinitCamera() async {
|
||||||
|
if (_isRecording || _isStoppingRecording) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isReinitializing = true;
|
||||||
|
_isInitialized = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
await _controller?.dispose();
|
||||||
|
_controller = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _createAndInitController();
|
||||||
|
} on CameraException catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isReinitializing = false;
|
||||||
|
_errorMessage = 'Camera error: ${e.description}';
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isReinitializing = false;
|
||||||
|
_errorMessage = 'Error: $e';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _takePicture() async {
|
||||||
|
if (_controller == null || !_isInitialized || _isCapturing) return;
|
||||||
|
setState(() => _isCapturing = true);
|
||||||
|
try {
|
||||||
|
final file = await _controller!.takePicture();
|
||||||
|
if (!mounted) return;
|
||||||
|
_mediaStore.add(file.path, MediaType.photo);
|
||||||
|
setState(() => _errorMessage = null);
|
||||||
|
} on CameraException catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Capture failed: ${e.description}')),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isCapturing = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startRecording() async {
|
||||||
|
if (_controller == null || !_isInitialized || _isRecording) return;
|
||||||
|
try {
|
||||||
|
await _controller!.startVideoRecording();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isRecording = true;
|
||||||
|
_recordingDuration = Duration.zero;
|
||||||
|
});
|
||||||
|
_recordingTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _recordingDuration += const Duration(seconds: 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} on CameraException catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Recording failed: ${e.description}')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _stopRecording() async {
|
||||||
|
if (!_isRecording || _isStoppingRecording || _controller == null) return;
|
||||||
|
_recordingTimer?.cancel();
|
||||||
|
final pendingId = _nextPendingVideoId++;
|
||||||
|
setState(() {
|
||||||
|
_isRecording = false;
|
||||||
|
_isStoppingRecording = true;
|
||||||
|
_pendingVideos.insert(0, _PendingVideo(id: pendingId));
|
||||||
|
});
|
||||||
|
|
||||||
|
unawaited(_finalizeVideoStop(pendingId));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _finalizeVideoStop(int pendingId) async {
|
||||||
|
try {
|
||||||
|
final file = await _controller!.stopVideoRecording();
|
||||||
|
if (!mounted) return;
|
||||||
|
_mediaStore.add(file.path, MediaType.video);
|
||||||
|
setState(() {
|
||||||
|
_isStoppingRecording = false;
|
||||||
|
_pendingVideos.removeWhere((p) => p.id == pendingId);
|
||||||
|
});
|
||||||
|
} on CameraException catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isStoppingRecording = false;
|
||||||
|
_pendingVideos.removeWhere((p) => p.id == pendingId);
|
||||||
|
});
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Stop recording failed: ${e.description}')),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isStoppingRecording = false;
|
||||||
|
_pendingVideos.removeWhere((p) => p.id == pendingId);
|
||||||
|
});
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('Stop recording failed: $e')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatTimer(Duration d) {
|
||||||
|
final minutes = d.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||||
|
final seconds = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||||
|
return '$minutes:$seconds';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_recordingTimer?.cancel();
|
||||||
|
_controller?.dispose();
|
||||||
|
_mediaStore.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Camera Desktop Example'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_showSettings ? Icons.settings : Icons.settings_outlined,
|
||||||
|
),
|
||||||
|
tooltip: 'Settings',
|
||||||
|
onPressed: _isRecording
|
||||||
|
? null
|
||||||
|
: () => setState(() => _showSettings = !_showSettings),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Badge(
|
||||||
|
label: Text('${_mediaStore.count + _pendingVideos.length}'),
|
||||||
|
isLabelVisible:
|
||||||
|
_mediaStore.isNotEmpty || _pendingVideos.isNotEmpty,
|
||||||
|
child: const Icon(Icons.photo_library),
|
||||||
|
),
|
||||||
|
tooltip:
|
||||||
|
'Gallery (${_mediaStore.count + _pendingVideos.length}/${RecentMediaStore.maxItems})',
|
||||||
|
onPressed: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => GalleryPage(items: _mediaStore.items),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildPreview()),
|
||||||
|
if (_showSettings && _isInitialized) _buildSettingsPanel(),
|
||||||
|
if (_isInitialized) _buildControlBar(),
|
||||||
|
if (_mediaStore.isNotEmpty || _pendingVideos.isNotEmpty)
|
||||||
|
_buildThumbnailStrip(),
|
||||||
|
if (_errorMessage != null && !_isInitialized)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
_errorMessage!,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPreview() {
|
||||||
|
if (_errorMessage != null && !_isInitialized && !_isReinitializing) {
|
||||||
|
return Center(child: Text(_errorMessage!));
|
||||||
|
}
|
||||||
|
if (!_isInitialized || _controller == null) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Center(child: CameraPreview(_controller!)),
|
||||||
|
if (_isRecording)
|
||||||
|
Positioned(
|
||||||
|
top: 16,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 6,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.fiber_manual_record,
|
||||||
|
color: Colors.red,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'REC ${_formatTimer(_recordingDuration)}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _resolutionLabel(ResolutionPreset preset) {
|
||||||
|
return switch (preset) {
|
||||||
|
ResolutionPreset.low => 'Low (240p)',
|
||||||
|
ResolutionPreset.medium => 'Medium (480p)',
|
||||||
|
ResolutionPreset.high => 'High (720p)',
|
||||||
|
ResolutionPreset.veryHigh => 'Very High (1080p)',
|
||||||
|
ResolutionPreset.ultraHigh => 'Ultra High (4K)',
|
||||||
|
ResolutionPreset.max => 'Max',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSettingsPanel() {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHighest,
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(color: theme.colorScheme.outlineVariant),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 24,
|
||||||
|
runSpacing: 12,
|
||||||
|
children: [
|
||||||
|
if (_cameras.length > 1)
|
||||||
|
_buildDropdownSetting<int>(
|
||||||
|
label: 'Camera',
|
||||||
|
value: _selectedCameraIndex,
|
||||||
|
items: [
|
||||||
|
for (var i = 0; i < _cameras.length; i++)
|
||||||
|
DropdownMenuItem(value: i, child: Text(_cameras[i].name)),
|
||||||
|
],
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v == null || v == _selectedCameraIndex) return;
|
||||||
|
setState(() => _selectedCameraIndex = v);
|
||||||
|
_reinitCamera();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildDropdownSetting<ResolutionPreset>(
|
||||||
|
label: 'Resolution',
|
||||||
|
value: _resolutionPreset,
|
||||||
|
items: ResolutionPreset.values
|
||||||
|
.map(
|
||||||
|
(p) => DropdownMenuItem(
|
||||||
|
value: p,
|
||||||
|
child: Text(_resolutionLabel(p)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v == null || v == _resolutionPreset) return;
|
||||||
|
setState(() => _resolutionPreset = v);
|
||||||
|
_reinitCamera();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildDropdownSetting<int>(
|
||||||
|
label: 'FPS',
|
||||||
|
value: _fps,
|
||||||
|
items: kFpsOptions
|
||||||
|
.map((f) => DropdownMenuItem(value: f, child: Text('$f fps')))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v == null || v == _fps) return;
|
||||||
|
setState(() => _fps = v);
|
||||||
|
_reinitCamera();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildDropdownSetting<int>(
|
||||||
|
label: 'Video Bitrate',
|
||||||
|
value: _videoBitrate,
|
||||||
|
items: kVideoBitrateOptions
|
||||||
|
.map(
|
||||||
|
(o) => DropdownMenuItem(value: o.value, child: Text(o.label)),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v == null || v == _videoBitrate) return;
|
||||||
|
setState(() => _videoBitrate = v);
|
||||||
|
_reinitCamera();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildDropdownSetting<int>(
|
||||||
|
label: 'Audio Bitrate',
|
||||||
|
value: _audioBitrate,
|
||||||
|
items: kAudioBitrateOptions
|
||||||
|
.map(
|
||||||
|
(o) => DropdownMenuItem(value: o.value, child: Text(o.label)),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v == null || v == _audioBitrate) return;
|
||||||
|
setState(() => _audioBitrate = v);
|
||||||
|
_reinitCamera();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildSwitchSetting(
|
||||||
|
label: 'Audio',
|
||||||
|
value: _enableAudio,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() => _enableAudio = v);
|
||||||
|
_reinitCamera();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDropdownSetting<T>({
|
||||||
|
required String label,
|
||||||
|
required T value,
|
||||||
|
required List<DropdownMenuItem<T>> items,
|
||||||
|
required ValueChanged<T?> onChanged,
|
||||||
|
}) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'$label: ',
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
DropdownButton<T>(
|
||||||
|
value: value,
|
||||||
|
items: items,
|
||||||
|
onChanged: _isReinitializing ? null : onChanged,
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSwitchSetting({
|
||||||
|
required String label,
|
||||||
|
required bool value,
|
||||||
|
required ValueChanged<bool> onChanged,
|
||||||
|
}) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'$label: ',
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
Switch(value: value, onChanged: _isReinitializing ? null : onChanged),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildControlBar() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SegmentedButton<CaptureMode>(
|
||||||
|
segments: const [
|
||||||
|
ButtonSegment(
|
||||||
|
value: CaptureMode.photo,
|
||||||
|
icon: Icon(Icons.camera_alt),
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: CaptureMode.video,
|
||||||
|
icon: Icon(Icons.videocam),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: {_mode},
|
||||||
|
onSelectionChanged: _isRecording
|
||||||
|
? null
|
||||||
|
: (selection) => setState(() => _mode = selection.first),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 24),
|
||||||
|
_buildCaptureButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCaptureButton() {
|
||||||
|
if (_mode == CaptureMode.photo) {
|
||||||
|
return FloatingActionButton(
|
||||||
|
onPressed: _isCapturing ? null : _takePicture,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Colors.black87,
|
||||||
|
child: _isCapturing
|
||||||
|
? const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.camera_alt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video mode
|
||||||
|
if (_isRecording) {
|
||||||
|
return FloatingActionButton(
|
||||||
|
onPressed: _stopRecording,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
child: const Icon(Icons.stop),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FloatingActionButton(
|
||||||
|
onPressed: _isStoppingRecording ? null : _startRecording,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
child: _isStoppingRecording
|
||||||
|
? const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.fiber_manual_record),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildThumbnailStrip() {
|
||||||
|
final items = _mediaStore.items;
|
||||||
|
final pendingCount = _pendingVideos.length;
|
||||||
|
return SizedBox(
|
||||||
|
height: 72,
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
itemCount: pendingCount + items.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final isPending = index < pendingCount;
|
||||||
|
final item = isPending ? null : items[index - pendingCount];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (isPending) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Video is still processing. Please wait.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_onThumbnailTap(item!, index - pendingCount);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
child: isPending
|
||||||
|
? _buildPendingVideoThumbnail()
|
||||||
|
: item!.isVideo
|
||||||
|
? _buildVideoThumbnail()
|
||||||
|
: _buildPhotoThumbnail(item),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onThumbnailTap(MediaEntry item, int index) {
|
||||||
|
if (item.isVideo) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => VideoPlayerPage(path: item.path)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final photoItems = _mediaStore.items.where((e) => e.isPhoto).toList();
|
||||||
|
final photoIndex = photoItems.indexOf(item);
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => PhotoViewerPage(
|
||||||
|
items: photoItems,
|
||||||
|
initialIndex: photoIndex >= 0 ? photoIndex : 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVideoThumbnail() {
|
||||||
|
return Container(
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(Icons.play_circle_fill, size: 28, color: Colors.white70),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPendingVideoThumbnail() {
|
||||||
|
return Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Container(color: Colors.grey.shade700),
|
||||||
|
const Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black54,
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: const Text(
|
||||||
|
'Processing',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 9),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPhotoThumbnail(MediaEntry item) {
|
||||||
|
return Image.file(
|
||||||
|
File(item.path),
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
cacheWidth: 120,
|
||||||
|
errorBuilder: (_, error, stackTrace) => Container(
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
child: const Icon(Icons.broken_image, size: 20),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PendingVideo {
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
_PendingVideo({required this.id});
|
||||||
|
}
|
||||||
109
plugins/camera_desktop/example/lib/photo_viewer_page.dart
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'recent_media.dart';
|
||||||
|
|
||||||
|
class PhotoViewerPage extends StatefulWidget {
|
||||||
|
const PhotoViewerPage({
|
||||||
|
super.key,
|
||||||
|
required this.items,
|
||||||
|
required this.initialIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<MediaEntry> items;
|
||||||
|
final int initialIndex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PhotoViewerPage> createState() => _PhotoViewerPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PhotoViewerPageState extends State<PhotoViewerPage> {
|
||||||
|
late final PageController _pageController;
|
||||||
|
late int _currentIndex;
|
||||||
|
late final FocusNode _focusNode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentIndex = widget.initialIndex.clamp(0, widget.items.length - 1);
|
||||||
|
_pageController = PageController(initialPage: _currentIndex);
|
||||||
|
_focusNode = FocusNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pageController.dispose();
|
||||||
|
_focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onKey(KeyEvent event) {
|
||||||
|
if (event is! KeyDownEvent) return;
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||||
|
if (_currentIndex > 0) {
|
||||||
|
_pageController.animateToPage(
|
||||||
|
_currentIndex - 1,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||||
|
if (_currentIndex < widget.items.length - 1) {
|
||||||
|
_pageController.animateToPage(
|
||||||
|
_currentIndex + 1,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Photo ${_currentIndex + 1} of ${widget.items.length}'),
|
||||||
|
backgroundColor: Colors.black54,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
body: KeyboardListener(
|
||||||
|
focusNode: _focusNode,
|
||||||
|
autofocus: true,
|
||||||
|
onKeyEvent: _onKey,
|
||||||
|
child: PageView.builder(
|
||||||
|
controller: _pageController,
|
||||||
|
itemCount: widget.items.length,
|
||||||
|
onPageChanged: (i) => setState(() => _currentIndex = i),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return InteractiveViewer(
|
||||||
|
minScale: 1.0,
|
||||||
|
maxScale: 5.0,
|
||||||
|
child: Center(
|
||||||
|
child: Image.file(
|
||||||
|
File(widget.items[index].path),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
errorBuilder: (_, error, stackTrace) => const Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.broken_image, size: 64, color: Colors.grey),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Photo unavailable',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
plugins/camera_desktop/example/lib/recent_media.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
enum MediaType { photo, video }
|
||||||
|
|
||||||
|
class MediaEntry {
|
||||||
|
final String path;
|
||||||
|
final DateTime capturedAt;
|
||||||
|
final MediaType type;
|
||||||
|
|
||||||
|
MediaEntry({
|
||||||
|
required this.path,
|
||||||
|
required this.capturedAt,
|
||||||
|
required this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get isVideo => type == MediaType.video;
|
||||||
|
bool get isPhoto => type == MediaType.photo;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecentMediaStore extends ChangeNotifier {
|
||||||
|
static const int maxItems = 10;
|
||||||
|
final List<MediaEntry> _items = [];
|
||||||
|
|
||||||
|
List<MediaEntry> get items => List.unmodifiable(_items);
|
||||||
|
int get count => _items.length;
|
||||||
|
bool get isEmpty => _items.isEmpty;
|
||||||
|
bool get isNotEmpty => _items.isNotEmpty;
|
||||||
|
|
||||||
|
void add(String path, MediaType type) {
|
||||||
|
_items.insert(
|
||||||
|
0,
|
||||||
|
MediaEntry(path: path, capturedAt: DateTime.now(), type: type),
|
||||||
|
);
|
||||||
|
if (_items.length > maxItems) {
|
||||||
|
_items.removeLast();
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
161
plugins/camera_desktop/example/lib/video_player_page.dart
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart';
|
||||||
|
import 'package:media_kit_video/media_kit_video.dart';
|
||||||
|
|
||||||
|
class VideoPlayerPage extends StatefulWidget {
|
||||||
|
const VideoPlayerPage({super.key, required this.path});
|
||||||
|
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VideoPlayerPage> createState() => _VideoPlayerPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoPlayerPageState extends State<VideoPlayerPage> {
|
||||||
|
late final Player _player;
|
||||||
|
late final VideoController _videoController;
|
||||||
|
late final FocusNode _focusNode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_player = Player();
|
||||||
|
_videoController = VideoController(_player);
|
||||||
|
_focusNode = FocusNode();
|
||||||
|
_player.open(Media(widget.path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_player.dispose();
|
||||||
|
_focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onKey(KeyEvent event) {
|
||||||
|
if (event is! KeyDownEvent) return;
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
} else if (event.logicalKey == LogicalKeyboardKey.space) {
|
||||||
|
_player.playOrPause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDuration(Duration d) {
|
||||||
|
final minutes = d.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||||
|
final seconds = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||||
|
if (d.inHours > 0) {
|
||||||
|
final hours = d.inHours.toString().padLeft(2, '0');
|
||||||
|
return '$hours:$minutes:$seconds';
|
||||||
|
}
|
||||||
|
return '$minutes:$seconds';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Video'),
|
||||||
|
backgroundColor: Colors.black54,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
body: KeyboardListener(
|
||||||
|
focusNode: _focusNode,
|
||||||
|
autofocus: true,
|
||||||
|
onKeyEvent: _onKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Platform.isWindows
|
||||||
|
? Transform(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
transform: Matrix4.diagonal3Values(-1, 1, 1),
|
||||||
|
child: Video(
|
||||||
|
controller: _videoController,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Video(controller: _videoController, fit: BoxFit.contain),
|
||||||
|
),
|
||||||
|
_buildControls(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildControls() {
|
||||||
|
return Container(
|
||||||
|
color: Colors.black87,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
StreamBuilder<bool>(
|
||||||
|
stream: _player.stream.playing,
|
||||||
|
builder: (context, snap) {
|
||||||
|
final playing = snap.data ?? false;
|
||||||
|
return IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
playing ? Icons.pause : Icons.play_arrow,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
onPressed: _player.playOrPause,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
StreamBuilder<Duration>(
|
||||||
|
stream: _player.stream.position,
|
||||||
|
builder: (context, snap) {
|
||||||
|
return Text(
|
||||||
|
_formatDuration(snap.data ?? Duration.zero),
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: StreamBuilder<Duration>(
|
||||||
|
stream: _player.stream.duration,
|
||||||
|
builder: (context, durSnap) {
|
||||||
|
final duration = durSnap.data ?? Duration.zero;
|
||||||
|
return StreamBuilder<Duration>(
|
||||||
|
stream: _player.stream.position,
|
||||||
|
builder: (context, posSnap) {
|
||||||
|
final position = posSnap.data ?? Duration.zero;
|
||||||
|
return Slider(
|
||||||
|
value: duration.inMilliseconds > 0
|
||||||
|
? position.inMilliseconds
|
||||||
|
.clamp(0, duration.inMilliseconds)
|
||||||
|
.toDouble()
|
||||||
|
: 0,
|
||||||
|
max: duration.inMilliseconds > 0
|
||||||
|
? duration.inMilliseconds.toDouble()
|
||||||
|
: 1,
|
||||||
|
onChanged: (v) {
|
||||||
|
_player.seek(Duration(milliseconds: v.toInt()));
|
||||||
|
},
|
||||||
|
activeColor: Colors.white,
|
||||||
|
inactiveColor: Colors.white24,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
StreamBuilder<Duration>(
|
||||||
|
stream: _player.stream.duration,
|
||||||
|
builder: (context, snap) {
|
||||||
|
return Text(
|
||||||
|
_formatDuration(snap.data ?? Duration.zero),
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
plugins/camera_desktop/example/linux/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
flutter/ephemeral
|
||||||
130
plugins/camera_desktop/example/linux/CMakeLists.txt
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# Project-level configuration.
|
||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
|
# The name of the executable created for the application. Change this to change
|
||||||
|
# the on-disk name of your application.
|
||||||
|
set(BINARY_NAME "camera_desktop_example")
|
||||||
|
# The unique GTK application identifier for this application. See:
|
||||||
|
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||||
|
set(APPLICATION_ID "com.hugocornellier.camera_desktop")
|
||||||
|
|
||||||
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
|
# versions of CMake.
|
||||||
|
cmake_policy(SET CMP0063 NEW)
|
||||||
|
|
||||||
|
# Load bundled libraries from the lib/ directory relative to the binary.
|
||||||
|
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)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Define build configuration 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()
|
||||||
|
|
||||||
|
# Compilation settings that should be applied to most targets.
|
||||||
|
#
|
||||||
|
# Be cautious about adding new options here, as plugins use this function by
|
||||||
|
# default. In most cases, you should add new options to specific targets instead
|
||||||
|
# of modifying this function.
|
||||||
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
|
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||||
|
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||||
|
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Flutter library and tool build rules.
|
||||||
|
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||||
|
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||||
|
|
||||||
|
# System-level dependencies.
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||||
|
|
||||||
|
# Application build; see runner/CMakeLists.txt.
|
||||||
|
add_subdirectory("runner")
|
||||||
|
|
||||||
|
# Run the Flutter tool portions of the build. This must not be removed.
|
||||||
|
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||||
|
|
||||||
|
# Only the install-generated bundle's copy of the executable will launch
|
||||||
|
# correctly, since the resources must in the right relative locations. To avoid
|
||||||
|
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||||
|
# the default top-level location.
|
||||||
|
set_target_properties(${BINARY_NAME}
|
||||||
|
PROPERTIES
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enable the test target.
|
||||||
|
set(include_camera_desktop_tests TRUE)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||||
|
install(FILES "${bundled_library}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endforeach(bundled_library)
|
||||||
|
|
||||||
|
# Copy the native assets provided by the build.dart from all packages.
|
||||||
|
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
|
||||||
|
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
# 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()
|
||||||
88
plugins/camera_desktop/example/linux/flutter/CMakeLists.txt
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# This file controls Flutter-level build steps. It should not be edited.
|
||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
|
||||||
|
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||||
|
|
||||||
|
# Configuration provided via flutter tool.
|
||||||
|
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||||
|
|
||||||
|
# TODO: Move the rest of this into files in ephemeral. See
|
||||||
|
# https://github.com/flutter/flutter/issues/57146.
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||||
|
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||||
|
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||||
|
|
||||||
|
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||||
|
|
||||||
|
# Published to parent scope for install step.
|
||||||
|
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||||
|
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||||
|
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||||
|
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||||
|
"fl_basic_message_channel.h"
|
||||||
|
"fl_binary_codec.h"
|
||||||
|
"fl_binary_messenger.h"
|
||||||
|
"fl_dart_project.h"
|
||||||
|
"fl_engine.h"
|
||||||
|
"fl_json_message_codec.h"
|
||||||
|
"fl_json_method_codec.h"
|
||||||
|
"fl_message_codec.h"
|
||||||
|
"fl_method_call.h"
|
||||||
|
"fl_method_channel.h"
|
||||||
|
"fl_method_codec.h"
|
||||||
|
"fl_method_response.h"
|
||||||
|
"fl_plugin_registrar.h"
|
||||||
|
"fl_plugin_registry.h"
|
||||||
|
"fl_standard_message_codec.h"
|
||||||
|
"fl_standard_method_codec.h"
|
||||||
|
"fl_string_codec.h"
|
||||||
|
"fl_value.h"
|
||||||
|
"fl_view.h"
|
||||||
|
"flutter_linux.h"
|
||||||
|
)
|
||||||
|
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||||
|
add_library(flutter INTERFACE)
|
||||||
|
target_include_directories(flutter INTERFACE
|
||||||
|
"${EPHEMERAL_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||||
|
target_link_libraries(flutter INTERFACE
|
||||||
|
PkgConfig::GTK
|
||||||
|
PkgConfig::GLIB
|
||||||
|
PkgConfig::GIO
|
||||||
|
)
|
||||||
|
add_dependencies(flutter flutter_assemble)
|
||||||
|
|
||||||
|
# === Flutter tool backend ===
|
||||||
|
# _phony_ is a non-existent file to force this command to run every time,
|
||||||
|
# since currently there's no way to get a full input/output list from the
|
||||||
|
# flutter tool.
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
|
${FLUTTER_TOOL_ENVIRONMENT}
|
||||||
|
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||||
|
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(flutter_assemble DEPENDS
|
||||||
|
"${FLUTTER_LIBRARY}"
|
||||||
|
${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <camera_desktop/camera_desktop_plugin.h>
|
||||||
|
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||||
|
#include <media_kit_video/media_kit_video_plugin.h>
|
||||||
|
|
||||||
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) camera_desktop_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "CameraDesktopPlugin");
|
||||||
|
camera_desktop_plugin_register_with_registrar(camera_desktop_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
|
||||||
|
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) media_kit_video_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
|
||||||
|
media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
#define GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
|
||||||
|
// Registers Flutter plugins.
|
||||||
|
void fl_register_plugins(FlPluginRegistry* registry);
|
||||||
|
|
||||||
|
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#
|
||||||
|
# Generated file, do not edit.
|
||||||
|
#
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
camera_desktop
|
||||||
|
media_kit_libs_linux
|
||||||
|
media_kit_video
|
||||||
|
)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|
||||||
|
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||||
|
endforeach(plugin)
|
||||||
|
|
||||||
|
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||||
|
endforeach(ffi_plugin)
|
||||||
26
plugins/camera_desktop/example/linux/runner/CMakeLists.txt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
|
# Define the application target. To change its name, change BINARY_NAME in the
|
||||||
|
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||||
|
# work.
|
||||||
|
#
|
||||||
|
# Any new source files that you add to the application should be added here.
|
||||||
|
add_executable(${BINARY_NAME}
|
||||||
|
"main.cc"
|
||||||
|
"my_application.cc"
|
||||||
|
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply the standard set of build settings. This can be removed for applications
|
||||||
|
# that need different build settings.
|
||||||
|
apply_standard_settings(${BINARY_NAME})
|
||||||
|
|
||||||
|
# Add preprocessor definitions for the application ID.
|
||||||
|
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||||
|
|
||||||
|
# Add dependency libraries. Add any application-specific dependencies here.
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||||
|
|
||||||
|
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
6
plugins/camera_desktop/example/linux/runner/main.cc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#include "my_application.h"
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
g_autoptr(MyApplication) app = my_application_new();
|
||||||
|
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||||
|
}
|
||||||
148
plugins/camera_desktop/example/linux/runner/my_application.cc
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
#include "my_application.h"
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
#ifdef GDK_WINDOWING_X11
|
||||||
|
#include <gdk/gdkx.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
struct _MyApplication {
|
||||||
|
GtkApplication parent_instance;
|
||||||
|
char** dart_entrypoint_arguments;
|
||||||
|
};
|
||||||
|
|
||||||
|
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||||
|
|
||||||
|
// Called when first Flutter frame received.
|
||||||
|
static void first_frame_cb(MyApplication* self, FlView* view) {
|
||||||
|
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GApplication::activate.
|
||||||
|
static void my_application_activate(GApplication* application) {
|
||||||
|
MyApplication* self = MY_APPLICATION(application);
|
||||||
|
GtkWindow* window =
|
||||||
|
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||||
|
|
||||||
|
// Use a header bar when running in GNOME as this is the common style used
|
||||||
|
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||||
|
// desktop).
|
||||||
|
// If running on X and not using GNOME then just use a traditional title bar
|
||||||
|
// in case the window manager does more exotic layout, e.g. tiling.
|
||||||
|
// If running on Wayland assume the header bar will work (may need changing
|
||||||
|
// if future cases occur).
|
||||||
|
gboolean use_header_bar = TRUE;
|
||||||
|
#ifdef GDK_WINDOWING_X11
|
||||||
|
GdkScreen* screen = gtk_window_get_screen(window);
|
||||||
|
if (GDK_IS_X11_SCREEN(screen)) {
|
||||||
|
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||||
|
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||||
|
use_header_bar = FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (use_header_bar) {
|
||||||
|
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||||
|
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||||
|
gtk_header_bar_set_title(header_bar, "camera_desktop_example");
|
||||||
|
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||||
|
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||||
|
} else {
|
||||||
|
gtk_window_set_title(window, "camera_desktop_example");
|
||||||
|
}
|
||||||
|
|
||||||
|
gtk_window_set_default_size(window, 1280, 720);
|
||||||
|
|
||||||
|
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||||
|
fl_dart_project_set_dart_entrypoint_arguments(
|
||||||
|
project, self->dart_entrypoint_arguments);
|
||||||
|
|
||||||
|
FlView* view = fl_view_new(project);
|
||||||
|
GdkRGBA background_color;
|
||||||
|
// Background defaults to black, override it here if necessary, e.g. #00000000
|
||||||
|
// for transparent.
|
||||||
|
gdk_rgba_parse(&background_color, "#000000");
|
||||||
|
fl_view_set_background_color(view, &background_color);
|
||||||
|
gtk_widget_show(GTK_WIDGET(view));
|
||||||
|
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||||
|
|
||||||
|
// Show the window when Flutter renders.
|
||||||
|
// Requires the view to be realized so we can start rendering.
|
||||||
|
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
|
||||||
|
self);
|
||||||
|
gtk_widget_realize(GTK_WIDGET(view));
|
||||||
|
|
||||||
|
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||||
|
|
||||||
|
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GApplication::local_command_line.
|
||||||
|
static gboolean my_application_local_command_line(GApplication* application,
|
||||||
|
gchar*** arguments,
|
||||||
|
int* exit_status) {
|
||||||
|
MyApplication* self = MY_APPLICATION(application);
|
||||||
|
// Strip out the first argument as it is the binary name.
|
||||||
|
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||||
|
|
||||||
|
g_autoptr(GError) error = nullptr;
|
||||||
|
if (!g_application_register(application, nullptr, &error)) {
|
||||||
|
g_warning("Failed to register: %s", error->message);
|
||||||
|
*exit_status = 1;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_application_activate(application);
|
||||||
|
*exit_status = 0;
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GApplication::startup.
|
||||||
|
static void my_application_startup(GApplication* application) {
|
||||||
|
// MyApplication* self = MY_APPLICATION(object);
|
||||||
|
|
||||||
|
// Perform any actions required at application startup.
|
||||||
|
|
||||||
|
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GApplication::shutdown.
|
||||||
|
static void my_application_shutdown(GApplication* application) {
|
||||||
|
// MyApplication* self = MY_APPLICATION(object);
|
||||||
|
|
||||||
|
// Perform any actions required at application shutdown.
|
||||||
|
|
||||||
|
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GObject::dispose.
|
||||||
|
static void my_application_dispose(GObject* object) {
|
||||||
|
MyApplication* self = MY_APPLICATION(object);
|
||||||
|
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||||
|
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void my_application_class_init(MyApplicationClass* klass) {
|
||||||
|
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||||
|
G_APPLICATION_CLASS(klass)->local_command_line =
|
||||||
|
my_application_local_command_line;
|
||||||
|
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
|
||||||
|
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
|
||||||
|
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void my_application_init(MyApplication* self) {}
|
||||||
|
|
||||||
|
MyApplication* my_application_new() {
|
||||||
|
// Set the program name to the application ID, which helps various systems
|
||||||
|
// like GTK and desktop environments map this running application to its
|
||||||
|
// corresponding .desktop file. This ensures better integration by allowing
|
||||||
|
// the application to be recognized beyond its binary name.
|
||||||
|
g_set_prgname(APPLICATION_ID);
|
||||||
|
|
||||||
|
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||||
|
"application-id", APPLICATION_ID, "flags",
|
||||||
|
G_APPLICATION_NON_UNIQUE, nullptr));
|
||||||
|
}
|
||||||
21
plugins/camera_desktop/example/linux/runner/my_application.h
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||||
|
#define FLUTTER_MY_APPLICATION_H_
|
||||||
|
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
G_DECLARE_FINAL_TYPE(MyApplication,
|
||||||
|
my_application,
|
||||||
|
MY,
|
||||||
|
APPLICATION,
|
||||||
|
GtkApplication)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* my_application_new:
|
||||||
|
*
|
||||||
|
* Creates a new Flutter-based application.
|
||||||
|
*
|
||||||
|
* Returns: a new #MyApplication.
|
||||||
|
*/
|
||||||
|
MyApplication* my_application_new();
|
||||||
|
|
||||||
|
#endif // FLUTTER_MY_APPLICATION_H_
|
||||||
7
plugins/camera_desktop/example/macos/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Flutter-related
|
||||||
|
**/Flutter/ephemeral/
|
||||||
|
**/Pods/
|
||||||
|
|
||||||
|
# Xcode-related
|
||||||
|
**/dgph
|
||||||
|
**/xcuserdata/
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
|
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
|
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
import FlutterMacOS
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
import camera_desktop
|
||||||
|
import media_kit_libs_macos_video
|
||||||
|
import media_kit_video
|
||||||
|
import package_info_plus
|
||||||
|
import wakelock_plus
|
||||||
|
|
||||||
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
CameraDesktopPlugin.register(with: registry.registrar(forPlugin: "CameraDesktopPlugin"))
|
||||||
|
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||||
|
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
||||||
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
|
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||||
|
}
|
||||||
62
plugins/camera_desktop/example/macos/Podfile
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
platform :osx, '10.15'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
project 'Runner', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Profile' => :release,
|
||||||
|
'Release' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutter_root
|
||||||
|
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
|
||||||
|
unless File.exist?(generated_xcode_build_settings_path)
|
||||||
|
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||||
|
return matches[1].strip if matches
|
||||||
|
end
|
||||||
|
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||||
|
|
||||||
|
flutter_macos_podfile_setup
|
||||||
|
|
||||||
|
target 'Runner' do
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
target 'RunnerTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_macos_build_settings(target)
|
||||||
|
end
|
||||||
|
|
||||||
|
# media_kit_video ships an xcconfig where GCC_PREPROCESSOR_DEFINITIONS contains
|
||||||
|
# "$(inherited)" twice, which expands the build-config macros (POD_CONFIGURATION_*,
|
||||||
|
# DEBUG) twice and triggers "macro redefined" warnings. Strip the duplicate.
|
||||||
|
Dir.glob(File.join(installer.sandbox.root, '**', '*.xcconfig')).each do |xcconfig|
|
||||||
|
contents = File.read(xcconfig)
|
||||||
|
fixed = contents.gsub(/^(GCC_PREPROCESSOR_DEFINITIONS\s*=\s*)(.*)$/) do
|
||||||
|
prefix, value = $1, $2
|
||||||
|
seen_inherited = false
|
||||||
|
tokens = value.split(/\s+/).reject do |tok|
|
||||||
|
if tok == '$(inherited)' || tok == '"$(inherited)"'
|
||||||
|
next true if seen_inherited
|
||||||
|
seen_inherited = true
|
||||||
|
end
|
||||||
|
false
|
||||||
|
end
|
||||||
|
"#{prefix}#{tokens.join(' ')}"
|
||||||
|
end
|
||||||
|
File.write(xcconfig, fixed) if fixed != contents
|
||||||
|
end
|
||||||
|
end
|
||||||
28
plugins/camera_desktop/example/macos/Podfile.lock
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
PODS:
|
||||||
|
- FlutterMacOS (1.0.0)
|
||||||
|
- media_kit_libs_macos_video (1.0.4):
|
||||||
|
- FlutterMacOS
|
||||||
|
- media_kit_video (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
|
DEPENDENCIES:
|
||||||
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
|
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
||||||
|
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
|
||||||
|
|
||||||
|
EXTERNAL SOURCES:
|
||||||
|
FlutterMacOS:
|
||||||
|
:path: Flutter/ephemeral
|
||||||
|
media_kit_libs_macos_video:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
|
||||||
|
media_kit_video:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos
|
||||||
|
|
||||||
|
SPEC CHECKSUMS:
|
||||||
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
|
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
|
||||||
|
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
|
||||||
|
|
||||||
|
PODFILE CHECKSUM: d73b3795a102e716fcfac4d702dd615b914e690e
|
||||||
|
|
||||||
|
COCOAPODS: 1.16.2
|
||||||
@@ -0,0 +1,829 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 54;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXAggregateTarget section */
|
||||||
|
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
|
||||||
|
isa = PBXAggregateTarget;
|
||||||
|
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
|
||||||
|
buildPhases = (
|
||||||
|
33CC111E2044C6BF0003C045 /* ShellScript */,
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = "Flutter Assemble";
|
||||||
|
productName = FLX;
|
||||||
|
};
|
||||||
|
/* End PBXAggregateTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
|
||||||
|
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
|
||||||
|
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
||||||
|
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||||
|
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||||
|
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
|
78EFE35BF83BFD45AEF6B49D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 242FF7202E4D346E25CA6876 /* Pods_RunnerTests.framework */; };
|
||||||
|
CA173EE46481C9CA75E4E823 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3FA453647A92249E3F85B718 /* Pods_Runner.framework */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
|
||||||
|
remoteInfo = Runner;
|
||||||
|
};
|
||||||
|
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
|
||||||
|
remoteInfo = FLX;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
33CC110E2044A8840003C045 /* Bundle Framework */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 10;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
name = "Bundle Framework";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
242FF7202E4D346E25CA6876 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
2EACE3DA6D35A68C3CD7B732 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
|
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||||
|
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||||
|
33CC10ED2044A3C60003C045 /* camera_desktop_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = camera_desktop_example.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||||
|
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
|
||||||
|
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
|
||||||
|
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||||
|
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||||
|
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||||
|
3FA453647A92249E3F85B718 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
784666492D4C4C64000A1A5F /* FlutterFramework */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterFramework; path = ephemeral/Packages/.packages/FlutterFramework; sourceTree = "<group>"; };
|
||||||
|
78DABEA22ED26510000E7860 /* camera_desktop */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = camera_desktop; path = ../../../macos/camera_desktop; sourceTree = "<group>"; };
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
B4CFE74B02A679967C8E566C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
DC48F9159AD5A6E9DC4AE8AF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
E384C909A2092068A1672556 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
E9DAE22C81AE8ADF1F1D6EE7 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
F0D230BBE672EE22D04C4895 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
331C80D2294CF70F00263BE5 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
78EFE35BF83BFD45AEF6B49D /* Pods_RunnerTests.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
33CC10EA2044A3C60003C045 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||||
|
CA173EE46481C9CA75E4E823 /* Pods_Runner.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
331C80D6294CF71000263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
|
||||||
|
);
|
||||||
|
path = RunnerTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
33BA886A226E78AF003329D5 /* Configs */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
|
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
|
||||||
|
);
|
||||||
|
path = Configs;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
33CC10E42044A3C60003C045 = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
33FAB671232836740065AC1E /* Runner */,
|
||||||
|
33CEB47122A05771004F2AC0 /* Flutter */,
|
||||||
|
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||||
|
33CC10EE2044A3C60003C045 /* Products */,
|
||||||
|
D9B5B589C9B9197BEF1D9DA1 /* Pods */,
|
||||||
|
65943964AD6ADD2D768A7FEF /* Frameworks */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
33CC10EE2044A3C60003C045 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
33CC10ED2044A3C60003C045 /* camera_desktop_example.app */,
|
||||||
|
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
33CC11242044D66E0003C045 /* Resources */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
33CC10F22044A3C60003C045 /* Assets.xcassets */,
|
||||||
|
33CC10F42044A3C60003C045 /* MainMenu.xib */,
|
||||||
|
33CC10F72044A3C60003C045 /* Info.plist */,
|
||||||
|
);
|
||||||
|
name = Resources;
|
||||||
|
path = ..;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
33CEB47122A05771004F2AC0 /* Flutter */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
78DABEA22ED26510000E7860 /* camera_desktop */,
|
||||||
|
784666492D4C4C64000A1A5F /* FlutterFramework */,
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
|
||||||
|
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
|
||||||
|
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
|
||||||
|
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
|
||||||
|
);
|
||||||
|
path = Flutter;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
33FAB671232836740065AC1E /* Runner */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
|
||||||
|
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
|
||||||
|
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
|
||||||
|
33E51914231749380026EE4D /* Release.entitlements */,
|
||||||
|
33CC11242044D66E0003C045 /* Resources */,
|
||||||
|
33BA886A226E78AF003329D5 /* Configs */,
|
||||||
|
);
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
65943964AD6ADD2D768A7FEF /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3FA453647A92249E3F85B718 /* Pods_Runner.framework */,
|
||||||
|
242FF7202E4D346E25CA6876 /* Pods_RunnerTests.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D9B5B589C9B9197BEF1D9DA1 /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E9DAE22C81AE8ADF1F1D6EE7 /* Pods-Runner.debug.xcconfig */,
|
||||||
|
DC48F9159AD5A6E9DC4AE8AF /* Pods-Runner.release.xcconfig */,
|
||||||
|
F0D230BBE672EE22D04C4895 /* Pods-Runner.profile.xcconfig */,
|
||||||
|
B4CFE74B02A679967C8E566C /* Pods-RunnerTests.debug.xcconfig */,
|
||||||
|
E384C909A2092068A1672556 /* Pods-RunnerTests.release.xcconfig */,
|
||||||
|
2EACE3DA6D35A68C3CD7B732 /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
75DFE264BFBC588855DF7735 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
331C80D1294CF70F00263BE5 /* Sources */,
|
||||||
|
331C80D2294CF70F00263BE5 /* Frameworks */,
|
||||||
|
331C80D3294CF70F00263BE5 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = RunnerTests;
|
||||||
|
productName = RunnerTests;
|
||||||
|
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
33CC10EC2044A3C60003C045 /* Runner */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
|
buildPhases = (
|
||||||
|
66940AF51424E4C81651F1C7 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
33CC10E92044A3C60003C045 /* Sources */,
|
||||||
|
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||||
|
33CC10EB2044A3C60003C045 /* Resources */,
|
||||||
|
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||||
|
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||||
|
267A2FD4C05E3C94C9133090 /* [CP] Embed Pods Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
33CC11202044C79F0003C045 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = Runner;
|
||||||
|
packageProductDependencies = (
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
);
|
||||||
|
productName = Runner;
|
||||||
|
productReference = 33CC10ED2044A3C60003C045 /* camera_desktop_example.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
33CC10E52044A3C60003C045 /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
LastSwiftUpdateCheck = 0920;
|
||||||
|
LastUpgradeCheck = 1510;
|
||||||
|
ORGANIZATIONNAME = "";
|
||||||
|
TargetAttributes = {
|
||||||
|
331C80D4294CF70F00263BE5 = {
|
||||||
|
CreatedOnToolsVersion = 14.0;
|
||||||
|
TestTargetID = 33CC10EC2044A3C60003C045;
|
||||||
|
};
|
||||||
|
33CC10EC2044A3C60003C045 = {
|
||||||
|
CreatedOnToolsVersion = 9.2;
|
||||||
|
LastSwiftMigration = 1100;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
|
SystemCapabilities = {
|
||||||
|
com.apple.Sandbox = {
|
||||||
|
enabled = 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
33CC111A2044C6BA0003C045 = {
|
||||||
|
CreatedOnToolsVersion = 9.2;
|
||||||
|
ProvisioningStyle = Manual;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 33CC10E42044A3C60003C045;
|
||||||
|
packageReferences = (
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
|
||||||
|
);
|
||||||
|
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
33CC10EC2044A3C60003C045 /* Runner */,
|
||||||
|
331C80D4294CF70F00263BE5 /* RunnerTests */,
|
||||||
|
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
331C80D3294CF70F00263BE5 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
33CC10EB2044A3C60003C045 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
|
||||||
|
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
267A2FD4C05E3C94C9133090 /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
|
||||||
|
};
|
||||||
|
33CC111E2044C6BF0003C045 /* ShellScript */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
Flutter/ephemeral/FlutterInputs.xcfilelist,
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
Flutter/ephemeral/tripwire,
|
||||||
|
);
|
||||||
|
outputFileListPaths = (
|
||||||
|
Flutter/ephemeral/FlutterOutputs.xcfilelist,
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
||||||
|
};
|
||||||
|
66940AF51424E4C81651F1C7 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
75DFE264BFBC588855DF7735 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
331C80D1294CF70F00263BE5 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
33CC10E92044A3C60003C045 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
|
||||||
|
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
|
||||||
|
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 33CC10EC2044A3C60003C045 /* Runner */;
|
||||||
|
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
|
||||||
|
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
33CC10F52044A3C60003C045 /* Base */,
|
||||||
|
);
|
||||||
|
name = MainMenu.xib;
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
331C80DB294CF71000263BE5 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = B4CFE74B02A679967C8E566C /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.cameraDesktopExample.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/camera_desktop_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/camera_desktop_example";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
331C80DC294CF71000263BE5 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = E384C909A2092068A1672556 /* Pods-RunnerTests.release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.cameraDesktopExample.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/camera_desktop_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/camera_desktop_example";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
331C80DD294CF71000263BE5 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 2EACE3DA6D35A68C3CD7B732 /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.cameraDesktopExample.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/camera_desktop_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/camera_desktop_example";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
338D0CE9231458BD00FA5F75 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "-";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEAD_CODE_STRIPPING = YES;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
338D0CEA231458BD00FA5F75 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
);
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
338D0CEB231458BD00FA5F75 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
33CC10F92044A3C60003C045 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "-";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEAD_CODE_STRIPPING = YES;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
33CC10FA2044A3C60003C045 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "-";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEAD_CODE_STRIPPING = YES;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
33CC10FC2044A3C60003C045 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
);
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
33CC10FD2044A3C60003C045 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
);
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
33CC111C2044C6BA0003C045 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
33CC111D2044C6BA0003C045 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
331C80DB294CF71000263BE5 /* Debug */,
|
||||||
|
331C80DC294CF71000263BE5 /* Release */,
|
||||||
|
331C80DD294CF71000263BE5 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
33CC10F92044A3C60003C045 /* Debug */,
|
||||||
|
33CC10FA2044A3C60003C045 /* Release */,
|
||||||
|
338D0CE9231458BD00FA5F75 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
33CC10FC2044A3C60003C045 /* Debug */,
|
||||||
|
33CC10FD2044A3C60003C045 /* Release */,
|
||||||
|
338D0CEA231458BD00FA5F75 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
33CC111C2044C6BA0003C045 /* Debug */,
|
||||||
|
33CC111D2044C6BA0003C045 /* Release */,
|
||||||
|
338D0CEB231458BD00FA5F75 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
|
||||||
|
isa = XCLocalSwiftPackageReference;
|
||||||
|
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
};
|
||||||
|
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1510"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<PreActions>
|
||||||
|
<ExecutionAction
|
||||||
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
|
<ActionContent
|
||||||
|
title = "Run Prepare Flutter Framework Script"
|
||||||
|
scriptText = ""$FLUTTER_ROOT"/packages/flutter_tools/bin/macos_assemble.sh prepare ">
|
||||||
|
<EnvironmentBuildable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||||
|
BuildableName = "camera_desktop_example.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</EnvironmentBuildable>
|
||||||
|
</ActionContent>
|
||||||
|
</ExecutionAction>
|
||||||
|
</PreActions>
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||||
|
BuildableName = "camera_desktop_example.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||||
|
BuildableName = "camera_desktop_example.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
|
||||||
|
BuildableName = "RunnerTests.xctest"
|
||||||
|
BlueprintName = "RunnerTests"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||||
|
BuildableName = "camera_desktop_example.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Profile"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||||
|
BuildableName = "camera_desktop_example.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
10
plugins/camera_desktop/example/macos/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import Cocoa
|
||||||
|
import FlutterMacOS
|
||||||
|
|
||||||
|
@main
|
||||||
|
class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "16x16",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_16.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "16x16",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_32.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "32x32",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_32.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "32x32",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_64.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "128x128",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_128.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "128x128",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_256.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "256x256",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_256.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "256x256",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_512.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "512x512",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_512.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "512x512",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"filename" : "app_icon_1024.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 520 B |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,343 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="macosx"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
|
||||||
|
</connections>
|
||||||
|
</customObject>
|
||||||
|
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||||
|
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||||
|
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
|
||||||
|
<connections>
|
||||||
|
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
|
||||||
|
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
|
||||||
|
</connections>
|
||||||
|
</customObject>
|
||||||
|
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||||
|
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||||
|
<items>
|
||||||
|
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
|
||||||
|
<items>
|
||||||
|
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||||
|
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||||
|
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||||
|
<menuItem title="Services" id="NMo-om-nkz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||||
|
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
|
||||||
|
<connections>
|
||||||
|
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||||
|
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
|
||||||
|
<connections>
|
||||||
|
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||||
|
<connections>
|
||||||
|
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||||
|
<connections>
|
||||||
|
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||||
|
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||||
|
<connections>
|
||||||
|
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||||
|
<connections>
|
||||||
|
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||||
|
<connections>
|
||||||
|
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||||
|
<connections>
|
||||||
|
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
||||||
|
<menuItem title="Find" id="4EN-yA-p0u">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
||||||
|
<connections>
|
||||||
|
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
||||||
|
<connections>
|
||||||
|
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
||||||
|
<connections>
|
||||||
|
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
||||||
|
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Substitutions" id="9ic-FL-obx">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
||||||
|
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smart Links" id="cwL-P1-jid">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Speech" id="xrE-MZ-jX0">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="View" id="H8h-7b-M4v">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="View" id="HyV-fh-RgO">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Window" id="aUF-d1-5bR">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
|
||||||
|
<connections>
|
||||||
|
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Zoom" id="R4o-n2-Eq4">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
|
||||||
|
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Help" id="EPT-qC-fAb">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
<point key="canvasLocation" x="142" y="-258"/>
|
||||||
|
</menu>
|
||||||
|
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
|
||||||
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||||
|
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
|
||||||
|
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
|
||||||
|
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</view>
|
||||||
|
</window>
|
||||||
|
</objects>
|
||||||
|
</document>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Application-level settings for the Runner target.
|
||||||
|
//
|
||||||
|
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
|
||||||
|
// future. If not, the values below would default to using the project name when this becomes a
|
||||||
|
// 'flutter create' template.
|
||||||
|
|
||||||
|
// The application's name. By default this is also the title of the Flutter window.
|
||||||
|
PRODUCT_NAME = camera_desktop_example
|
||||||
|
|
||||||
|
// The application's bundle identifier
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.cameraDesktopExample
|
||||||
|
|
||||||
|
// The copyright displayed in application information
|
||||||
|
PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#include "../../Flutter/Flutter-Debug.xcconfig"
|
||||||
|
#include "Warnings.xcconfig"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#include "../../Flutter/Flutter-Release.xcconfig"
|
||||||
|
#include "Warnings.xcconfig"
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES
|
||||||
|
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
|
||||||
|
CLANG_WARN_PRAGMA_PACK = YES
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES
|
||||||
|
CLANG_WARN_COMMA = YES
|
||||||
|
GCC_WARN_STRICT_SELECTOR_MATCH = YES
|
||||||
|
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
|
||||||
|
GCC_WARN_SHADOW = YES
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.server</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.device.camera</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.device.audio-input</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
36
plugins/camera_desktop/example/macos/Runner/Info.plist
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string></string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||||
|
<key>NSMainNibFile</key>
|
||||||
|
<string>MainMenu</string>
|
||||||
|
<key>NSPrincipalClass</key>
|
||||||
|
<string>NSApplication</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>This app needs camera access for the camera preview demo.</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>This app needs microphone access for video recording with audio.</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import Cocoa
|
||||||
|
import FlutterMacOS
|
||||||
|
|
||||||
|
class MainFlutterWindow: NSWindow {
|
||||||
|
override func awakeFromNib() {
|
||||||
|
let flutterViewController = FlutterViewController()
|
||||||
|
let windowFrame = self.frame
|
||||||
|
self.contentViewController = flutterViewController
|
||||||
|
self.setFrame(windowFrame, display: true)
|
||||||
|
|
||||||
|
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||||
|
|
||||||
|
super.awakeFromNib()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.device.camera</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.device.audio-input</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import Cocoa
|
||||||
|
import FlutterMacOS
|
||||||
|
import XCTest
|
||||||
|
import AVFoundation
|
||||||
|
@testable import camera_desktop
|
||||||
|
|
||||||
|
class RunnerTests: XCTestCase {
|
||||||
|
|
||||||
|
func testDeviceEnumeration() {
|
||||||
|
// Should return a list (may be empty on CI/headless machines).
|
||||||
|
let devices = DeviceEnumerator.enumerateDevices()
|
||||||
|
XCTAssertNotNil(devices)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeviceIdExtraction() {
|
||||||
|
let name = "FaceTime HD Camera (0x1234567890)"
|
||||||
|
let deviceId = DeviceEnumerator.extractDeviceId(from: name)
|
||||||
|
XCTAssertEqual(deviceId, "0x1234567890")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeviceIdExtractionNoParens() {
|
||||||
|
let name = "NoParen"
|
||||||
|
let deviceId = DeviceEnumerator.extractDeviceId(from: name)
|
||||||
|
XCTAssertNil(deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeviceIdExtractionNestedParens() {
|
||||||
|
let name = "Camera (Model X) (ABC123)"
|
||||||
|
let deviceId = DeviceEnumerator.extractDeviceId(from: name)
|
||||||
|
XCTAssertEqual(deviceId, "ABC123")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSessionPresetMapping() {
|
||||||
|
XCTAssertEqual(DeviceEnumerator.sessionPreset(for: 0), .low)
|
||||||
|
XCTAssertEqual(DeviceEnumerator.sessionPreset(for: 1), .medium)
|
||||||
|
XCTAssertEqual(DeviceEnumerator.sessionPreset(for: 2), .high)
|
||||||
|
XCTAssertEqual(DeviceEnumerator.sessionPreset(for: 3), .hd1280x720)
|
||||||
|
XCTAssertEqual(DeviceEnumerator.sessionPreset(for: 4), .hd1920x1080)
|
||||||
|
XCTAssertEqual(DeviceEnumerator.sessionPreset(for: 5), .hd1920x1080)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPhotoPathGeneration() {
|
||||||
|
let path = PhotoHandler.generatePath(cameraId: 42)
|
||||||
|
XCTAssertTrue(path.contains("camera_desktop_42_"))
|
||||||
|
XCTAssertTrue(path.hasSuffix(".jpg"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRecordPathGeneration() {
|
||||||
|
let path = RecordHandler.generatePath()
|
||||||
|
XCTAssertTrue(path.contains("camera_desktop_video_"))
|
||||||
|
XCTAssertTrue(path.hasSuffix(".mp4"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Regression test for the image-stream buffer-release fix.
|
||||||
|
///
|
||||||
|
/// Before the fix, stopping the image stream left both shared FFI buffers
|
||||||
|
/// allocated until full session disposal. This asserts (deterministically,
|
||||||
|
/// no camera or process-memory measurement required) that releaseBuffers()
|
||||||
|
/// reclaims all buffer memory and that the instance stays reusable afterward.
|
||||||
|
func testImageStreamBuffersReleasedOnStop() {
|
||||||
|
let width = 640
|
||||||
|
let height = 480
|
||||||
|
|
||||||
|
var pb: CVPixelBuffer?
|
||||||
|
let status = CVPixelBufferCreate(
|
||||||
|
kCFAllocatorDefault, width, height,
|
||||||
|
kCVPixelFormatType_32BGRA, [:] as CFDictionary, &pb)
|
||||||
|
XCTAssertEqual(status, kCVReturnSuccess)
|
||||||
|
guard let pixelBuffer = pb else {
|
||||||
|
XCTFail("Failed to create test pixel buffer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
CVPixelBufferLockBaseAddress(pixelBuffer, [])
|
||||||
|
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
|
||||||
|
if let base = CVPixelBufferGetBaseAddress(pixelBuffer) {
|
||||||
|
memset(base, 0x7F, bytesPerRow * height)
|
||||||
|
}
|
||||||
|
CVPixelBufferUnlockBaseAddress(pixelBuffer, [])
|
||||||
|
let perBuffer = ImageStreamFFI.headerSize + bytesPerRow * height
|
||||||
|
|
||||||
|
let ffi = ImageStreamFFI()
|
||||||
|
XCTAssertEqual(ffi.allocatedByteCount, 0, "starts with nothing allocated")
|
||||||
|
|
||||||
|
ffi.writeFrame(pixelBuffer: pixelBuffer, cameraId: 1)
|
||||||
|
ffi.writeFrame(pixelBuffer: pixelBuffer, cameraId: 1)
|
||||||
|
XCTAssertEqual(ffi.allocatedByteCount, 2 * perBuffer,
|
||||||
|
"both shared buffers are allocated while streaming")
|
||||||
|
XCTAssertNotNil(ffi.getBufferPointer(), "front buffer is readable while streaming")
|
||||||
|
|
||||||
|
// The fix under test: stopping the stream reclaims all buffer memory.
|
||||||
|
ffi.releaseBuffers()
|
||||||
|
XCTAssertEqual(ffi.allocatedByteCount, 0, "releaseBuffers() reclaims all buffer memory")
|
||||||
|
XCTAssertNil(ffi.getBufferPointer(), "no front buffer after release")
|
||||||
|
|
||||||
|
// The instance remains usable: a later frame re-allocates lazily.
|
||||||
|
ffi.writeFrame(pixelBuffer: pixelBuffer, cameraId: 1)
|
||||||
|
XCTAssertEqual(ffi.allocatedByteCount, perBuffer,
|
||||||
|
"one buffer is re-allocated after release")
|
||||||
|
}
|
||||||
|
}
|
||||||
576
plugins/camera_desktop/example/pubspec.lock
Normal file
@@ -0,0 +1,576 @@
|
|||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.9"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.13.0"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
camera:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: camera
|
||||||
|
sha256: "034c38cb8014d29698dcae6d20276688a1bf74e6487dfeb274d70ea05d5f7777"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.0+1"
|
||||||
|
camera_android_camerax:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: camera_android_camerax
|
||||||
|
sha256: b5064cf25a2787d122d0bf12e77c7b1033a2b983d0730e3091f770ee376efde5
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.2"
|
||||||
|
camera_avfoundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: camera_avfoundation
|
||||||
|
sha256: "90e4cc3fde331581a3b2d35d83be41dbb7393af0ab857eb27b732174289cb96d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.1"
|
||||||
|
camera_desktop:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: ".."
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "1.2.1"
|
||||||
|
camera_platform_interface:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: camera_platform_interface
|
||||||
|
sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.12.0"
|
||||||
|
camera_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: camera_web
|
||||||
|
sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5+3"
|
||||||
|
characters:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: characters
|
||||||
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.1"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5+2"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.7"
|
||||||
|
dbus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dbus
|
||||||
|
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.12"
|
||||||
|
fake_async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fake_async
|
||||||
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.3"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_driver:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_lints
|
||||||
|
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.0"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.33"
|
||||||
|
flutter_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
fuchsia_remote_debug_protocol:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.0"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.2"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.8.0"
|
||||||
|
integration_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker
|
||||||
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.0.2"
|
||||||
|
leak_tracker_flutter_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_flutter_testing
|
||||||
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.10"
|
||||||
|
leak_tracker_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_testing
|
||||||
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
lints:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.19"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.13.0"
|
||||||
|
media_kit:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: media_kit
|
||||||
|
sha256: ae9e79597500c7ad6083a3c7b7b7544ddabfceacce7ae5c9709b0ec16a5d6643
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.6"
|
||||||
|
media_kit_libs_linux:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: media_kit_libs_linux
|
||||||
|
sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
media_kit_libs_macos_video:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: media_kit_libs_macos_video
|
||||||
|
sha256: f26aa1452b665df288e360393758f84b911f70ffb3878032e1aabba23aa1032d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.4"
|
||||||
|
media_kit_libs_windows_video:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: media_kit_libs_windows_video
|
||||||
|
sha256: dff76da2778729ab650229e6b4ec6ec111eb5151431002cbd7ea304ff1f112ab
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.11"
|
||||||
|
media_kit_video:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: media_kit_video
|
||||||
|
sha256: afaa509e7b7e0bf247557a3a740cde903a52c34ace9810f94500e127bd7b043d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.18.0"
|
||||||
|
package_info_plus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info_plus
|
||||||
|
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.0.0"
|
||||||
|
package_info_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info_plus_platform_interface
|
||||||
|
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.1"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.2"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.6"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
|
process:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: process
|
||||||
|
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.5"
|
||||||
|
safe_local_storage:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: safe_local_storage
|
||||||
|
sha256: "287ea1f667c0b93cdc127dccc707158e2d81ee59fba0459c31a0c7da4d09c755"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.3"
|
||||||
|
sky_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.2"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
stream_transform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_transform
|
||||||
|
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
sync_http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sync_http
|
||||||
|
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.1"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.0"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.11"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
universal_platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: universal_platform
|
||||||
|
sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
uri_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uri_parser
|
||||||
|
sha256: "051c62e5f693de98ca9f130ee707f8916e2266945565926be3ff20659f7853ce"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.3"
|
||||||
|
vector_math:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_math
|
||||||
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "15.0.2"
|
||||||
|
wakelock_plus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: wakelock_plus
|
||||||
|
sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
wakelock_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: wakelock_plus_platform_interface
|
||||||
|
sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
webdriver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webdriver
|
||||||
|
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.15.0"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.6.1"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.11.0 <4.0.0"
|
||||||
|
flutter: ">=3.35.0"
|
||||||
29
plugins/camera_desktop/example/pubspec.yaml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: camera_desktop_example
|
||||||
|
description: Demonstrates how to use the camera_desktop plugin.
|
||||||
|
publish_to: 'none'
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.11.0
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
camera: ^0.12.0
|
||||||
|
camera_desktop:
|
||||||
|
path: ../
|
||||||
|
camera_platform_interface: ^2.8.0
|
||||||
|
media_kit: ^1.2.6
|
||||||
|
media_kit_video: ^2.0.1
|
||||||
|
media_kit_libs_windows_video: ^1.0.9
|
||||||
|
media_kit_libs_macos_video: ^1.1.4
|
||||||
|
media_kit_libs_linux: ^1.2.1
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
integration_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^6.0.0
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
9
plugins/camera_desktop/example/test/widget_test.dart
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:camera_desktop_example/main.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('CameraExampleApp builds', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const CameraExampleApp());
|
||||||
|
expect(find.text('Camera Desktop Example'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
17
plugins/camera_desktop/example/windows/.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
flutter/ephemeral/
|
||||||
|
|
||||||
|
# Visual Studio user-specific files.
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Visual Studio build-related files.
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
109
plugins/camera_desktop/example/windows/CMakeLists.txt
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# Project-level configuration.
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(camera_desktop_example LANGUAGES CXX)
|
||||||
|
|
||||||
|
# The name of the executable created for the application. Change this to change
|
||||||
|
# the on-disk name of your application.
|
||||||
|
set(BINARY_NAME "camera_desktop_example")
|
||||||
|
|
||||||
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
|
# versions of CMake.
|
||||||
|
cmake_policy(VERSION 3.14...3.25)
|
||||||
|
|
||||||
|
# Define build configuration option.
|
||||||
|
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||||
|
if(IS_MULTICONFIG)
|
||||||
|
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
|
||||||
|
CACHE STRING "" FORCE)
|
||||||
|
else()
|
||||||
|
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()
|
||||||
|
endif()
|
||||||
|
# Define settings for the Profile build mode.
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
|
||||||
|
|
||||||
|
# Use Unicode for all projects.
|
||||||
|
add_definitions(-DUNICODE -D_UNICODE)
|
||||||
|
|
||||||
|
# Compilation settings that should be applied to most targets.
|
||||||
|
#
|
||||||
|
# Be cautious about adding new options here, as plugins use this function by
|
||||||
|
# default. In most cases, you should add new options to specific targets instead
|
||||||
|
# of modifying this function.
|
||||||
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
|
target_compile_features(${TARGET} PUBLIC cxx_std_17)
|
||||||
|
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
|
||||||
|
target_compile_options(${TARGET} PRIVATE /EHsc)
|
||||||
|
target_compile_options(${TARGET} PRIVATE /utf-8)
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Flutter library and tool build rules.
|
||||||
|
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||||
|
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||||
|
|
||||||
|
# Application build; see runner/CMakeLists.txt.
|
||||||
|
add_subdirectory("runner")
|
||||||
|
|
||||||
|
|
||||||
|
# Generated plugin build rules, which manage building the plugins and adding
|
||||||
|
# them to the application.
|
||||||
|
include(flutter/generated_plugins.cmake)
|
||||||
|
|
||||||
|
|
||||||
|
# === Installation ===
|
||||||
|
# Support files are copied into place next to the executable, so that it can
|
||||||
|
# run in place. This is done instead of making a separate bundle (as on Linux)
|
||||||
|
# so that building and running from within Visual Studio will work.
|
||||||
|
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
|
||||||
|
# Make the "install" step default, as it's required to run.
|
||||||
|
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
|
||||||
|
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||||
|
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||||
|
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Copy the native assets provided by the build.dart from all packages.
|
||||||
|
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
|
||||||
|
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
CONFIGURATIONS Profile;Release
|
||||||
|
COMPONENT Runtime)
|
||||||
109
plugins/camera_desktop/example/windows/flutter/CMakeLists.txt
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# This file controls Flutter-level build steps. It should not be edited.
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
|
||||||
|
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||||
|
|
||||||
|
# Configuration provided via flutter tool.
|
||||||
|
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||||
|
|
||||||
|
# TODO: Move the rest of this into files in ephemeral. See
|
||||||
|
# https://github.com/flutter/flutter/issues/57146.
|
||||||
|
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
|
||||||
|
|
||||||
|
# Set fallback configurations for older versions of the flutter tool.
|
||||||
|
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
|
||||||
|
set(FLUTTER_TARGET_PLATFORM "windows-x64")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# === Flutter Library ===
|
||||||
|
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
||||||
|
|
||||||
|
# Published to parent scope for install step.
|
||||||
|
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||||
|
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||||
|
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||||
|
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||||
|
"flutter_export.h"
|
||||||
|
"flutter_windows.h"
|
||||||
|
"flutter_messenger.h"
|
||||||
|
"flutter_plugin_registrar.h"
|
||||||
|
"flutter_texture_registrar.h"
|
||||||
|
)
|
||||||
|
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
|
||||||
|
add_library(flutter INTERFACE)
|
||||||
|
target_include_directories(flutter INTERFACE
|
||||||
|
"${EPHEMERAL_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
|
||||||
|
add_dependencies(flutter flutter_assemble)
|
||||||
|
|
||||||
|
# === Wrapper ===
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_CORE
|
||||||
|
"core_implementations.cc"
|
||||||
|
"standard_codec.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
|
||||||
|
"plugin_registrar.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_APP
|
||||||
|
"flutter_engine.cc"
|
||||||
|
"flutter_view_controller.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${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)
|
||||||
|
|
||||||
|
# === Flutter tool backend ===
|
||||||
|
# _phony_ is a non-existent file to force this command to run every time,
|
||||||
|
# since currently there's no way to get a full input/output list from the
|
||||||
|
# flutter tool.
|
||||||
|
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
|
||||||
|
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
${PHONY_OUTPUT}
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
|
${FLUTTER_TOOL_ENVIRONMENT}
|
||||||
|
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
||||||
|
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(flutter_assemble DEPENDS
|
||||||
|
"${FLUTTER_LIBRARY}"
|
||||||
|
${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <camera_desktop/camera_desktop_plugin.h>
|
||||||
|
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||||
|
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||||
|
|
||||||
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
CameraDesktopPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("CameraDesktopPlugin"));
|
||||||
|
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
|
||||||
|
MediaKitVideoPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
#define GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
|
||||||
|
#include <flutter/plugin_registry.h>
|
||||||
|
|
||||||
|
// Registers Flutter plugins.
|
||||||
|
void RegisterPlugins(flutter::PluginRegistry* registry);
|
||||||
|
|
||||||
|
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#
|
||||||
|
# Generated file, do not edit.
|
||||||
|
#
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
camera_desktop
|
||||||
|
media_kit_libs_windows_video
|
||||||
|
media_kit_video
|
||||||
|
)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|
||||||
|
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||||
|
endforeach(plugin)
|
||||||
|
|
||||||
|
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||||
|
endforeach(ffi_plugin)
|
||||||
40
plugins/camera_desktop/example/windows/runner/CMakeLists.txt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
|
# Define the application target. To change its name, change BINARY_NAME in the
|
||||||
|
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||||
|
# work.
|
||||||
|
#
|
||||||
|
# Any new source files that you add to the application should be added here.
|
||||||
|
add_executable(${BINARY_NAME} WIN32
|
||||||
|
"flutter_window.cpp"
|
||||||
|
"main.cpp"
|
||||||
|
"utils.cpp"
|
||||||
|
"win32_window.cpp"
|
||||||
|
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||||
|
"Runner.rc"
|
||||||
|
"runner.exe.manifest"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply the standard set of build settings. This can be removed for applications
|
||||||
|
# that need different build settings.
|
||||||
|
apply_standard_settings(${BINARY_NAME})
|
||||||
|
|
||||||
|
# Add preprocessor definitions for the build version.
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
|
||||||
|
|
||||||
|
# Disable Windows macros that collide with C++ standard library functions.
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||||
|
|
||||||
|
# Add dependency libraries and include directories. Add any application-specific
|
||||||
|
# dependencies here.
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
|
||||||
|
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
|
|
||||||
|
# Run the Flutter tool portions of the build. This must not be removed.
|
||||||
|
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||||
121
plugins/camera_desktop/example/windows/runner/Runner.rc
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// Microsoft Visual C++ generated resource script.
|
||||||
|
//
|
||||||
|
#pragma code_page(65001)
|
||||||
|
#include "resource.h"
|
||||||
|
|
||||||
|
#define APSTUDIO_READONLY_SYMBOLS
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Generated from the TEXTINCLUDE 2 resource.
|
||||||
|
//
|
||||||
|
#include "winres.h"
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
#undef APSTUDIO_READONLY_SYMBOLS
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// English (United States) resources
|
||||||
|
|
||||||
|
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||||
|
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||||
|
|
||||||
|
#ifdef APSTUDIO_INVOKED
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// TEXTINCLUDE
|
||||||
|
//
|
||||||
|
|
||||||
|
1 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"resource.h\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
2 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"#include ""winres.h""\r\n"
|
||||||
|
"\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
3 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"\r\n"
|
||||||
|
"\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
#endif // APSTUDIO_INVOKED
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Icon
|
||||||
|
//
|
||||||
|
|
||||||
|
// Icon with lowest ID value placed first to ensure application icon
|
||||||
|
// remains consistent on all systems.
|
||||||
|
IDI_APP_ICON ICON "resources\\app_icon.ico"
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Version
|
||||||
|
//
|
||||||
|
|
||||||
|
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
|
||||||
|
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
|
||||||
|
#else
|
||||||
|
#define VERSION_AS_NUMBER 1,0,0,0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(FLUTTER_VERSION)
|
||||||
|
#define VERSION_AS_STRING FLUTTER_VERSION
|
||||||
|
#else
|
||||||
|
#define VERSION_AS_STRING "1.0.0"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEVERSION VERSION_AS_NUMBER
|
||||||
|
PRODUCTVERSION VERSION_AS_NUMBER
|
||||||
|
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||||
|
#ifdef _DEBUG
|
||||||
|
FILEFLAGS VS_FF_DEBUG
|
||||||
|
#else
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
#endif
|
||||||
|
FILEOS VOS__WINDOWS32
|
||||||
|
FILETYPE VFT_APP
|
||||||
|
FILESUBTYPE 0x0L
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904e4"
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", "com.example" "\0"
|
||||||
|
VALUE "FileDescription", "camera_desktop_example" "\0"
|
||||||
|
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||||
|
VALUE "InternalName", "camera_desktop_example" "\0"
|
||||||
|
VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
|
||||||
|
VALUE "OriginalFilename", "camera_desktop_example.exe" "\0"
|
||||||
|
VALUE "ProductName", "camera_desktop_example" "\0"
|
||||||
|
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 1252
|
||||||
|
END
|
||||||
|
END
|
||||||
|
|
||||||
|
#endif // English (United States) resources
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef APSTUDIO_INVOKED
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Generated from the TEXTINCLUDE 3 resource.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
#endif // not APSTUDIO_INVOKED
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#include "flutter_window.h"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
||||||
|
: project_(project) {}
|
||||||
|
|
||||||
|
FlutterWindow::~FlutterWindow() {}
|
||||||
|
|
||||||
|
bool FlutterWindow::OnCreate() {
|
||||||
|
if (!Win32Window::OnCreate()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT frame = GetClientArea();
|
||||||
|
|
||||||
|
// The size here must match the window dimensions to avoid unnecessary surface
|
||||||
|
// creation / destruction in the startup path.
|
||||||
|
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
|
||||||
|
frame.right - frame.left, frame.bottom - frame.top, project_);
|
||||||
|
// Ensure that basic setup of the controller was successful.
|
||||||
|
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RegisterPlugins(flutter_controller_->engine());
|
||||||
|
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||||
|
|
||||||
|
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
||||||
|
this->Show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Flutter can complete the first frame before the "show window" callback is
|
||||||
|
// registered. The following call ensures a frame is pending to ensure the
|
||||||
|
// window is shown. It is a no-op if the first frame hasn't completed yet.
|
||||||
|
flutter_controller_->ForceRedraw();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlutterWindow::OnDestroy() {
|
||||||
|
if (flutter_controller_) {
|
||||||
|
flutter_controller_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window::OnDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT
|
||||||
|
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
// Give Flutter, including plugins, an opportunity to handle window messages.
|
||||||
|
if (flutter_controller_) {
|
||||||
|
std::optional<LRESULT> result =
|
||||||
|
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
|
||||||
|
lparam);
|
||||||
|
if (result) {
|
||||||
|
return *result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message) {
|
||||||
|
case WM_FONTCHANGE:
|
||||||
|
flutter_controller_->engine()->ReloadSystemFonts();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#ifndef RUNNER_FLUTTER_WINDOW_H_
|
||||||
|
#define RUNNER_FLUTTER_WINDOW_H_
|
||||||
|
|
||||||
|
#include <flutter/dart_project.h>
|
||||||
|
#include <flutter/flutter_view_controller.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "win32_window.h"
|
||||||
|
|
||||||
|
// A window that does nothing but host a Flutter view.
|
||||||
|
class FlutterWindow : public Win32Window {
|
||||||
|
public:
|
||||||
|
// Creates a new FlutterWindow hosting a Flutter view running |project|.
|
||||||
|
explicit FlutterWindow(const flutter::DartProject& project);
|
||||||
|
virtual ~FlutterWindow();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Win32Window:
|
||||||
|
bool OnCreate() override;
|
||||||
|
void OnDestroy() override;
|
||||||
|
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// The project to run.
|
||||||
|
flutter::DartProject project_;
|
||||||
|
|
||||||
|
// The Flutter instance hosted by this window.
|
||||||
|
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // RUNNER_FLUTTER_WINDOW_H_
|
||||||
43
plugins/camera_desktop/example/windows/runner/main.cpp
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#include <flutter/dart_project.h>
|
||||||
|
#include <flutter/flutter_view_controller.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "flutter_window.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||||
|
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||||
|
// Attach to console when present (e.g., 'flutter run') or create a
|
||||||
|
// new console when running with a debugger.
|
||||||
|
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
||||||
|
CreateAndAttachConsole();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize COM, so that it is available for use in the library and/or
|
||||||
|
// plugins.
|
||||||
|
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||||
|
|
||||||
|
flutter::DartProject project(L"data");
|
||||||
|
|
||||||
|
std::vector<std::string> command_line_arguments =
|
||||||
|
GetCommandLineArguments();
|
||||||
|
|
||||||
|
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
||||||
|
|
||||||
|
FlutterWindow window(project);
|
||||||
|
Win32Window::Point origin(10, 10);
|
||||||
|
Win32Window::Size size(1280, 720);
|
||||||
|
if (!window.Create(L"camera_desktop_example", origin, size)) {
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
window.SetQuitOnClose(true);
|
||||||
|
|
||||||
|
::MSG msg;
|
||||||
|
while (::GetMessage(&msg, nullptr, 0, 0)) {
|
||||||
|
::TranslateMessage(&msg);
|
||||||
|
::DispatchMessage(&msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
::CoUninitialize();
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
16
plugins/camera_desktop/example/windows/runner/resource.h
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//{{NO_DEPENDENCIES}}
|
||||||
|
// Microsoft Visual C++ generated include file.
|
||||||
|
// Used by Runner.rc
|
||||||
|
//
|
||||||
|
#define IDI_APP_ICON 101
|
||||||
|
|
||||||
|
// Next default values for new objects
|
||||||
|
//
|
||||||
|
#ifdef APSTUDIO_INVOKED
|
||||||
|
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||||
|
#define _APS_NEXT_RESOURCE_VALUE 102
|
||||||
|
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||||
|
#define _APS_NEXT_CONTROL_VALUE 1001
|
||||||
|
#define _APS_NEXT_SYMED_VALUE 101
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- Windows 10 and Windows 11 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
65
plugins/camera_desktop/example/windows/runner/utils.cpp
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
#include <flutter_windows.h>
|
||||||
|
#include <io.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
void CreateAndAttachConsole() {
|
||||||
|
if (::AllocConsole()) {
|
||||||
|
FILE *unused;
|
||||||
|
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
|
||||||
|
_dup2(_fileno(stdout), 1);
|
||||||
|
}
|
||||||
|
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
|
||||||
|
_dup2(_fileno(stdout), 2);
|
||||||
|
}
|
||||||
|
std::ios::sync_with_stdio();
|
||||||
|
FlutterDesktopResyncOutputStreams();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> GetCommandLineArguments() {
|
||||||
|
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
|
||||||
|
int argc;
|
||||||
|
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
|
||||||
|
if (argv == nullptr) {
|
||||||
|
return std::vector<std::string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> command_line_arguments;
|
||||||
|
|
||||||
|
// Skip the first argument as it's the binary name.
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
::LocalFree(argv);
|
||||||
|
|
||||||
|
return command_line_arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
|
||||||
|
if (utf16_string == nullptr) {
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
unsigned int target_length = ::WideCharToMultiByte(
|
||||||
|
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||||
|
-1, nullptr, 0, nullptr, nullptr)
|
||||||
|
-1; // remove the trailing null character
|
||||||
|
int input_length = (int)wcslen(utf16_string);
|
||||||
|
std::string utf8_string;
|
||||||
|
if (target_length == 0 || target_length > utf8_string.max_size()) {
|
||||||
|
return utf8_string;
|
||||||
|
}
|
||||||
|
utf8_string.resize(target_length);
|
||||||
|
int converted_length = ::WideCharToMultiByte(
|
||||||
|
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||||
|
input_length, utf8_string.data(), target_length, nullptr, nullptr);
|
||||||
|
if (converted_length == 0) {
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
return utf8_string;
|
||||||
|
}
|
||||||
19
plugins/camera_desktop/example/windows/runner/utils.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#ifndef RUNNER_UTILS_H_
|
||||||
|
#define RUNNER_UTILS_H_
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Creates a console for the process, and redirects stdout and stderr to
|
||||||
|
// it for both the runner and the Flutter library.
|
||||||
|
void CreateAndAttachConsole();
|
||||||
|
|
||||||
|
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
|
||||||
|
// encoded in UTF-8. Returns an empty std::string on failure.
|
||||||
|
std::string Utf8FromUtf16(const wchar_t* utf16_string);
|
||||||
|
|
||||||
|
// Gets the command line arguments passed in as a std::vector<std::string>,
|
||||||
|
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
|
||||||
|
std::vector<std::string> GetCommandLineArguments();
|
||||||
|
|
||||||
|
#endif // RUNNER_UTILS_H_
|
||||||
288
plugins/camera_desktop/example/windows/runner/win32_window.cpp
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
#include "win32_window.h"
|
||||||
|
|
||||||
|
#include <dwmapi.h>
|
||||||
|
#include <flutter_windows.h>
|
||||||
|
|
||||||
|
#include "resource.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/// Window attribute that enables dark mode window decorations.
|
||||||
|
///
|
||||||
|
/// Redefined in case the developer's machine has a Windows SDK older than
|
||||||
|
/// version 10.0.22000.0.
|
||||||
|
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
|
||||||
|
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||||
|
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||||
|
#endif
|
||||||
|
|
||||||
|
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||||
|
|
||||||
|
/// Registry key for app theme preference.
|
||||||
|
///
|
||||||
|
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
|
||||||
|
/// value indicates apps should use light mode.
|
||||||
|
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
|
||||||
|
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||||
|
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
|
||||||
|
|
||||||
|
// The number of Win32Window objects that currently exist.
|
||||||
|
static int g_active_window_count = 0;
|
||||||
|
|
||||||
|
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||||
|
|
||||||
|
// Scale helper to convert logical scaler values to physical using passed in
|
||||||
|
// scale factor
|
||||||
|
int Scale(int source, double scale_factor) {
|
||||||
|
return static_cast<int>(source * scale_factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
|
||||||
|
// This API is only needed for PerMonitor V1 awareness mode.
|
||||||
|
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
|
||||||
|
HMODULE user32_module = LoadLibraryA("User32.dll");
|
||||||
|
if (!user32_module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto enable_non_client_dpi_scaling =
|
||||||
|
reinterpret_cast<EnableNonClientDpiScaling*>(
|
||||||
|
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
|
||||||
|
if (enable_non_client_dpi_scaling != nullptr) {
|
||||||
|
enable_non_client_dpi_scaling(hwnd);
|
||||||
|
}
|
||||||
|
FreeLibrary(user32_module);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Manages the Win32Window's window class registration.
|
||||||
|
class WindowClassRegistrar {
|
||||||
|
public:
|
||||||
|
~WindowClassRegistrar() = default;
|
||||||
|
|
||||||
|
// Returns the singleton registrar instance.
|
||||||
|
static WindowClassRegistrar* GetInstance() {
|
||||||
|
if (!instance_) {
|
||||||
|
instance_ = new WindowClassRegistrar();
|
||||||
|
}
|
||||||
|
return instance_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the name of the window class, registering the class if it hasn't
|
||||||
|
// previously been registered.
|
||||||
|
const wchar_t* GetWindowClass();
|
||||||
|
|
||||||
|
// Unregisters the window class. Should only be called if there are no
|
||||||
|
// instances of the window.
|
||||||
|
void UnregisterWindowClass();
|
||||||
|
|
||||||
|
private:
|
||||||
|
WindowClassRegistrar() = default;
|
||||||
|
|
||||||
|
static WindowClassRegistrar* instance_;
|
||||||
|
|
||||||
|
bool class_registered_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
|
||||||
|
|
||||||
|
const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||||
|
if (!class_registered_) {
|
||||||
|
WNDCLASS window_class{};
|
||||||
|
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||||
|
window_class.lpszClassName = kWindowClassName;
|
||||||
|
window_class.style = CS_HREDRAW | CS_VREDRAW;
|
||||||
|
window_class.cbClsExtra = 0;
|
||||||
|
window_class.cbWndExtra = 0;
|
||||||
|
window_class.hInstance = GetModuleHandle(nullptr);
|
||||||
|
window_class.hIcon =
|
||||||
|
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
||||||
|
window_class.hbrBackground = 0;
|
||||||
|
window_class.lpszMenuName = nullptr;
|
||||||
|
window_class.lpfnWndProc = Win32Window::WndProc;
|
||||||
|
RegisterClass(&window_class);
|
||||||
|
class_registered_ = true;
|
||||||
|
}
|
||||||
|
return kWindowClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowClassRegistrar::UnregisterWindowClass() {
|
||||||
|
UnregisterClass(kWindowClassName, nullptr);
|
||||||
|
class_registered_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window::Win32Window() {
|
||||||
|
++g_active_window_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window::~Win32Window() {
|
||||||
|
--g_active_window_count;
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Win32Window::Create(const std::wstring& title,
|
||||||
|
const Point& origin,
|
||||||
|
const Size& size) {
|
||||||
|
Destroy();
|
||||||
|
|
||||||
|
const wchar_t* window_class =
|
||||||
|
WindowClassRegistrar::GetInstance()->GetWindowClass();
|
||||||
|
|
||||||
|
const POINT target_point = {static_cast<LONG>(origin.x),
|
||||||
|
static_cast<LONG>(origin.y)};
|
||||||
|
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
|
||||||
|
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
|
||||||
|
double scale_factor = dpi / 96.0;
|
||||||
|
|
||||||
|
HWND window = CreateWindow(
|
||||||
|
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
|
||||||
|
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
|
||||||
|
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
|
||||||
|
nullptr, nullptr, GetModuleHandle(nullptr), this);
|
||||||
|
|
||||||
|
if (!window) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateTheme(window);
|
||||||
|
|
||||||
|
return OnCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Win32Window::Show() {
|
||||||
|
return ShowWindow(window_handle_, SW_SHOWNORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
if (message == WM_NCCREATE) {
|
||||||
|
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
|
||||||
|
SetWindowLongPtr(window, GWLP_USERDATA,
|
||||||
|
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
|
||||||
|
|
||||||
|
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
||||||
|
EnableFullDpiSupportIfAvailable(window);
|
||||||
|
that->window_handle_ = window;
|
||||||
|
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
||||||
|
return that->MessageHandler(window, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProc(window, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT
|
||||||
|
Win32Window::MessageHandler(HWND hwnd,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
switch (message) {
|
||||||
|
case WM_DESTROY:
|
||||||
|
window_handle_ = nullptr;
|
||||||
|
Destroy();
|
||||||
|
if (quit_on_close_) {
|
||||||
|
PostQuitMessage(0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_DPICHANGED: {
|
||||||
|
auto newRectSize = reinterpret_cast<RECT*>(lparam);
|
||||||
|
LONG newWidth = newRectSize->right - newRectSize->left;
|
||||||
|
LONG newHeight = newRectSize->bottom - newRectSize->top;
|
||||||
|
|
||||||
|
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
|
||||||
|
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WM_SIZE: {
|
||||||
|
RECT rect = GetClientArea();
|
||||||
|
if (child_content_ != nullptr) {
|
||||||
|
// Size and position the child window.
|
||||||
|
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
||||||
|
rect.bottom - rect.top, TRUE);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
case WM_ACTIVATE:
|
||||||
|
if (child_content_ != nullptr) {
|
||||||
|
SetFocus(child_content_);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_DWMCOLORIZATIONCOLORCHANGED:
|
||||||
|
UpdateTheme(hwnd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProc(window_handle_, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::Destroy() {
|
||||||
|
OnDestroy();
|
||||||
|
|
||||||
|
if (window_handle_) {
|
||||||
|
DestroyWindow(window_handle_);
|
||||||
|
window_handle_ = nullptr;
|
||||||
|
}
|
||||||
|
if (g_active_window_count == 0) {
|
||||||
|
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
|
||||||
|
return reinterpret_cast<Win32Window*>(
|
||||||
|
GetWindowLongPtr(window, GWLP_USERDATA));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::SetChildContent(HWND content) {
|
||||||
|
child_content_ = content;
|
||||||
|
SetParent(content, window_handle_);
|
||||||
|
RECT frame = GetClientArea();
|
||||||
|
|
||||||
|
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
||||||
|
frame.bottom - frame.top, true);
|
||||||
|
|
||||||
|
SetFocus(child_content_);
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT Win32Window::GetClientArea() {
|
||||||
|
RECT frame;
|
||||||
|
GetClientRect(window_handle_, &frame);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND Win32Window::GetHandle() {
|
||||||
|
return window_handle_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::SetQuitOnClose(bool quit_on_close) {
|
||||||
|
quit_on_close_ = quit_on_close;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Win32Window::OnCreate() {
|
||||||
|
// No-op; provided for subclasses.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::OnDestroy() {
|
||||||
|
// No-op; provided for subclasses.
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::UpdateTheme(HWND const window) {
|
||||||
|
DWORD light_mode;
|
||||||
|
DWORD light_mode_size = sizeof(light_mode);
|
||||||
|
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
|
||||||
|
kGetPreferredBrightnessRegValue,
|
||||||
|
RRF_RT_REG_DWORD, nullptr, &light_mode,
|
||||||
|
&light_mode_size);
|
||||||
|
|
||||||
|
if (result == ERROR_SUCCESS) {
|
||||||
|
BOOL enable_dark_mode = light_mode == 0;
|
||||||
|
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||||
|
&enable_dark_mode, sizeof(enable_dark_mode));
|
||||||
|
}
|
||||||
|
}
|
||||||
102
plugins/camera_desktop/example/windows/runner/win32_window.h
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#ifndef RUNNER_WIN32_WINDOW_H_
|
||||||
|
#define RUNNER_WIN32_WINDOW_H_
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
|
||||||
|
// inherited from by classes that wish to specialize with custom
|
||||||
|
// rendering and input handling
|
||||||
|
class Win32Window {
|
||||||
|
public:
|
||||||
|
struct Point {
|
||||||
|
unsigned int x;
|
||||||
|
unsigned int y;
|
||||||
|
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Size {
|
||||||
|
unsigned int width;
|
||||||
|
unsigned int height;
|
||||||
|
Size(unsigned int width, unsigned int height)
|
||||||
|
: width(width), height(height) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Win32Window();
|
||||||
|
virtual ~Win32Window();
|
||||||
|
|
||||||
|
// Creates a win32 window with |title| that is positioned and sized using
|
||||||
|
// |origin| and |size|. New windows are created on the default monitor. Window
|
||||||
|
// sizes are specified to the OS in physical pixels, hence to ensure a
|
||||||
|
// consistent size this function will scale the inputted width and height as
|
||||||
|
// as appropriate for the default monitor. The window is invisible until
|
||||||
|
// |Show| is called. Returns true if the window was created successfully.
|
||||||
|
bool Create(const std::wstring& title, const Point& origin, const Size& size);
|
||||||
|
|
||||||
|
// Show the current window. Returns true if the window was successfully shown.
|
||||||
|
bool Show();
|
||||||
|
|
||||||
|
// Release OS resources associated with window.
|
||||||
|
void Destroy();
|
||||||
|
|
||||||
|
// Inserts |content| into the window tree.
|
||||||
|
void SetChildContent(HWND content);
|
||||||
|
|
||||||
|
// Returns the backing Window handle to enable clients to set icon and other
|
||||||
|
// window properties. Returns nullptr if the window has been destroyed.
|
||||||
|
HWND GetHandle();
|
||||||
|
|
||||||
|
// If true, closing this window will quit the application.
|
||||||
|
void SetQuitOnClose(bool quit_on_close);
|
||||||
|
|
||||||
|
// Return a RECT representing the bounds of the current client area.
|
||||||
|
RECT GetClientArea();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Processes and route salient window messages for mouse handling,
|
||||||
|
// size change and DPI. Delegates handling of these to member overloads that
|
||||||
|
// inheriting classes can handle.
|
||||||
|
virtual LRESULT MessageHandler(HWND window,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept;
|
||||||
|
|
||||||
|
// Called when CreateAndShow is called, allowing subclass window-related
|
||||||
|
// setup. Subclasses should return false if setup fails.
|
||||||
|
virtual bool OnCreate();
|
||||||
|
|
||||||
|
// Called when Destroy is called.
|
||||||
|
virtual void OnDestroy();
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class WindowClassRegistrar;
|
||||||
|
|
||||||
|
// OS callback called by message pump. Handles the WM_NCCREATE message which
|
||||||
|
// is passed when the non-client area is being created and enables automatic
|
||||||
|
// non-client DPI scaling so that the non-client area automatically
|
||||||
|
// responds to changes in DPI. All other messages are handled by
|
||||||
|
// MessageHandler.
|
||||||
|
static LRESULT CALLBACK WndProc(HWND const window,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept;
|
||||||
|
|
||||||
|
// Retrieves a class instance pointer for |window|
|
||||||
|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
|
||||||
|
|
||||||
|
// Update the window frame's theme to match the system theme.
|
||||||
|
static void UpdateTheme(HWND const window);
|
||||||
|
|
||||||
|
bool quit_on_close_ = false;
|
||||||
|
|
||||||
|
// window handle for top level window.
|
||||||
|
HWND window_handle_ = nullptr;
|
||||||
|
|
||||||
|
// window handle for hosted content.
|
||||||
|
HWND child_content_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // RUNNER_WIN32_WINDOW_H_
|
||||||
23
plugins/camera_desktop/ios/camera_desktop.podspec
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = 'camera_desktop'
|
||||||
|
s.version = '1.2.1'
|
||||||
|
s.summary = 'Flutter camera plugin (iOS stub).'
|
||||||
|
s.description = <<-DESC
|
||||||
|
Flutter camera plugin for desktop platforms. iOS stub for platform declaration.
|
||||||
|
DESC
|
||||||
|
s.homepage = 'https://github.com/hugocornellier/camera_desktop'
|
||||||
|
s.license = { :file => '../LICENSE' }
|
||||||
|
s.author = { 'Hugo Cornellier' => 'hugo@hugocornellier.com' }
|
||||||
|
s.source = { :path => '.' }
|
||||||
|
s.source_files = 'camera_desktop/Sources/camera_desktop/**/*.{swift,h,m}'
|
||||||
|
s.dependency 'Flutter'
|
||||||
|
s.platform = :ios, '13.0'
|
||||||
|
|
||||||
|
s.resource_bundles = { 'camera_desktop_privacy' => ['camera_desktop/Sources/camera_desktop/PrivacyInfo.xcprivacy'] }
|
||||||
|
|
||||||
|
s.pod_target_xcconfig = {
|
||||||
|
'DEFINES_MODULE' => 'YES',
|
||||||
|
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
|
||||||
|
}
|
||||||
|
s.swift_version = '5.0'
|
||||||
|
end
|
||||||
27
plugins/camera_desktop/ios/camera_desktop/Package.swift
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// swift-tools-version: 5.9
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "camera_desktop",
|
||||||
|
platforms: [
|
||||||
|
.iOS("13.0")
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
.library(name: "camera-desktop", targets: ["camera_desktop"])
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(name: "FlutterFramework", path: "../FlutterFramework")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "camera_desktop",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "FlutterFramework", package: "FlutterFramework")
|
||||||
|
],
|
||||||
|
resources: [
|
||||||
|
.process("PrivacyInfo.xcprivacy"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public class CameraDesktopPlugin: NSObject, FlutterPlugin {
|
||||||
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||||
|
let channel = FlutterMethodChannel(name: "camera_desktop", binaryMessenger: registrar.messenger())
|
||||||
|
let instance = CameraDesktopPlugin()
|
||||||
|
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
switch call.method {
|
||||||
|
case "getPlatformVersion":
|
||||||
|
result("iOS " + UIDevice.current.systemVersion)
|
||||||
|
default:
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyTrackingDomains</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
9
plugins/camera_desktop/lib/camera_desktop.dart
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/// A Flutter camera plugin for desktop platforms (Linux, macOS, Windows).
|
||||||
|
///
|
||||||
|
/// This plugin implements [CameraPlatform] from camera_platform_interface,
|
||||||
|
/// allowing it to work seamlessly with the standard camera package.
|
||||||
|
/// Users simply add camera_desktop as a dependency and the standard
|
||||||
|
/// CameraController works on desktop automatically.
|
||||||
|
library;
|
||||||
|
|
||||||
|
export 'src/camera_desktop_plugin.dart';
|
||||||
685
plugins/camera_desktop/lib/src/camera_desktop_plugin.dart
Normal file
@@ -0,0 +1,685 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:camera_platform_interface/camera_platform_interface.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:stream_transform/stream_transform.dart';
|
||||||
|
|
||||||
|
import 'image_stream_ffi.dart';
|
||||||
|
|
||||||
|
/// Desktop implementation of [CameraPlatform].
|
||||||
|
///
|
||||||
|
/// On Linux, uses GStreamer + V4L2. On macOS, uses AVFoundation.
|
||||||
|
/// On Windows, uses Media Foundation (IMFCaptureEngine).
|
||||||
|
///
|
||||||
|
/// This plugin registers itself as the camera platform implementation for
|
||||||
|
/// desktop. When an app depends on both `camera` and `camera_desktop`, Flutter
|
||||||
|
/// automatically calls [registerWith], making [CameraController] work out of
|
||||||
|
/// the box.
|
||||||
|
class CameraDesktopPlugin extends CameraPlatform {
|
||||||
|
/// Creates a new [CameraDesktopPlugin].
|
||||||
|
///
|
||||||
|
/// The [channel] parameter is exposed for testing only.
|
||||||
|
CameraDesktopPlugin({
|
||||||
|
@visibleForTesting MethodChannel? channel,
|
||||||
|
this.mirrorPreview = true,
|
||||||
|
}) : _channel =
|
||||||
|
channel ?? const MethodChannel('plugins.flutter.io/camera_desktop');
|
||||||
|
|
||||||
|
/// Registers this class as the default [CameraPlatform] implementation.
|
||||||
|
static void registerWith() {
|
||||||
|
CameraPlatform.instance = CameraDesktopPlugin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The method channel used to communicate with the native platform.
|
||||||
|
final MethodChannel _channel;
|
||||||
|
|
||||||
|
/// Returns desktop backend capabilities for feature-gating advanced controls.
|
||||||
|
///
|
||||||
|
/// Keys are stable capability names (e.g. `supportsMirrorControl`).
|
||||||
|
/// If the native side does not implement this method yet, returns an empty map.
|
||||||
|
Future<Map<String, bool>> getPlatformCapabilities() async {
|
||||||
|
try {
|
||||||
|
final raw = await _channel.invokeMapMethod<String, dynamic>(
|
||||||
|
'getPlatformCapabilities',
|
||||||
|
);
|
||||||
|
if (raw == null) return const <String, bool>{};
|
||||||
|
final out = <String, bool>{};
|
||||||
|
raw.forEach((key, value) {
|
||||||
|
if (value is bool) {
|
||||||
|
out[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
} on MissingPluginException {
|
||||||
|
return const <String, bool>{};
|
||||||
|
} on PlatformException {
|
||||||
|
return const <String, bool>{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether to mirror the preview horizontally (like a mirror).
|
||||||
|
/// Defaults to `true`. Set to `false` to show the unmirrored camera image.
|
||||||
|
@Deprecated('Mirroring is now handled at the native capture level.')
|
||||||
|
final bool mirrorPreview;
|
||||||
|
|
||||||
|
/// Whether the native → Dart method-call handler has been installed.
|
||||||
|
bool _nativeCallHandlerSet = false;
|
||||||
|
|
||||||
|
/// Lazily installs the native → Dart method-call handler.
|
||||||
|
///
|
||||||
|
/// Called before the first camera is created. This cannot run in the
|
||||||
|
/// constructor because [registerWith] executes during plugin registration,
|
||||||
|
/// before [WidgetsFlutterBinding.ensureInitialized].
|
||||||
|
void _ensureNativeCallHandler() {
|
||||||
|
if (!_nativeCallHandlerSet) {
|
||||||
|
_channel.setMethodCallHandler(_handleNativeCall);
|
||||||
|
_nativeCallHandlerSet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mapping from cameraId to textureId (separate to decouple lifecycles).
|
||||||
|
final Map<int, int> _textureIds = {};
|
||||||
|
|
||||||
|
/// Broadcast stream for all camera events, filtered by cameraId downstream.
|
||||||
|
final StreamController<CameraEvent> _eventStreamController =
|
||||||
|
StreamController<CameraEvent>.broadcast();
|
||||||
|
|
||||||
|
/// Per-camera image stream controllers for [onStreamedFrameAvailable].
|
||||||
|
///
|
||||||
|
/// Only populated when [ImageStreamFfi] is unavailable and the fallback
|
||||||
|
/// MethodChannel path is used for frame delivery. When FFI is active,
|
||||||
|
/// frames bypass this map entirely and `_handleNativeCall`'s
|
||||||
|
/// `imageStreamFrame` branch is a no-op for that camera.
|
||||||
|
final Map<int, StreamController<CameraImageData>> _imageStreamControllers =
|
||||||
|
{};
|
||||||
|
|
||||||
|
/// Active image streams (FFI or fallback) keyed by cameraId.
|
||||||
|
///
|
||||||
|
/// Lets [dispose] tear down a stream whose subscription was never cancelled —
|
||||||
|
/// e.g. when an app disposes its `CameraController` without first calling
|
||||||
|
/// `stopImageStream()`. In that case `onCancel` never fires, so without this
|
||||||
|
/// the FFI poll timer (and its controller) would leak and keep polling.
|
||||||
|
final Map<int, _ActiveImageStream> _activeImageStreams = {};
|
||||||
|
|
||||||
|
/// Factory for the FFI image-stream poller. Overridable in tests to inject a
|
||||||
|
/// fake (or capture the real) poller without depending on call timing.
|
||||||
|
@visibleForTesting
|
||||||
|
ImageStreamPoller? Function(int streamHandle) imageStreamPollerFactory =
|
||||||
|
ImageStreamFfi.tryCreate;
|
||||||
|
|
||||||
|
/// Handles method calls from the native side (events pushed to Dart).
|
||||||
|
///
|
||||||
|
/// Dispatches `cameraError`, `cameraClosing`, and `imageStreamFrame`
|
||||||
|
/// events from native code into the appropriate Dart stream controllers.
|
||||||
|
Future<dynamic> _handleNativeCall(MethodCall call) async {
|
||||||
|
final args = call.arguments as Map<Object?, Object?>?;
|
||||||
|
switch (call.method) {
|
||||||
|
case 'cameraError':
|
||||||
|
final cameraId = args!['cameraId']! as int;
|
||||||
|
final description = args['description']! as String;
|
||||||
|
_eventStreamController.add(CameraErrorEvent(cameraId, description));
|
||||||
|
case 'cameraClosing':
|
||||||
|
final cameraId = args!['cameraId']! as int;
|
||||||
|
_eventStreamController.add(CameraClosingEvent(cameraId));
|
||||||
|
case 'imageStreamFrame':
|
||||||
|
final cameraId = args!['cameraId']! as int;
|
||||||
|
final controller = _imageStreamControllers[cameraId];
|
||||||
|
if (controller != null && !controller.isClosed) {
|
||||||
|
final width = args['width']! as int;
|
||||||
|
final height = args['height']! as int;
|
||||||
|
final bytesPerRow = args['bytesPerRow'] as int? ?? (width * 4);
|
||||||
|
final bytes = args['bytes']! as Uint8List;
|
||||||
|
controller.add(
|
||||||
|
CameraImageData(
|
||||||
|
format: CameraImageFormat(
|
||||||
|
ImageFormatGroup.bgra8888,
|
||||||
|
raw: Platform.isMacOS ? 'BGRA' : 'RGBA',
|
||||||
|
),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
planes: [
|
||||||
|
CameraImagePlane(
|
||||||
|
bytes: bytes,
|
||||||
|
bytesPerRow: bytesPerRow,
|
||||||
|
bytesPerPixel: 4,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filters the global event stream to events for a specific [cameraId].
|
||||||
|
Stream<CameraEvent> _cameraEvents(int cameraId) => _eventStreamController
|
||||||
|
.stream
|
||||||
|
.where((CameraEvent e) => e.cameraId == cameraId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<CameraDescription>> availableCameras() async {
|
||||||
|
final result = await _channel.invokeListMethod<Map<dynamic, dynamic>>(
|
||||||
|
'availableCameras',
|
||||||
|
);
|
||||||
|
if (result == null) return <CameraDescription>[];
|
||||||
|
return result.map((Map<dynamic, dynamic> m) {
|
||||||
|
return CameraDescription(
|
||||||
|
name: m['name'] as String,
|
||||||
|
lensDirection: CameraLensDirection.values[m['lensDirection'] as int],
|
||||||
|
sensorOrientation: m['sensorOrientation'] as int,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> createCamera(
|
||||||
|
CameraDescription cameraDescription,
|
||||||
|
ResolutionPreset? resolutionPreset, {
|
||||||
|
bool enableAudio = false,
|
||||||
|
}) async {
|
||||||
|
return createCameraWithSettings(
|
||||||
|
cameraDescription,
|
||||||
|
MediaSettings(
|
||||||
|
resolutionPreset: resolutionPreset,
|
||||||
|
enableAudio: enableAudio,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a camera with the given [mediaSettings].
|
||||||
|
///
|
||||||
|
/// The `videoBitrate` and `audioBitrate` fields are accessed via dynamic
|
||||||
|
/// dispatch with try/catch because older versions of
|
||||||
|
/// `camera_platform_interface` may not expose them.
|
||||||
|
@override
|
||||||
|
Future<int> createCameraWithSettings(
|
||||||
|
CameraDescription cameraDescription,
|
||||||
|
MediaSettings mediaSettings,
|
||||||
|
) async {
|
||||||
|
_ensureNativeCallHandler();
|
||||||
|
int? videoBitrate;
|
||||||
|
try {
|
||||||
|
final dynamic dynamicSettings = mediaSettings;
|
||||||
|
final dynamic value = dynamicSettings.videoBitrate;
|
||||||
|
if (value is int) {
|
||||||
|
videoBitrate = value;
|
||||||
|
} else if (value is num) {
|
||||||
|
videoBitrate = value.toInt();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
int? audioBitrate;
|
||||||
|
try {
|
||||||
|
final dynamic dynamicSettings = mediaSettings;
|
||||||
|
final dynamic value = dynamicSettings.audioBitrate;
|
||||||
|
if (value is int) {
|
||||||
|
audioBitrate = value;
|
||||||
|
} else if (value is num) {
|
||||||
|
audioBitrate = value.toInt();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
try {
|
||||||
|
final result = await _channel.invokeMapMethod<String, dynamic>('create', {
|
||||||
|
'cameraName': cameraDescription.name,
|
||||||
|
'resolutionPreset':
|
||||||
|
mediaSettings.resolutionPreset?.index ?? ResolutionPreset.max.index,
|
||||||
|
'enableAudio': mediaSettings.enableAudio,
|
||||||
|
'fps': mediaSettings.fps,
|
||||||
|
if (videoBitrate != null) 'videoBitrate': videoBitrate,
|
||||||
|
if (audioBitrate != null) 'audioBitrate': audioBitrate,
|
||||||
|
});
|
||||||
|
final cameraId = result!['cameraId'] as int;
|
||||||
|
final textureId = result['textureId'] as int;
|
||||||
|
_textureIds[cameraId] = textureId;
|
||||||
|
return cameraId;
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
throw CameraException(e.code, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> initializeCamera(
|
||||||
|
int cameraId, {
|
||||||
|
ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final result = await _channel.invokeMapMethod<String, dynamic>(
|
||||||
|
'initialize',
|
||||||
|
{'cameraId': cameraId},
|
||||||
|
);
|
||||||
|
_eventStreamController.add(
|
||||||
|
CameraInitializedEvent(
|
||||||
|
cameraId,
|
||||||
|
(result!['previewWidth'] as num).toDouble(),
|
||||||
|
(result['previewHeight'] as num).toDouble(),
|
||||||
|
ExposureMode.auto,
|
||||||
|
false,
|
||||||
|
FocusMode.auto,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
_eventStreamController.add(
|
||||||
|
CameraErrorEvent(cameraId, e.message ?? 'Initialization failed'),
|
||||||
|
);
|
||||||
|
throw CameraException(e.code, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disposes the camera and releases all associated resources.
|
||||||
|
///
|
||||||
|
/// Platform exceptions during disposal are silently ignored to ensure
|
||||||
|
/// cleanup always completes.
|
||||||
|
@override
|
||||||
|
Future<void> dispose(int cameraId) async {
|
||||||
|
// Stop any active image-stream poller BEFORE native dispose. This prevents
|
||||||
|
// a leaked 8ms poll timer when the stream subscription was never cancelled
|
||||||
|
// (CameraController.dispose() does not stop image streams), and ensures no
|
||||||
|
// Dart poll reads a shared buffer the native side is about to free.
|
||||||
|
final active = _activeImageStreams.remove(cameraId);
|
||||||
|
if (active != null) {
|
||||||
|
active.tornDown = true;
|
||||||
|
active.ffi?.stop();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await _channel.invokeMethod<void>('dispose', {'cameraId': cameraId});
|
||||||
|
} on PlatformException catch (_) {
|
||||||
|
} finally {
|
||||||
|
_textureIds.remove(cameraId);
|
||||||
|
final imageController = _imageStreamControllers.remove(cameraId);
|
||||||
|
if (imageController != null && !imageController.isClosed) {
|
||||||
|
imageController.close();
|
||||||
|
}
|
||||||
|
if (active != null) {
|
||||||
|
active.ffi?.dispose();
|
||||||
|
if (!active.controller.isClosed) {
|
||||||
|
await active.controller.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<CameraInitializedEvent> onCameraInitialized(int cameraId) =>
|
||||||
|
_cameraEvents(cameraId).whereType<CameraInitializedEvent>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<CameraResolutionChangedEvent> onCameraResolutionChanged(
|
||||||
|
int cameraId,
|
||||||
|
) => _cameraEvents(cameraId).whereType<CameraResolutionChangedEvent>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<CameraClosingEvent> onCameraClosing(int cameraId) =>
|
||||||
|
_cameraEvents(cameraId).whereType<CameraClosingEvent>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<CameraErrorEvent> onCameraError(int cameraId) =>
|
||||||
|
_cameraEvents(cameraId).whereType<CameraErrorEvent>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<VideoRecordedEvent> onVideoRecordedEvent(int cameraId) =>
|
||||||
|
_cameraEvents(cameraId).whereType<VideoRecordedEvent>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<DeviceOrientationChangedEvent> onDeviceOrientationChanged() =>
|
||||||
|
Stream<DeviceOrientationChangedEvent>.value(
|
||||||
|
const DeviceOrientationChangedEvent(DeviceOrientation.landscapeLeft),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Builds the camera preview widget for the given [cameraId].
|
||||||
|
///
|
||||||
|
/// On macOS and Linux the native backend mirrors the texture, so the
|
||||||
|
/// [Texture] widget is returned as-is. On Windows, IMFCaptureEngine does
|
||||||
|
/// not mirror natively, so the texture is wrapped in a horizontal
|
||||||
|
/// [Transform] flip.
|
||||||
|
@override
|
||||||
|
Widget buildPreview(int cameraId) {
|
||||||
|
final textureId = _textureIds[cameraId];
|
||||||
|
if (textureId == null) {
|
||||||
|
throw CameraException(
|
||||||
|
'buildPreview',
|
||||||
|
'Camera $cameraId has no registered texture. '
|
||||||
|
'Was createCamera called?',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final texture = Texture(textureId: textureId);
|
||||||
|
if (!Platform.isWindows) return texture;
|
||||||
|
return Transform(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
transform: Matrix4.diagonal3Values(-1, 1, 1),
|
||||||
|
child: texture,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> pausePreview(int cameraId) async {
|
||||||
|
try {
|
||||||
|
await _channel.invokeMethod<void>('pausePreview', {'cameraId': cameraId});
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
throw CameraException(e.code, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> resumePreview(int cameraId) async {
|
||||||
|
try {
|
||||||
|
await _channel.invokeMethod<void>('resumePreview', {
|
||||||
|
'cameraId': cameraId,
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
throw CameraException(e.code, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggles horizontal mirroring on the live camera feed.
|
||||||
|
///
|
||||||
|
/// On macOS, this sets `isVideoMirrored` on the AVCaptureConnection.
|
||||||
|
/// On Linux, this toggles the `videoflip` GStreamer element's method.
|
||||||
|
/// On Windows, this returns a platform `unsupported` error.
|
||||||
|
///
|
||||||
|
/// Can be called while the camera is running, no restart needed.
|
||||||
|
/// Silently ignored via [MissingPluginException] if the native side
|
||||||
|
/// has no handler for this platform.
|
||||||
|
Future<void> setMirror(int cameraId, bool mirrored) async {
|
||||||
|
try {
|
||||||
|
await _channel.invokeMethod<void>('setMirror', {
|
||||||
|
'cameraId': cameraId,
|
||||||
|
'mirrored': mirrored,
|
||||||
|
});
|
||||||
|
} on MissingPluginException catch (_) {
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
throw CameraException(e.code, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool supportsImageStreaming() => true;
|
||||||
|
|
||||||
|
/// Returns a stream of [CameraImageData] frames from the camera.
|
||||||
|
///
|
||||||
|
/// Image delivery uses a two-path architecture:
|
||||||
|
/// 1. **FFI path** (preferred): reads directly from a native shared buffer
|
||||||
|
/// via `dart:ffi` for minimal copies (1 per frame). When active, frames
|
||||||
|
/// bypass [_imageStreamControllers] entirely, so `_handleNativeCall`'s
|
||||||
|
/// `imageStreamFrame` branch is a no-op for that camera.
|
||||||
|
/// 2. **MethodChannel fallback**: if FFI setup fails (symbols not found),
|
||||||
|
/// frames are delivered through `_handleNativeCall` and stored in
|
||||||
|
/// [_imageStreamControllers].
|
||||||
|
///
|
||||||
|
/// The stream handle returned by native `startImageStream` may be an int
|
||||||
|
/// directly or a map containing a `streamHandle` key. Falls back to
|
||||||
|
/// [cameraId] for backward compatibility with older native implementations.
|
||||||
|
@override
|
||||||
|
Stream<CameraImageData> onStreamedFrameAvailable(
|
||||||
|
int cameraId, {
|
||||||
|
CameraImageStreamOptions? options,
|
||||||
|
}) {
|
||||||
|
int extractStreamHandle(dynamic value) {
|
||||||
|
if (value is int) return value;
|
||||||
|
if (value is Map<dynamic, dynamic>) {
|
||||||
|
final dynamic raw = value['streamHandle'];
|
||||||
|
if (raw is int) return raw;
|
||||||
|
}
|
||||||
|
return cameraId;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageStreamPoller? ffi;
|
||||||
|
int streamHandle = cameraId;
|
||||||
|
late final StreamController<CameraImageData> controller;
|
||||||
|
|
||||||
|
controller = StreamController<CameraImageData>(
|
||||||
|
onListen: () async {
|
||||||
|
// Register the active stream up front so a concurrent dispose() can
|
||||||
|
// find and tear it down even while startImageStream is still in flight.
|
||||||
|
final active = _ActiveImageStream(controller);
|
||||||
|
_activeImageStreams[cameraId] = active;
|
||||||
|
|
||||||
|
final dynamic value = await _channel.invokeMethod<dynamic>(
|
||||||
|
'startImageStream',
|
||||||
|
{'cameraId': cameraId},
|
||||||
|
);
|
||||||
|
streamHandle = extractStreamHandle(value);
|
||||||
|
|
||||||
|
// dispose() may have run while we awaited startImageStream. If so, do
|
||||||
|
// not start polling — the camera is already being torn down.
|
||||||
|
if (active.tornDown) return;
|
||||||
|
|
||||||
|
ffi = imageStreamPollerFactory(streamHandle);
|
||||||
|
active.ffi = ffi;
|
||||||
|
if (ffi == null) {
|
||||||
|
_imageStreamControllers[cameraId] = controller;
|
||||||
|
} else {
|
||||||
|
ffi!.start(controller);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel: () async {
|
||||||
|
final active = _activeImageStreams.remove(cameraId);
|
||||||
|
// If dispose() already took over teardown, it owns the native stop and
|
||||||
|
// FFI cleanup. Just ensure the local poller is stopped and bail, so we
|
||||||
|
// never call stopImageStream on an already-disposed camera.
|
||||||
|
if (active == null || active.tornDown) {
|
||||||
|
ffi?.stop();
|
||||||
|
ffi?.dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister the native callback first so no new frames are dispatched.
|
||||||
|
ffi?.stop();
|
||||||
|
_imageStreamControllers.remove(cameraId);
|
||||||
|
// Tell native to stop streaming. Wrapped defensively: the camera may
|
||||||
|
// have been disposed between the check above and this call.
|
||||||
|
try {
|
||||||
|
await _channel.invokeMethod<void>('stopImageStream', {
|
||||||
|
'cameraId': cameraId,
|
||||||
|
'streamHandle': streamHandle,
|
||||||
|
});
|
||||||
|
} on PlatformException catch (_) {}
|
||||||
|
// Native has stopped, safe to release FFI resources.
|
||||||
|
ffi?.dispose();
|
||||||
|
},
|
||||||
|
onPause: () {},
|
||||||
|
onResume: () {},
|
||||||
|
);
|
||||||
|
|
||||||
|
return controller.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<XFile> takePicture(int cameraId) async {
|
||||||
|
try {
|
||||||
|
final path = await _channel.invokeMethod<String>('takePicture', {
|
||||||
|
'cameraId': cameraId,
|
||||||
|
});
|
||||||
|
return XFile(path!);
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
throw CameraException(e.code, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No-op on desktop, no preparation needed before recording.
|
||||||
|
@override
|
||||||
|
Future<void> prepareForVideoRecording() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> startVideoCapturing(VideoCaptureOptions options) async {
|
||||||
|
if (options.streamCallback != null) {
|
||||||
|
throw CameraException(
|
||||||
|
'startVideoCapturing',
|
||||||
|
'Simultaneous recording and streaming via streamCallback is not yet supported on desktop. Use onStreamedFrameAvailable() and startVideoRecording() separately.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await startVideoRecording(options.cameraId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> startVideoRecording(
|
||||||
|
int cameraId, {
|
||||||
|
Duration? maxVideoDuration,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _channel.invokeMethod<void>('startVideoRecording', {
|
||||||
|
'cameraId': cameraId,
|
||||||
|
if (maxVideoDuration != null)
|
||||||
|
'maxVideoDuration': maxVideoDuration.inMilliseconds,
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
throw CameraException(e.code, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<XFile> stopVideoRecording(int cameraId) async {
|
||||||
|
try {
|
||||||
|
final dynamic value = await _channel.invokeMethod<dynamic>(
|
||||||
|
'stopVideoRecording',
|
||||||
|
{'cameraId': cameraId},
|
||||||
|
);
|
||||||
|
if (value is String) {
|
||||||
|
return XFile(value);
|
||||||
|
}
|
||||||
|
final map = value as Map<dynamic, dynamic>;
|
||||||
|
final path = map['path'] as String?;
|
||||||
|
if (path == null || path.isEmpty) {
|
||||||
|
throw CameraException(
|
||||||
|
'stopVideoRecording',
|
||||||
|
'Native stopVideoRecording returned no output path.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return XFile(path);
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
throw CameraException(e.code, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> pauseVideoRecording(int cameraId) async {
|
||||||
|
throw CameraException(
|
||||||
|
'pauseVideoRecording',
|
||||||
|
'Pausing video recording is not supported on desktop.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> resumeVideoRecording(int cameraId) async {
|
||||||
|
throw CameraException(
|
||||||
|
'resumeVideoRecording',
|
||||||
|
'Resuming video recording is not supported on desktop.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No-op for [FlashMode.off]; desktop cameras typically lack flash hardware.
|
||||||
|
@override
|
||||||
|
Future<void> setFlashMode(int cameraId, FlashMode mode) async {
|
||||||
|
if (mode == FlashMode.off) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw CameraException(
|
||||||
|
'setFlashMode',
|
||||||
|
'Flash mode is not supported on desktop.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No-op for [ExposureMode.auto] (the default); throws otherwise.
|
||||||
|
@override
|
||||||
|
Future<void> setExposureMode(int cameraId, ExposureMode mode) async {
|
||||||
|
if (mode == ExposureMode.auto) return;
|
||||||
|
throw CameraException(
|
||||||
|
'setExposureMode',
|
||||||
|
'Exposure mode control is not supported on desktop.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setExposurePoint(int cameraId, Point<double>? point) async {
|
||||||
|
throw CameraException(
|
||||||
|
'setExposurePoint',
|
||||||
|
'Exposure point is not supported on desktop.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<double> getMinExposureOffset(int cameraId) async => 0.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<double> getMaxExposureOffset(int cameraId) async => 0.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<double> getExposureOffsetStepSize(int cameraId) async => 0.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<double> setExposureOffset(int cameraId, double offset) async => 0.0;
|
||||||
|
|
||||||
|
/// No-op for [FocusMode.auto] (the default); throws otherwise.
|
||||||
|
@override
|
||||||
|
Future<void> setFocusMode(int cameraId, FocusMode mode) async {
|
||||||
|
if (mode == FocusMode.auto) return;
|
||||||
|
throw CameraException(
|
||||||
|
'setFocusMode',
|
||||||
|
'Focus mode control is not supported on desktop.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setFocusPoint(int cameraId, Point<double>? point) async {
|
||||||
|
throw CameraException(
|
||||||
|
'setFocusPoint',
|
||||||
|
'Focus point is not supported on desktop.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<double> getMinZoomLevel(int cameraId) async => 1.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<double> getMaxZoomLevel(int cameraId) async => 1.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setZoomLevel(int cameraId, double zoom) async {
|
||||||
|
if (zoom != 1.0) {
|
||||||
|
throw CameraException(
|
||||||
|
'setZoomLevel',
|
||||||
|
'Zoom is not supported on desktop. Only 1.0 is accepted.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No-op on desktop, orientation locking is not applicable.
|
||||||
|
@override
|
||||||
|
Future<void> lockCaptureOrientation(
|
||||||
|
int cameraId,
|
||||||
|
DeviceOrientation orientation,
|
||||||
|
) async {}
|
||||||
|
|
||||||
|
/// No-op on desktop, orientation locking is not applicable.
|
||||||
|
@override
|
||||||
|
Future<void> unlockCaptureOrientation(int cameraId) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setDescriptionWhileRecording(
|
||||||
|
CameraDescription description,
|
||||||
|
) async {
|
||||||
|
throw CameraException(
|
||||||
|
'setDescriptionWhileRecording',
|
||||||
|
'Switching camera during recording is not supported on desktop.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tracks a single active image stream so [CameraDesktopPlugin.dispose] can tear
|
||||||
|
/// it down even when its subscription was never cancelled.
|
||||||
|
class _ActiveImageStream {
|
||||||
|
_ActiveImageStream(this.controller);
|
||||||
|
|
||||||
|
/// The controller backing the camera's image stream.
|
||||||
|
final StreamController<CameraImageData> controller;
|
||||||
|
|
||||||
|
/// The FFI poller, if the FFI fast path is active (null on the fallback path).
|
||||||
|
ImageStreamPoller? ffi;
|
||||||
|
|
||||||
|
/// Set once [CameraDesktopPlugin.dispose] has taken over teardown, so
|
||||||
|
/// `onListen`/`onCancel` know not to start polling or to double-stop native.
|
||||||
|
bool tornDown = false;
|
||||||
|
}
|
||||||
18
plugins/camera_desktop/lib/src/camera_desktop_stub.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/// Flutter plugin registration stub for Dart-only initialization.
|
||||||
|
///
|
||||||
|
/// This class is automatically used by Flutter's plugin registration system
|
||||||
|
/// to initialize the camera_desktop plugin on platforms that support
|
||||||
|
/// Dart-only plugins.
|
||||||
|
///
|
||||||
|
/// **Note:** End users do not need to interact with this class directly.
|
||||||
|
/// Flutter handles plugin registration automatically.
|
||||||
|
class CameraDesktopDart {
|
||||||
|
/// Creates an instance for Dart-only plugin registration.
|
||||||
|
const CameraDesktopDart();
|
||||||
|
|
||||||
|
/// Registers the Dart implementation of this plugin.
|
||||||
|
///
|
||||||
|
/// Called automatically by Flutter during plugin initialization.
|
||||||
|
/// Do not call this method directly.
|
||||||
|
static void registerWith() {}
|
||||||
|
}
|
||||||
5
plugins/camera_desktop/lib/src/camera_desktop_web.dart
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/// Web plugin registration stub.
|
||||||
|
class CameraDesktopWeb {
|
||||||
|
/// No-op registration, camera_desktop is a desktop-only plugin.
|
||||||
|
static void registerWith(dynamic registrar) {}
|
||||||
|
}
|
||||||
292
plugins/camera_desktop/lib/src/image_stream_ffi.dart
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ffi';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:camera_platform_interface/camera_platform_interface.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
/// FFI struct matching the native ImageStreamBuffer layout (32-byte header).
|
||||||
|
///
|
||||||
|
/// Layout:
|
||||||
|
/// int64_t sequence (offset 0)
|
||||||
|
/// int32_t width (offset 8)
|
||||||
|
/// int32_t height (offset 12)
|
||||||
|
/// int32_t bytes_per_row (offset 16)
|
||||||
|
/// int32_t format (offset 20) -- 0=BGRA, 1=RGBA
|
||||||
|
/// int32_t ready (offset 24) -- 1=Dart may read, 0=native writing
|
||||||
|
/// int32_t _pad (offset 28)
|
||||||
|
/// uint8_t pixels[] (offset 32)
|
||||||
|
final class ImageStreamBuffer extends Struct {
|
||||||
|
/// Frame sequence number, incremented by native code for each new frame.
|
||||||
|
@Int64()
|
||||||
|
external int sequence;
|
||||||
|
|
||||||
|
/// Frame width in pixels.
|
||||||
|
@Int32()
|
||||||
|
external int width;
|
||||||
|
|
||||||
|
/// Frame height in pixels.
|
||||||
|
@Int32()
|
||||||
|
external int height;
|
||||||
|
|
||||||
|
/// Number of bytes per row (may include padding beyond width * 4).
|
||||||
|
@Int32()
|
||||||
|
external int bytesPerRow;
|
||||||
|
|
||||||
|
/// Pixel format: 0 = BGRA (macOS), 1 = RGBA (Linux/Windows).
|
||||||
|
@Int32()
|
||||||
|
external int format;
|
||||||
|
|
||||||
|
/// Ready flag: 1 = Dart may read, 0 = native is writing.
|
||||||
|
@Int32()
|
||||||
|
external int ready;
|
||||||
|
|
||||||
|
/// Padding for 8-byte alignment.
|
||||||
|
@Int32()
|
||||||
|
// ignore: unused_field
|
||||||
|
external int _pad;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Native function signature for retrieving the shared image buffer pointer.
|
||||||
|
typedef _GetBufferNative = Pointer<Void> Function(Int64 streamHandle);
|
||||||
|
|
||||||
|
/// Dart-side function type for [_GetBufferNative].
|
||||||
|
typedef _GetBufferDart = Pointer<Void> Function(int streamHandle);
|
||||||
|
|
||||||
|
/// Native function signature for registering a frame-ready callback.
|
||||||
|
typedef _RegisterCallbackNative =
|
||||||
|
Void Function(
|
||||||
|
Int64 streamHandle,
|
||||||
|
Pointer<NativeFunction<Void Function(Int32)>> callback,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Dart-side function type for [_RegisterCallbackNative].
|
||||||
|
typedef _RegisterCallbackDart =
|
||||||
|
void Function(
|
||||||
|
int streamHandle,
|
||||||
|
Pointer<NativeFunction<Void Function(Int32)>> callback,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Native function signature for unregistering a frame-ready callback.
|
||||||
|
typedef _UnregisterCallbackNative = Void Function(Int64 streamHandle);
|
||||||
|
|
||||||
|
/// Dart-side function type for [_UnregisterCallbackNative].
|
||||||
|
typedef _UnregisterCallbackDart = void Function(int streamHandle);
|
||||||
|
|
||||||
|
/// Minimal interface for an image-stream frame poller, so the plugin can hold
|
||||||
|
/// either a real [ImageStreamFfi] or a test fake.
|
||||||
|
abstract interface class ImageStreamPoller {
|
||||||
|
/// Begins delivering frames to [controller].
|
||||||
|
void start(StreamController<CameraImageData> controller);
|
||||||
|
|
||||||
|
/// Stops delivering frames (cancels the poll timer).
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
/// Releases all resources.
|
||||||
|
void dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manages FFI-based image stream for a single camera.
|
||||||
|
///
|
||||||
|
/// Instead of receiving frame data through MethodChannel serialization
|
||||||
|
/// (3 copies per frame), this reads directly from a native shared buffer
|
||||||
|
/// via dart:ffi (1 copy per frame, into a Dart-owned Uint8List).
|
||||||
|
///
|
||||||
|
/// If FFI setup fails (symbols not found, library not loadable), returns
|
||||||
|
/// null from [tryCreate] and the caller falls back to MethodChannel.
|
||||||
|
class ImageStreamFfi implements ImageStreamPoller {
|
||||||
|
ImageStreamFfi._(
|
||||||
|
this._streamHandle,
|
||||||
|
this._getBuffer,
|
||||||
|
this._registerCallback,
|
||||||
|
this._unregisterCallback,
|
||||||
|
this._nativeNoopCallback,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The native stream handle used to identify this stream to native code.
|
||||||
|
final int _streamHandle;
|
||||||
|
|
||||||
|
/// FFI function to retrieve the shared buffer pointer.
|
||||||
|
final _GetBufferDart _getBuffer;
|
||||||
|
|
||||||
|
/// FFI function to register a frame-ready callback with native code.
|
||||||
|
final _RegisterCallbackDart _registerCallback;
|
||||||
|
|
||||||
|
/// FFI function to unregister the frame-ready callback.
|
||||||
|
final _UnregisterCallbackDart _unregisterCallback;
|
||||||
|
|
||||||
|
/// A native no-op callback symbol.
|
||||||
|
///
|
||||||
|
/// Registered with native to keep the shared-buffer FFI fast path active
|
||||||
|
/// without storing a Dart callback trampoline that can become invalid after
|
||||||
|
/// hot restart.
|
||||||
|
final Pointer<NativeFunction<Void Function(Int32)>> _nativeNoopCallback;
|
||||||
|
|
||||||
|
/// Polls the shared buffer for new sequence numbers.
|
||||||
|
Timer? _pollTimer;
|
||||||
|
|
||||||
|
/// Prevents re-entrant polling when frame decoding/copying takes longer than
|
||||||
|
/// the poll interval.
|
||||||
|
bool _pollInProgress = false;
|
||||||
|
|
||||||
|
/// The stream controller to which decoded frames are added.
|
||||||
|
StreamController<CameraImageData>? _controller;
|
||||||
|
|
||||||
|
/// The sequence number of the last frame delivered, used to skip duplicates.
|
||||||
|
int _lastSequence = 0;
|
||||||
|
|
||||||
|
/// Number of poll ticks executed since [start]. Test/diagnostic hook used to
|
||||||
|
/// verify the poller actually stops after the stream is torn down.
|
||||||
|
@visibleForTesting
|
||||||
|
int pollCount = 0;
|
||||||
|
|
||||||
|
/// Attempts to set up the FFI image stream.
|
||||||
|
///
|
||||||
|
/// Returns null if the native library or required symbols cannot be found,
|
||||||
|
/// allowing the caller to fall back to MethodChannel frame delivery.
|
||||||
|
static ImageStreamFfi? tryCreate(int streamHandle) {
|
||||||
|
try {
|
||||||
|
final lib = _loadNativeLibrary();
|
||||||
|
|
||||||
|
final getBuffer = lib.lookupFunction<_GetBufferNative, _GetBufferDart>(
|
||||||
|
'camera_desktop_get_image_stream_buffer',
|
||||||
|
);
|
||||||
|
final registerCallback = lib
|
||||||
|
.lookupFunction<_RegisterCallbackNative, _RegisterCallbackDart>(
|
||||||
|
'camera_desktop_register_image_stream_callback',
|
||||||
|
);
|
||||||
|
final unregisterCallback = lib
|
||||||
|
.lookupFunction<_UnregisterCallbackNative, _UnregisterCallbackDart>(
|
||||||
|
'camera_desktop_unregister_image_stream_callback',
|
||||||
|
);
|
||||||
|
final nativeNoopCallback = lib
|
||||||
|
.lookup<NativeFunction<Void Function(Int32)>>(
|
||||||
|
'camera_desktop_image_stream_noop_callback',
|
||||||
|
);
|
||||||
|
|
||||||
|
return ImageStreamFfi._(
|
||||||
|
streamHandle,
|
||||||
|
getBuffer,
|
||||||
|
registerCallback,
|
||||||
|
unregisterCallback,
|
||||||
|
nativeNoopCallback,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the native library containing the FFI image stream symbols.
|
||||||
|
///
|
||||||
|
/// On all desktop platforms, the plugin's native code is compiled into a
|
||||||
|
/// shared library loaded by the Flutter engine. [DynamicLibrary.process]
|
||||||
|
/// searches the current process's symbol table. On Windows, falls back to
|
||||||
|
/// explicitly opening `camera_desktop_plugin.dll` if process lookup fails.
|
||||||
|
static DynamicLibrary _loadNativeLibrary() {
|
||||||
|
if (Platform.isMacOS || Platform.isLinux) {
|
||||||
|
return DynamicLibrary.process();
|
||||||
|
}
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
try {
|
||||||
|
return DynamicLibrary.process();
|
||||||
|
} catch (_) {
|
||||||
|
return DynamicLibrary.open('camera_desktop_plugin.dll');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw UnsupportedError('Unsupported platform for FFI image stream');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers a native no-op callback and starts polling the shared buffer.
|
||||||
|
///
|
||||||
|
/// Using a native callback symbol (instead of [NativeCallable.listener])
|
||||||
|
/// avoids stale Dart callback metadata crashes during hot restart.
|
||||||
|
@override
|
||||||
|
void start(StreamController<CameraImageData> controller) {
|
||||||
|
_controller = controller;
|
||||||
|
_lastSequence = 0;
|
||||||
|
_pollInProgress = false;
|
||||||
|
|
||||||
|
_registerCallback(_streamHandle, _nativeNoopCallback);
|
||||||
|
_pollTimer?.cancel();
|
||||||
|
_pollTimer = Timer.periodic(
|
||||||
|
const Duration(milliseconds: 8),
|
||||||
|
(_) => _pollForFrame(),
|
||||||
|
);
|
||||||
|
_pollForFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Polls for one new frame and emits it if sequence has advanced.
|
||||||
|
void _pollForFrame() {
|
||||||
|
pollCount++;
|
||||||
|
if (_pollInProgress) return;
|
||||||
|
_pollInProgress = true;
|
||||||
|
try {
|
||||||
|
_readLatestFrame();
|
||||||
|
} finally {
|
||||||
|
_pollInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the shared buffer, skips duplicate
|
||||||
|
/// frames by comparing sequence numbers, creates a zero-copy view over
|
||||||
|
/// the native pixel buffer, then copies into a Dart-owned [Uint8List]
|
||||||
|
/// (1 copy, required by the platform interface contract).
|
||||||
|
void _readLatestFrame() {
|
||||||
|
final controller = _controller;
|
||||||
|
if (controller == null || controller.isClosed) return;
|
||||||
|
|
||||||
|
final bufPtr = _getBuffer(_streamHandle);
|
||||||
|
if (bufPtr == nullptr) return;
|
||||||
|
|
||||||
|
final buf = bufPtr.cast<ImageStreamBuffer>().ref;
|
||||||
|
if (buf.ready != 1) return;
|
||||||
|
|
||||||
|
if (buf.sequence <= _lastSequence) return;
|
||||||
|
_lastSequence = buf.sequence;
|
||||||
|
|
||||||
|
final width = buf.width;
|
||||||
|
final height = buf.height;
|
||||||
|
final bytesPerRow = buf.bytesPerRow;
|
||||||
|
final format = buf.format;
|
||||||
|
final dataSize = bytesPerRow * height;
|
||||||
|
|
||||||
|
final pixelsPtr = bufPtr.cast<Uint8>() + sizeOf<ImageStreamBuffer>();
|
||||||
|
final nativeView = pixelsPtr.asTypedList(dataSize);
|
||||||
|
|
||||||
|
final bytes = Uint8List.fromList(nativeView);
|
||||||
|
|
||||||
|
final rawFormat = format == 0 ? 'BGRA' : 'RGBA';
|
||||||
|
|
||||||
|
controller.add(
|
||||||
|
CameraImageData(
|
||||||
|
format: CameraImageFormat(ImageFormatGroup.bgra8888, raw: rawFormat),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
planes: [
|
||||||
|
CameraImagePlane(
|
||||||
|
bytes: bytes,
|
||||||
|
bytesPerRow: bytesPerRow,
|
||||||
|
bytesPerPixel: 4,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unregisters the native callback.
|
||||||
|
@override
|
||||||
|
void stop() {
|
||||||
|
_pollTimer?.cancel();
|
||||||
|
_pollTimer = null;
|
||||||
|
_unregisterCallback(_streamHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Releases all resources.
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
stop();
|
||||||
|
_controller = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
54
plugins/camera_desktop/linux/CMakeLists.txt
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
|
||||||
|
set(PROJECT_NAME "camera_desktop")
|
||||||
|
project(${PROJECT_NAME} LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(PLUGIN_NAME "camera_desktop_plugin")
|
||||||
|
|
||||||
|
list(APPEND PLUGIN_SOURCES
|
||||||
|
"camera_desktop_plugin.cc"
|
||||||
|
"camera_texture.cc"
|
||||||
|
"camera.cc"
|
||||||
|
"device_enumerator.cc"
|
||||||
|
"photo_handler.cc"
|
||||||
|
"record_handler.cc"
|
||||||
|
"image_stream_ffi.cc"
|
||||||
|
"pipewire_portal.cc"
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(${PLUGIN_NAME} SHARED
|
||||||
|
${PLUGIN_SOURCES}
|
||||||
|
)
|
||||||
|
|
||||||
|
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_include_directories(${PLUGIN_NAME} PRIVATE
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
|
|
||||||
|
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter)
|
||||||
|
target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK)
|
||||||
|
|
||||||
|
# GStreamer dependencies
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(GSTREAMER REQUIRED
|
||||||
|
gstreamer-1.0
|
||||||
|
gstreamer-app-1.0
|
||||||
|
gstreamer-video-1.0
|
||||||
|
)
|
||||||
|
target_include_directories(${PLUGIN_NAME} PRIVATE ${GSTREAMER_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(${PLUGIN_NAME} PRIVATE ${GSTREAMER_LIBRARIES})
|
||||||
|
|
||||||
|
set(camera_desktop_bundled_libraries
|
||||||
|
""
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
|
|
||||||
|
if(include_camera_desktop_tests)
|
||||||
|
add_subdirectory(test)
|
||||||
|
endif()
|
||||||
771
plugins/camera_desktop/linux/camera.cc
Normal file
@@ -0,0 +1,771 @@
|
|||||||
|
#include "camera.h"
|
||||||
|
#include "photo_handler.h"
|
||||||
|
|
||||||
|
#include <gio/gio.h>
|
||||||
|
#include <gst/video/video.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
static const guint kInitTimeoutMs = 8000;
|
||||||
|
|
||||||
|
Camera::Camera(int camera_id,
|
||||||
|
FlTextureRegistrar* texture_registrar,
|
||||||
|
FlMethodChannel* method_channel,
|
||||||
|
const CameraConfig& config)
|
||||||
|
: camera_id_(camera_id),
|
||||||
|
texture_id_(-1),
|
||||||
|
state_(CameraState::kCreated),
|
||||||
|
config_(config),
|
||||||
|
texture_registrar_(texture_registrar),
|
||||||
|
method_channel_(method_channel),
|
||||||
|
texture_(nullptr),
|
||||||
|
pipeline_(nullptr),
|
||||||
|
tee_(nullptr),
|
||||||
|
appsink_(nullptr),
|
||||||
|
videoflip_(nullptr),
|
||||||
|
bus_watch_id_(0),
|
||||||
|
init_timeout_id_(0),
|
||||||
|
record_handler_(std::make_unique<RecordHandler>()),
|
||||||
|
pending_init_call_(nullptr),
|
||||||
|
first_frame_received_(false),
|
||||||
|
preview_paused_(false),
|
||||||
|
image_streaming_(false),
|
||||||
|
image_stream_callback_(nullptr),
|
||||||
|
actual_width_(0),
|
||||||
|
actual_height_(0) {}
|
||||||
|
|
||||||
|
Camera::~Camera() {
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t Camera::RegisterTexture() {
|
||||||
|
texture_ = camera_texture_new();
|
||||||
|
FlTexture* fl_tex = camera_texture_as_fl_texture(texture_);
|
||||||
|
if (!fl_texture_registrar_register_texture(texture_registrar_, fl_tex)) {
|
||||||
|
g_info("[camera_desktop] Camera %d: failed to register Flutter texture",
|
||||||
|
camera_id_);
|
||||||
|
g_object_unref(texture_);
|
||||||
|
texture_ = nullptr;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
texture_id_ = fl_texture_get_id(fl_tex);
|
||||||
|
return texture_id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::Initialize(FlMethodCall* method_call) {
|
||||||
|
if (state_.load() != CameraState::kCreated) {
|
||||||
|
g_info("[camera_desktop] Camera %d: Initialize called in unexpected state %d",
|
||||||
|
camera_id_, static_cast<int>(state_.load()));
|
||||||
|
g_autoptr(FlValue) error_details = fl_value_new_null();
|
||||||
|
fl_method_call_respond_error(method_call, "already_initialized",
|
||||||
|
"Camera is already initialized or disposed",
|
||||||
|
error_details, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state_.store(CameraState::kInitializing);
|
||||||
|
pending_init_call_ = FL_METHOD_CALL(g_object_ref(method_call));
|
||||||
|
first_frame_received_.store(false);
|
||||||
|
|
||||||
|
g_info("[camera_desktop] Initializing camera %d (%s backend, %dx%d@%dfps)",
|
||||||
|
camera_id_,
|
||||||
|
config_.backend == CameraBackend::kPipeWire ? "PipeWire" : "V4L2",
|
||||||
|
config_.target_width, config_.target_height, config_.target_fps);
|
||||||
|
|
||||||
|
GError* error = nullptr;
|
||||||
|
if (!BuildPipeline(&error)) {
|
||||||
|
g_info("[camera_desktop] Camera %d: BuildPipeline failed: %s", camera_id_,
|
||||||
|
error ? error->message : "unknown error");
|
||||||
|
RespondToPendingInit(false, error->message);
|
||||||
|
g_error_free(error);
|
||||||
|
state_.store(CameraState::kCreated);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set pipeline to PLAYING.
|
||||||
|
GstStateChangeReturn ret =
|
||||||
|
gst_element_set_state(pipeline_, GST_STATE_PLAYING);
|
||||||
|
if (ret == GST_STATE_CHANGE_FAILURE) {
|
||||||
|
g_info("[camera_desktop] Camera %d: gst_element_set_state(PLAYING) failed",
|
||||||
|
camera_id_);
|
||||||
|
RespondToPendingInit(false, "Failed to start GStreamer pipeline");
|
||||||
|
gst_object_unref(pipeline_);
|
||||||
|
pipeline_ = nullptr;
|
||||||
|
appsink_ = nullptr;
|
||||||
|
state_.store(CameraState::kCreated);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a timeout for initialization, if no frame arrives in time, fail.
|
||||||
|
init_timeout_id_ =
|
||||||
|
g_timeout_add(kInitTimeoutMs, Camera::OnInitTimeout, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Camera::BuildPipeline(GError** error) {
|
||||||
|
// Build pipeline with a tee to support branching for recording:
|
||||||
|
// [source] ! videoconvert ! videoscale ! videorate ! caps ! tee name=t
|
||||||
|
// t. ! queue ! appsink (preview)
|
||||||
|
// t. ! [recording branch, added later by RecordHandler]
|
||||||
|
//
|
||||||
|
// videoscale/videorate let v4l2src negotiate a native mode (e.g. MJPEG or a
|
||||||
|
// lower YUYV resolution) instead of failing not-negotiated when the device
|
||||||
|
// cannot output the target size/fps in uncompressed YUV (common on USB cams).
|
||||||
|
const int w = config_.target_width;
|
||||||
|
const int h = config_.target_height;
|
||||||
|
const int fps = config_.target_fps;
|
||||||
|
|
||||||
|
gchar preview_tail[512];
|
||||||
|
g_snprintf(
|
||||||
|
preview_tail, sizeof(preview_tail),
|
||||||
|
"! videoflip name=flip method=horizontal-flip "
|
||||||
|
"! videoscale ! videorate "
|
||||||
|
"! video/x-raw,format=RGBA,width=%d,height=%d,framerate=%d/1 "
|
||||||
|
"! tee name=t "
|
||||||
|
"t. ! queue name=preview_queue ! "
|
||||||
|
"appsink name=sink emit-signals=true max-buffers=2 drop=true "
|
||||||
|
"sync=false",
|
||||||
|
w, h, fps);
|
||||||
|
|
||||||
|
gchar* pipeline_str = nullptr;
|
||||||
|
|
||||||
|
if (config_.backend == CameraBackend::kPipeWire) {
|
||||||
|
// PipeWire portal path: use pipewiresrc with the portal-provided fd.
|
||||||
|
std::string pw_node_id = config_.device_path.substr(3);
|
||||||
|
pipeline_str = g_strdup_printf(
|
||||||
|
"pipewiresrc fd=%d path=%s do-timestamp=true "
|
||||||
|
"! videoconvert "
|
||||||
|
"%s",
|
||||||
|
config_.pw_fd, pw_node_id.c_str(), preview_tail);
|
||||||
|
} else {
|
||||||
|
// V4L2: prefer MJPEG at the target size when the device actually supports
|
||||||
|
// it (native 720p/1080p on many USB cameras, and the only way some can
|
||||||
|
// deliver high resolutions within USB 2.0 bandwidth). The choice must be
|
||||||
|
// probed up front: gst_parse_launch() succeeds even for an MJPEG pipeline
|
||||||
|
// the camera cannot satisfy, so a non-MJPEG camera would otherwise fail at
|
||||||
|
// PLAYING with a runtime "not-negotiated" error instead of falling back.
|
||||||
|
// Only width/height are pinned on the MJPEG caps; the native frame rate is
|
||||||
|
// left to float and the downstream videorate adapts it to the target fps,
|
||||||
|
// so requesting an fps the camera does not offer natively in MJPEG does not
|
||||||
|
// break negotiation. Cameras exposing only raw formats (e.g. NV12/YUYV) use
|
||||||
|
// the unconstrained capture + scale path, which negotiates any native mode
|
||||||
|
// and adapts it to the target via videoscale/videorate.
|
||||||
|
if (DeviceEnumerator::SupportsMjpeg(config_.device_path, w, h)) {
|
||||||
|
pipeline_str = g_strdup_printf(
|
||||||
|
"v4l2src device=%s "
|
||||||
|
"! image/jpeg,width=%d,height=%d "
|
||||||
|
"! jpegdec ! videoconvert "
|
||||||
|
"%s",
|
||||||
|
config_.device_path.c_str(), w, h, preview_tail);
|
||||||
|
} else {
|
||||||
|
pipeline_str = g_strdup_printf("v4l2src device=%s "
|
||||||
|
"! videoconvert "
|
||||||
|
"%s",
|
||||||
|
config_.device_path.c_str(), preview_tail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pipeline_str) {
|
||||||
|
g_info("[camera_desktop] Pipeline: %s", pipeline_str);
|
||||||
|
pipeline_ = gst_parse_launch(pipeline_str, error);
|
||||||
|
g_free(pipeline_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pipeline_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the tee element (needed for recording branch attachment).
|
||||||
|
tee_ = gst_bin_get_by_name(GST_BIN(pipeline_), "t");
|
||||||
|
if (!tee_) {
|
||||||
|
g_info("[camera_desktop] Camera %d: tee element not found in pipeline",
|
||||||
|
camera_id_);
|
||||||
|
g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
||||||
|
"Failed to find tee in pipeline");
|
||||||
|
gst_object_unref(pipeline_);
|
||||||
|
pipeline_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Release our ref (pipeline holds one).
|
||||||
|
gst_object_unref(tee_);
|
||||||
|
|
||||||
|
// Get the videoflip element for runtime mirror toggling.
|
||||||
|
videoflip_ = gst_bin_get_by_name(GST_BIN(pipeline_), "flip");
|
||||||
|
if (videoflip_) {
|
||||||
|
gst_object_unref(videoflip_); // Pipeline holds the ref.
|
||||||
|
} else {
|
||||||
|
g_info("[camera_desktop] Camera %d: videoflip element not found in pipeline"
|
||||||
|
" (mirror toggling will be unavailable)", camera_id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the appsink element.
|
||||||
|
appsink_ = gst_bin_get_by_name(GST_BIN(pipeline_), "sink");
|
||||||
|
if (!appsink_) {
|
||||||
|
g_info("[camera_desktop] Camera %d: appsink element not found in pipeline",
|
||||||
|
camera_id_);
|
||||||
|
g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
||||||
|
"Failed to find appsink in pipeline");
|
||||||
|
gst_object_unref(pipeline_);
|
||||||
|
pipeline_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect the new-sample signal.
|
||||||
|
GstAppSinkCallbacks callbacks = {};
|
||||||
|
callbacks.new_sample = Camera::OnNewSample;
|
||||||
|
gst_app_sink_set_callbacks(GST_APP_SINK(appsink_), &callbacks, this,
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
// Set up bus watch for error messages.
|
||||||
|
GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_));
|
||||||
|
bus_watch_id_ = gst_bus_add_watch(bus, Camera::OnBusMessage, this);
|
||||||
|
gst_object_unref(bus);
|
||||||
|
|
||||||
|
// Release our ref on the appsink (pipeline holds one).
|
||||||
|
gst_object_unref(appsink_);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::RespondToPendingInit(bool success, const char* error_message) {
|
||||||
|
if (!pending_init_call_) return;
|
||||||
|
|
||||||
|
// Cancel the timeout.
|
||||||
|
if (init_timeout_id_ > 0) {
|
||||||
|
g_source_remove(init_timeout_id_);
|
||||||
|
init_timeout_id_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
g_autoptr(FlValue) result = fl_value_new_map();
|
||||||
|
fl_value_set_string_take(result, "previewWidth",
|
||||||
|
fl_value_new_float((double)actual_width_.load()));
|
||||||
|
fl_value_set_string_take(result, "previewHeight",
|
||||||
|
fl_value_new_float((double)actual_height_.load()));
|
||||||
|
fl_method_call_respond_success(pending_init_call_, result, nullptr);
|
||||||
|
} else {
|
||||||
|
g_autoptr(FlValue) details = fl_value_new_null();
|
||||||
|
fl_method_call_respond_error(pending_init_call_, "initialization_failed",
|
||||||
|
error_message ? error_message : "Unknown error",
|
||||||
|
details, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
g_object_unref(pending_init_call_);
|
||||||
|
pending_init_call_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
GstFlowReturn Camera::OnNewSample(GstAppSink* sink, gpointer user_data) {
|
||||||
|
Camera* self = static_cast<Camera*>(user_data);
|
||||||
|
|
||||||
|
GstSample* sample = gst_app_sink_pull_sample(sink);
|
||||||
|
if (!sample) {
|
||||||
|
g_warning("[camera_desktop] OnNewSample: gst_app_sink_pull_sample returned null");
|
||||||
|
return GST_FLOW_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
GstBuffer* buffer = gst_sample_get_buffer(sample);
|
||||||
|
GstCaps* caps = gst_sample_get_caps(sample);
|
||||||
|
|
||||||
|
GstVideoInfo info;
|
||||||
|
if (!gst_video_info_from_caps(&info, caps)) {
|
||||||
|
g_warning("[camera_desktop] OnNewSample: gst_video_info_from_caps failed");
|
||||||
|
gst_sample_unref(sample);
|
||||||
|
return GST_FLOW_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
int width = GST_VIDEO_INFO_WIDTH(&info);
|
||||||
|
int height = GST_VIDEO_INFO_HEIGHT(&info);
|
||||||
|
int stride = GST_VIDEO_INFO_PLANE_STRIDE(&info, 0);
|
||||||
|
|
||||||
|
GstMapInfo map;
|
||||||
|
if (!gst_buffer_map(buffer, &map, GST_MAP_READ)) {
|
||||||
|
g_warning("[camera_desktop] OnNewSample: gst_buffer_map failed");
|
||||||
|
gst_sample_unref(sample);
|
||||||
|
return GST_FLOW_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle first-frame initialization response.
|
||||||
|
// C-2: first_frame_received_ is atomic, safe cross-thread read/write.
|
||||||
|
bool is_first_frame = !self->first_frame_received_.load();
|
||||||
|
if (is_first_frame) {
|
||||||
|
g_info("[camera_desktop] Camera %d: first frame received (%dx%d)",
|
||||||
|
self->camera_id_, width, height);
|
||||||
|
self->first_frame_received_.store(true);
|
||||||
|
// H-2: actual_width_/height_ are atomic, safe cross-thread write.
|
||||||
|
self->actual_width_.store(width);
|
||||||
|
self->actual_height_.store(height);
|
||||||
|
self->state_.store(CameraState::kRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the texture only if preview is not paused (or if this is the first
|
||||||
|
// frame, which we need for initialization).
|
||||||
|
// C-3: preview_paused_ is atomic, safe cross-thread read.
|
||||||
|
if (!self->preview_paused_.load() || is_first_frame) {
|
||||||
|
if (stride == width * 4) {
|
||||||
|
// No padding, direct copy.
|
||||||
|
camera_texture_update(self->texture_, map.data, width, height);
|
||||||
|
} else {
|
||||||
|
// Stride has padding, copy row-by-row into a tight buffer.
|
||||||
|
// M-1 note: this intermediate allocation is unavoidable here since
|
||||||
|
// camera_texture_update requires a tightly-packed buffer.
|
||||||
|
size_t tight_size = (size_t)width * height * 4;
|
||||||
|
uint8_t* tight = (uint8_t*)g_malloc(tight_size);
|
||||||
|
for (int row = 0; row < height; row++) {
|
||||||
|
memcpy(tight + row * width * 4, map.data + row * stride, width * 4);
|
||||||
|
}
|
||||||
|
camera_texture_update(self->texture_, tight, width, height);
|
||||||
|
g_free(tight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify Flutter that a new frame is available.
|
||||||
|
fl_texture_registrar_mark_texture_frame_available(
|
||||||
|
self->texture_registrar_,
|
||||||
|
camera_texture_as_fl_texture(self->texture_));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send frame to Dart image stream if streaming is active.
|
||||||
|
if (self->image_streaming_.load()) {
|
||||||
|
// C-4: load the callback pointer atomically once, then use the local copy.
|
||||||
|
// This prevents a TOCTOU race where the pointer is nulled between the
|
||||||
|
// check and the call.
|
||||||
|
ImageStreamCallback cb = self->image_stream_callback_.load();
|
||||||
|
if (cb) {
|
||||||
|
// FFI path: write to shared buffer, notify Dart directly.
|
||||||
|
size_t frame_size = (size_t)width * height * 4;
|
||||||
|
size_t total_size = offsetof(Camera::ImageStreamBuffer, pixels) + frame_size;
|
||||||
|
|
||||||
|
if (self->image_stream_buffer_size_ < total_size) {
|
||||||
|
g_free(self->image_stream_buffer_);
|
||||||
|
self->image_stream_buffer_ =
|
||||||
|
(Camera::ImageStreamBuffer*)g_malloc(total_size);
|
||||||
|
self->image_stream_buffer_size_ = total_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* buf = self->image_stream_buffer_;
|
||||||
|
buf->ready = 0;
|
||||||
|
|
||||||
|
if (stride == width * 4) {
|
||||||
|
memcpy(buf->pixels, map.data, frame_size);
|
||||||
|
} else {
|
||||||
|
for (int row = 0; row < height; row++) {
|
||||||
|
memcpy(buf->pixels + row * width * 4, map.data + row * stride,
|
||||||
|
width * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf->width = width;
|
||||||
|
buf->height = height;
|
||||||
|
buf->bytes_per_row = width * 4;
|
||||||
|
buf->format = 1; // RGBA (Linux GStreamer pipeline)
|
||||||
|
buf->sequence = ++self->image_stream_sequence_;
|
||||||
|
|
||||||
|
// C-5: release fence, guarantees all pixel and metadata writes above
|
||||||
|
// are visible to any thread that subsequently observes ready == 1.
|
||||||
|
std::atomic_thread_fence(std::memory_order_release);
|
||||||
|
buf->ready = 1;
|
||||||
|
|
||||||
|
cb(self->camera_id_);
|
||||||
|
} else {
|
||||||
|
// Legacy MethodChannel fallback path.
|
||||||
|
size_t frame_size = (size_t)width * height * 4;
|
||||||
|
uint8_t* frame_copy = (uint8_t*)g_malloc(frame_size);
|
||||||
|
if (stride == width * 4) {
|
||||||
|
memcpy(frame_copy, map.data, frame_size);
|
||||||
|
} else {
|
||||||
|
for (int row = 0; row < height; row++) {
|
||||||
|
memcpy(frame_copy + row * width * 4, map.data + row * stride,
|
||||||
|
width * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ImageStreamData {
|
||||||
|
FlMethodChannel* channel;
|
||||||
|
int camera_id;
|
||||||
|
uint8_t* pixels;
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
size_t size;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto* stream_data = new ImageStreamData();
|
||||||
|
stream_data->channel = self->method_channel_;
|
||||||
|
stream_data->camera_id = self->camera_id_;
|
||||||
|
stream_data->pixels = frame_copy;
|
||||||
|
stream_data->width = width;
|
||||||
|
stream_data->height = height;
|
||||||
|
stream_data->size = frame_size;
|
||||||
|
|
||||||
|
g_idle_add(
|
||||||
|
[](gpointer user_data) -> gboolean {
|
||||||
|
auto* data = static_cast<ImageStreamData*>(user_data);
|
||||||
|
|
||||||
|
g_autoptr(FlValue) args = fl_value_new_map();
|
||||||
|
fl_value_set_string_take(args, "cameraId",
|
||||||
|
fl_value_new_int(data->camera_id));
|
||||||
|
fl_value_set_string_take(args, "width",
|
||||||
|
fl_value_new_int(data->width));
|
||||||
|
fl_value_set_string_take(args, "height",
|
||||||
|
fl_value_new_int(data->height));
|
||||||
|
fl_value_set_string_take(
|
||||||
|
args, "bytes",
|
||||||
|
fl_value_new_uint8_list(data->pixels, data->size));
|
||||||
|
|
||||||
|
fl_method_channel_invoke_method(data->channel, "imageStreamFrame",
|
||||||
|
args, nullptr, nullptr, nullptr);
|
||||||
|
|
||||||
|
g_free(data->pixels);
|
||||||
|
delete data;
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
},
|
||||||
|
stream_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gst_buffer_unmap(buffer, &map);
|
||||||
|
gst_sample_unref(sample);
|
||||||
|
|
||||||
|
// Dispatch init response to the main thread (OnNewSample runs on the
|
||||||
|
// GStreamer streaming thread, but fl_method_call_respond_* must be called
|
||||||
|
// from the main GLib thread).
|
||||||
|
if (is_first_frame) {
|
||||||
|
g_idle_add(
|
||||||
|
[](gpointer user_data) -> gboolean {
|
||||||
|
Camera* cam = static_cast<Camera*>(user_data);
|
||||||
|
cam->RespondToPendingInit(true, nullptr);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
},
|
||||||
|
self);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GST_FLOW_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean Camera::OnBusMessage(GstBus* bus, GstMessage* msg,
|
||||||
|
gpointer user_data) {
|
||||||
|
Camera* self = static_cast<Camera*>(user_data);
|
||||||
|
|
||||||
|
switch (GST_MESSAGE_TYPE(msg)) {
|
||||||
|
case GST_MESSAGE_ERROR: {
|
||||||
|
GError* err = nullptr;
|
||||||
|
gchar* debug = nullptr;
|
||||||
|
gst_message_parse_error(msg, &err, &debug);
|
||||||
|
|
||||||
|
g_info("[camera_desktop] Camera %d: GStreamer error: %s (debug: %s)",
|
||||||
|
self->camera_id_,
|
||||||
|
err ? err->message : "unknown",
|
||||||
|
debug ? debug : "none");
|
||||||
|
|
||||||
|
// C-2: load state_ atomically.
|
||||||
|
CameraState s = self->state_.load();
|
||||||
|
if (s == CameraState::kInitializing) {
|
||||||
|
self->RespondToPendingInit(false, err->message);
|
||||||
|
self->state_.store(CameraState::kCreated);
|
||||||
|
} else if (s == CameraState::kRunning || s == CameraState::kPaused) {
|
||||||
|
self->SendError(err->message);
|
||||||
|
}
|
||||||
|
|
||||||
|
g_error_free(err);
|
||||||
|
g_free(debug);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GST_MESSAGE_EOS: {
|
||||||
|
// End of stream (e.g., device unplugged).
|
||||||
|
CameraState s = self->state_.load();
|
||||||
|
if (s == CameraState::kRunning || s == CameraState::kPaused) {
|
||||||
|
self->SendError("Camera stream ended unexpectedly");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean Camera::OnInitTimeout(gpointer user_data) {
|
||||||
|
Camera* self = static_cast<Camera*>(user_data);
|
||||||
|
self->init_timeout_id_ = 0;
|
||||||
|
|
||||||
|
if (self->state_.load() == CameraState::kInitializing) {
|
||||||
|
self->RespondToPendingInit(
|
||||||
|
false, "Camera initialization timed out, no frames received");
|
||||||
|
if (self->pipeline_) {
|
||||||
|
gst_element_set_state(self->pipeline_, GST_STATE_NULL);
|
||||||
|
}
|
||||||
|
self->state_.store(CameraState::kCreated);
|
||||||
|
}
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::SendError(const std::string& description) {
|
||||||
|
g_autoptr(FlValue) args = fl_value_new_map();
|
||||||
|
fl_value_set_string_take(args, "cameraId",
|
||||||
|
fl_value_new_int(camera_id_));
|
||||||
|
fl_value_set_string_take(args, "description",
|
||||||
|
fl_value_new_string(description.c_str()));
|
||||||
|
fl_method_channel_invoke_method(method_channel_, "cameraError", args,
|
||||||
|
nullptr, nullptr, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::TakePicture(FlMethodCall* method_call) {
|
||||||
|
CameraState s = state_.load();
|
||||||
|
if (s != CameraState::kRunning && s != CameraState::kPaused) {
|
||||||
|
g_info("[camera_desktop] Camera %d: TakePicture called but camera is not"
|
||||||
|
" running (state=%d)", camera_id_, static_cast<int>(s));
|
||||||
|
g_autoptr(FlValue) details = fl_value_new_null();
|
||||||
|
fl_method_call_respond_error(method_call, "not_running",
|
||||||
|
"Camera is not running", details, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique temporary file path using an atomic sequence counter
|
||||||
|
// rather than the wall clock to prevent collisions under NTP corrections.
|
||||||
|
static std::atomic<int64_t> capture_seq{0};
|
||||||
|
gchar* tmp_path =
|
||||||
|
g_strdup_printf("%s/camera_desktop_%d_%" G_GINT64_FORMAT ".jpg",
|
||||||
|
g_get_tmp_dir(), camera_id_,
|
||||||
|
capture_seq.fetch_add(1, std::memory_order_relaxed));
|
||||||
|
|
||||||
|
// C-7: gst_video_convert_sample performs synchronous JPEG encoding which
|
||||||
|
// can take 30-200 ms at 1080p. Offload to a GLib thread-pool task so the
|
||||||
|
// main/UI thread is never blocked.
|
||||||
|
//
|
||||||
|
// Take a GStreamer reference to appsink_ so it stays alive for the duration
|
||||||
|
// of the task even if Dispose() is called concurrently.
|
||||||
|
struct TakePictureData {
|
||||||
|
GstElement* appsink; // holds a gst_object_ref
|
||||||
|
std::string output_path;
|
||||||
|
std::string error_message;
|
||||||
|
bool success;
|
||||||
|
FlMethodCall* method_call; // holds a g_object_ref
|
||||||
|
};
|
||||||
|
|
||||||
|
auto* d = new TakePictureData();
|
||||||
|
d->appsink = GST_ELEMENT(gst_object_ref(appsink_));
|
||||||
|
d->output_path = tmp_path;
|
||||||
|
d->success = false;
|
||||||
|
d->method_call = FL_METHOD_CALL(g_object_ref(method_call));
|
||||||
|
g_free(tmp_path);
|
||||||
|
|
||||||
|
GTask* task = g_task_new(nullptr, nullptr, nullptr, nullptr);
|
||||||
|
g_task_set_task_data(task, d, nullptr);
|
||||||
|
g_task_run_in_thread(
|
||||||
|
task,
|
||||||
|
[](GTask* /*task*/, gpointer /*source*/, gpointer task_data,
|
||||||
|
GCancellable* /*cancel*/) {
|
||||||
|
auto* d = static_cast<TakePictureData*>(task_data);
|
||||||
|
GError* err = nullptr;
|
||||||
|
d->success = PhotoHandler::TakePicture(d->appsink, d->output_path, &err);
|
||||||
|
gst_object_unref(d->appsink);
|
||||||
|
d->appsink = nullptr;
|
||||||
|
if (!d->success && err) {
|
||||||
|
d->error_message = err->message;
|
||||||
|
g_error_free(err);
|
||||||
|
}
|
||||||
|
// Marshal the method-channel response back to the main GLib thread.
|
||||||
|
g_idle_add(
|
||||||
|
[](gpointer p) -> gboolean {
|
||||||
|
auto* d = static_cast<TakePictureData*>(p);
|
||||||
|
if (d->success) {
|
||||||
|
g_autoptr(FlValue) result =
|
||||||
|
fl_value_new_string(d->output_path.c_str());
|
||||||
|
fl_method_call_respond_success(d->method_call, result, nullptr);
|
||||||
|
} else {
|
||||||
|
g_autoptr(FlValue) details = fl_value_new_null();
|
||||||
|
fl_method_call_respond_error(
|
||||||
|
d->method_call, "capture_failed",
|
||||||
|
d->error_message.empty() ? "Failed to capture image"
|
||||||
|
: d->error_message.c_str(),
|
||||||
|
details, nullptr);
|
||||||
|
}
|
||||||
|
g_object_unref(d->method_call);
|
||||||
|
delete d;
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
},
|
||||||
|
d);
|
||||||
|
});
|
||||||
|
g_object_unref(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::StartVideoRecording(FlMethodCall* method_call) {
|
||||||
|
CameraState s = state_.load();
|
||||||
|
if (s != CameraState::kRunning && s != CameraState::kPaused) {
|
||||||
|
g_info("[camera_desktop] Camera %d: StartVideoRecording called but camera"
|
||||||
|
" is not running (state=%d)", camera_id_, static_cast<int>(s));
|
||||||
|
g_autoptr(FlValue) details = fl_value_new_null();
|
||||||
|
fl_method_call_respond_error(method_call, "not_running",
|
||||||
|
"Camera is not running", details, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the recording branch on first use.
|
||||||
|
if (!record_handler_->is_recording()) {
|
||||||
|
GError* error = nullptr;
|
||||||
|
// H-2: load actual dimensions atomically, they are written from the
|
||||||
|
// GStreamer streaming thread on first frame.
|
||||||
|
if (!record_handler_->Setup(pipeline_, tee_,
|
||||||
|
actual_width_.load(), actual_height_.load(),
|
||||||
|
config_.target_fps,
|
||||||
|
config_.target_bitrate,
|
||||||
|
config_.audio_bitrate,
|
||||||
|
config_.enable_audio, &error)) {
|
||||||
|
g_info("[camera_desktop] Camera %d: RecordHandler::Setup failed: %s",
|
||||||
|
camera_id_, error ? error->message : "unknown error");
|
||||||
|
g_autoptr(FlValue) details = fl_value_new_null();
|
||||||
|
fl_method_call_respond_error(
|
||||||
|
method_call, "recording_setup_failed",
|
||||||
|
error ? error->message : "Failed to set up recording", details,
|
||||||
|
nullptr);
|
||||||
|
if (error) g_error_free(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (config_.enable_audio && !record_handler_->has_audio()) {
|
||||||
|
SendError("Audio recording was requested but audio setup failed. "
|
||||||
|
"Recording will continue without audio.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// H-6: derive extension from the muxer that was actually selected so the
|
||||||
|
// file's extension always matches its container format.
|
||||||
|
static std::atomic<int64_t> rec_seq{0};
|
||||||
|
gchar* tmp_path = g_strdup_printf(
|
||||||
|
"%s/camera_desktop_%d_%" G_GINT64_FORMAT ".%s",
|
||||||
|
g_get_tmp_dir(), camera_id_,
|
||||||
|
rec_seq.fetch_add(1, std::memory_order_relaxed),
|
||||||
|
record_handler_->output_extension());
|
||||||
|
|
||||||
|
GError* error = nullptr;
|
||||||
|
if (!record_handler_->StartRecording(tmp_path, &error)) {
|
||||||
|
g_autoptr(FlValue) details = fl_value_new_null();
|
||||||
|
fl_method_call_respond_error(
|
||||||
|
method_call, "recording_start_failed",
|
||||||
|
error ? error->message : "Failed to start recording", details,
|
||||||
|
nullptr);
|
||||||
|
if (error) g_error_free(error);
|
||||||
|
g_free(tmp_path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_free(tmp_path);
|
||||||
|
fl_method_call_respond_success(method_call, fl_value_new_null(), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::StopVideoRecording(FlMethodCall* method_call) {
|
||||||
|
if (!record_handler_->is_recording()) {
|
||||||
|
g_info("[camera_desktop] Camera %d: StopVideoRecording called but not"
|
||||||
|
" recording", camera_id_);
|
||||||
|
g_autoptr(FlValue) details = fl_value_new_null();
|
||||||
|
fl_method_call_respond_error(method_call, "not_recording",
|
||||||
|
"No recording in progress", details, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
record_handler_->StopRecording(method_call);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::StartImageStream() {
|
||||||
|
g_info("[camera_desktop] Camera %d: starting image stream", camera_id_);
|
||||||
|
image_streaming_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::StopImageStream() {
|
||||||
|
g_info("[camera_desktop] Camera %d: stopping image stream", camera_id_);
|
||||||
|
image_streaming_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::RegisterImageStreamCallback(void (*callback)(int32_t)) {
|
||||||
|
// C-4: atomic store, safe to write from main thread while GStreamer thread
|
||||||
|
// reads. The GStreamer thread loads the pointer once per frame (see
|
||||||
|
// OnNewSample) so it cannot race between check and call.
|
||||||
|
image_stream_callback_.store(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::UnregisterImageStreamCallback() {
|
||||||
|
image_stream_callback_.store(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::PausePreview() {
|
||||||
|
g_info("[camera_desktop] Camera %d: pausing preview", camera_id_);
|
||||||
|
// C-3: atomic store, safe cross-thread write.
|
||||||
|
preview_paused_.store(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::ResumePreview() {
|
||||||
|
g_info("[camera_desktop] Camera %d: resuming preview", camera_id_);
|
||||||
|
preview_paused_.store(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::SetMirror(bool mirrored) {
|
||||||
|
if (!videoflip_) {
|
||||||
|
g_info("[camera_desktop] Camera %d: SetMirror(%s) ignored, videoflip_ is null",
|
||||||
|
camera_id_, mirrored ? "true" : "false");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_info("[camera_desktop] Camera %d: SetMirror(%s)", camera_id_,
|
||||||
|
mirrored ? "true" : "false");
|
||||||
|
// GstVideoFlipMethod: 0 = none (identity), 4 = horizontal-flip
|
||||||
|
g_object_set(videoflip_, "method", mirrored ? 4 : 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::Dispose() {
|
||||||
|
g_info("[camera_desktop] Camera %d: disposing", camera_id_);
|
||||||
|
// C-2: use atomic exchange so the check-and-set is race-free. If two threads
|
||||||
|
// somehow call Dispose() concurrently, only one proceeds.
|
||||||
|
CameraState prev = state_.exchange(CameraState::kDisposing);
|
||||||
|
if (prev == CameraState::kDisposed || prev == CameraState::kDisposing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel pending init if still waiting (main thread → main thread, safe).
|
||||||
|
if (pending_init_call_) {
|
||||||
|
RespondToPendingInit(false, "Camera disposed during initialization");
|
||||||
|
}
|
||||||
|
|
||||||
|
// C-4: null the callback atomically BEFORE stopping the pipeline. This
|
||||||
|
// prevents new FFI callbacks from being registered while we're tearing down,
|
||||||
|
// but does NOT free the buffer yet, that must wait until the pipeline stops.
|
||||||
|
image_stream_callback_.store(nullptr);
|
||||||
|
|
||||||
|
// C-1 FIX: stop the pipeline BEFORE freeing image_stream_buffer_.
|
||||||
|
// gst_element_set_state(NULL) blocks until the GStreamer streaming thread
|
||||||
|
// (which runs OnNewSample and accesses image_stream_buffer_) is fully
|
||||||
|
// stopped. Freeing before this point was a use-after-free.
|
||||||
|
if (pipeline_) {
|
||||||
|
videoflip_ = nullptr;
|
||||||
|
gst_element_set_state(pipeline_, GST_STATE_NULL);
|
||||||
|
if (bus_watch_id_ > 0) {
|
||||||
|
g_source_remove(bus_watch_id_);
|
||||||
|
bus_watch_id_ = 0;
|
||||||
|
}
|
||||||
|
gst_object_unref(pipeline_);
|
||||||
|
pipeline_ = nullptr;
|
||||||
|
appsink_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now safe: the GStreamer streaming thread is guaranteed to have exited
|
||||||
|
// OnNewSample and will never access image_stream_buffer_ again.
|
||||||
|
if (image_stream_buffer_) {
|
||||||
|
g_free(image_stream_buffer_);
|
||||||
|
image_stream_buffer_ = nullptr;
|
||||||
|
image_stream_buffer_size_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister the texture.
|
||||||
|
if (texture_ && texture_registrar_) {
|
||||||
|
fl_texture_registrar_unregister_texture(
|
||||||
|
texture_registrar_, camera_texture_as_fl_texture(texture_));
|
||||||
|
g_object_unref(texture_);
|
||||||
|
texture_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send closing event to Dart.
|
||||||
|
g_autoptr(FlValue) args = fl_value_new_map();
|
||||||
|
fl_value_set_string_take(args, "cameraId",
|
||||||
|
fl_value_new_int(camera_id_));
|
||||||
|
fl_method_channel_invoke_method(method_channel_, "cameraClosing", args,
|
||||||
|
nullptr, nullptr, nullptr);
|
||||||
|
|
||||||
|
state_.store(CameraState::kDisposed);
|
||||||
|
g_info("[camera_desktop] Camera %d: disposed", camera_id_);
|
||||||
|
}
|
||||||
172
plugins/camera_desktop/linux/camera.h
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
#ifndef CAMERA_H_
|
||||||
|
#define CAMERA_H_
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
#include <gst/gst.h>
|
||||||
|
#include <gst/app/gstappsink.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "camera_texture.h"
|
||||||
|
#include "device_enumerator.h"
|
||||||
|
#include "record_handler.h"
|
||||||
|
|
||||||
|
enum class CameraBackend {
|
||||||
|
kV4L2, // Traditional /dev/video* + v4l2src
|
||||||
|
kPipeWire, // Portal-authorized pipewiresrc
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class CameraState {
|
||||||
|
kCreated,
|
||||||
|
kInitializing,
|
||||||
|
kRunning,
|
||||||
|
kPaused,
|
||||||
|
kDisposing,
|
||||||
|
kDisposed,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Alias for the image-stream callback function pointer type.
|
||||||
|
using ImageStreamCallback = void (*)(int32_t);
|
||||||
|
|
||||||
|
struct CameraConfig {
|
||||||
|
std::string device_path;
|
||||||
|
int resolution_preset;
|
||||||
|
bool enable_audio;
|
||||||
|
int target_width;
|
||||||
|
int target_height;
|
||||||
|
int target_fps;
|
||||||
|
int target_bitrate;
|
||||||
|
int audio_bitrate = 0;
|
||||||
|
CameraBackend backend = CameraBackend::kV4L2;
|
||||||
|
int pw_fd = -1; // PipeWire remote fd (only used when backend == kPipeWire)
|
||||||
|
};
|
||||||
|
|
||||||
|
class Camera {
|
||||||
|
public:
|
||||||
|
Camera(int camera_id,
|
||||||
|
FlTextureRegistrar* texture_registrar,
|
||||||
|
FlMethodChannel* method_channel,
|
||||||
|
const CameraConfig& config);
|
||||||
|
~Camera();
|
||||||
|
|
||||||
|
int camera_id() const { return camera_id_; }
|
||||||
|
int64_t texture_id() const { return texture_id_; }
|
||||||
|
CameraState state() const { return state_; }
|
||||||
|
|
||||||
|
// Allocates the texture and registers it. Must be called before Initialize.
|
||||||
|
// Returns the texture_id on success, -1 on failure.
|
||||||
|
int64_t RegisterTexture();
|
||||||
|
|
||||||
|
// Builds and starts the GStreamer pipeline. Responds to |method_call|
|
||||||
|
// asynchronously once the first frame arrives or an error/timeout occurs.
|
||||||
|
void Initialize(FlMethodCall* method_call);
|
||||||
|
|
||||||
|
// Captures a still image and saves it to a temporary JPEG file.
|
||||||
|
// Responds to |method_call| with the file path or an error.
|
||||||
|
void TakePicture(FlMethodCall* method_call);
|
||||||
|
|
||||||
|
// Pauses/resumes the live preview.
|
||||||
|
void PausePreview();
|
||||||
|
void ResumePreview();
|
||||||
|
|
||||||
|
// Starts video recording (silent, no audio).
|
||||||
|
void StartVideoRecording(FlMethodCall* method_call);
|
||||||
|
|
||||||
|
// Stops video recording and returns the file path.
|
||||||
|
void StopVideoRecording(FlMethodCall* method_call);
|
||||||
|
|
||||||
|
// Starts/stops sending raw frame data to Dart via method channel.
|
||||||
|
void StartImageStream();
|
||||||
|
void StopImageStream();
|
||||||
|
|
||||||
|
// FFI image stream access.
|
||||||
|
void* GetImageStreamBuffer() const { return image_stream_buffer_; }
|
||||||
|
void RegisterImageStreamCallback(void (*callback)(int32_t));
|
||||||
|
void UnregisterImageStreamCallback();
|
||||||
|
|
||||||
|
// Toggles horizontal mirroring on the live video feed.
|
||||||
|
void SetMirror(bool mirrored);
|
||||||
|
|
||||||
|
// Tears down the pipeline and releases all resources.
|
||||||
|
void Dispose();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool BuildPipeline(GError** error);
|
||||||
|
void RespondToPendingInit(bool success, const char* error_message);
|
||||||
|
|
||||||
|
// GStreamer callbacks (static with user_data = Camera*).
|
||||||
|
static GstFlowReturn OnNewSample(GstAppSink* sink, gpointer user_data);
|
||||||
|
static gboolean OnBusMessage(GstBus* bus, GstMessage* msg,
|
||||||
|
gpointer user_data);
|
||||||
|
static gboolean OnInitTimeout(gpointer user_data);
|
||||||
|
|
||||||
|
// Sends an error event to Dart via the method channel.
|
||||||
|
void SendError(const std::string& description);
|
||||||
|
|
||||||
|
int camera_id_;
|
||||||
|
int64_t texture_id_;
|
||||||
|
// state_ is written from the GStreamer streaming thread (OnNewSample) and
|
||||||
|
// read/written from the main thread. Must be atomic. (C-2)
|
||||||
|
std::atomic<CameraState> state_;
|
||||||
|
CameraConfig config_;
|
||||||
|
|
||||||
|
FlTextureRegistrar* texture_registrar_; // Not owned.
|
||||||
|
FlMethodChannel* method_channel_; // Not owned.
|
||||||
|
CameraTexture* texture_; // Owned (GObject ref).
|
||||||
|
|
||||||
|
GstElement* pipeline_;
|
||||||
|
GstElement* tee_; // For branching preview + recording.
|
||||||
|
GstElement* appsink_;
|
||||||
|
GstElement* videoflip_; // Named element in pipeline for mirror toggle.
|
||||||
|
guint bus_watch_id_;
|
||||||
|
guint init_timeout_id_;
|
||||||
|
|
||||||
|
std::unique_ptr<RecordHandler> record_handler_;
|
||||||
|
|
||||||
|
// Pending async initialization, stores the FlMethodCall until first frame.
|
||||||
|
// Only accessed from the main thread (set in Initialize, cleared in
|
||||||
|
// RespondToPendingInit which is always dispatched via g_idle_add to main).
|
||||||
|
FlMethodCall* pending_init_call_;
|
||||||
|
|
||||||
|
// Written from the GStreamer streaming thread; read from main thread. (C-2)
|
||||||
|
std::atomic<bool> first_frame_received_;
|
||||||
|
|
||||||
|
// Read from GStreamer streaming thread, written from main thread. (C-3)
|
||||||
|
std::atomic<bool> preview_paused_;
|
||||||
|
|
||||||
|
std::atomic<bool> image_streaming_;
|
||||||
|
|
||||||
|
// FFI image stream shared buffer.
|
||||||
|
// NOTE: The |ready| field acts as a release/acquire flag between the
|
||||||
|
// GStreamer thread (writer) and Dart (reader). The native side MUST issue a
|
||||||
|
// std::atomic_thread_fence(release) before writing ready=1, ensuring all
|
||||||
|
// pixel writes are visible before Dart observes ready==1. (C-5)
|
||||||
|
struct ImageStreamBuffer {
|
||||||
|
int64_t sequence;
|
||||||
|
int32_t width;
|
||||||
|
int32_t height;
|
||||||
|
int32_t bytes_per_row;
|
||||||
|
int32_t format; // 0=BGRA, 1=RGBA
|
||||||
|
int32_t ready; // 1=Dart may read, 0=native writing
|
||||||
|
int32_t _pad;
|
||||||
|
uint8_t pixels[]; // flexible array member
|
||||||
|
};
|
||||||
|
|
||||||
|
ImageStreamBuffer* image_stream_buffer_ = nullptr;
|
||||||
|
size_t image_stream_buffer_size_ = 0;
|
||||||
|
|
||||||
|
// Written from the main thread, read from the GStreamer streaming thread.
|
||||||
|
// Must be atomic to avoid data races and torn reads. (C-4)
|
||||||
|
std::atomic<ImageStreamCallback> image_stream_callback_{nullptr};
|
||||||
|
|
||||||
|
int64_t image_stream_sequence_ = 0;
|
||||||
|
|
||||||
|
// Written from the GStreamer streaming thread on first frame, read from the
|
||||||
|
// main thread in StartVideoRecording. Must be atomic. (H-2)
|
||||||
|
std::atomic<int> actual_width_;
|
||||||
|
std::atomic<int> actual_height_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // CAMERA_H_
|
||||||
467
plugins/camera_desktop/linux/camera_desktop_plugin.cc
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
#include "include/camera_desktop/camera_desktop_plugin.h"
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
#include <gst/gst.h>
|
||||||
|
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "camera.h"
|
||||||
|
#include "device_enumerator.h"
|
||||||
|
#include "pipewire_portal.h"
|
||||||
|
|
||||||
|
int64_t camera_desktop_ffi_register_stream_handle(Camera* camera);
|
||||||
|
void camera_desktop_ffi_release_stream_handle(int64_t stream_handle);
|
||||||
|
void camera_desktop_ffi_release_handles_for_camera(Camera* camera);
|
||||||
|
|
||||||
|
// Plugin data stored as an opaque C++ pointer inside the GObject struct.
|
||||||
|
struct PluginData {
|
||||||
|
std::map<int, std::unique_ptr<Camera>> cameras;
|
||||||
|
int next_camera_id = 1;
|
||||||
|
std::unique_ptr<PipeWirePortal> portal;
|
||||||
|
bool use_pipewire = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define CAMERA_DESKTOP_PLUGIN(obj) \
|
||||||
|
(G_TYPE_CHECK_INSTANCE_CAST((obj), camera_desktop_plugin_get_type(), \
|
||||||
|
CameraDesktopPlugin))
|
||||||
|
|
||||||
|
struct _CameraDesktopPlugin {
|
||||||
|
GObject parent_instance;
|
||||||
|
FlMethodChannel* channel;
|
||||||
|
FlTextureRegistrar* texture_registrar;
|
||||||
|
PluginData* data;
|
||||||
|
};
|
||||||
|
|
||||||
|
G_DEFINE_TYPE(CameraDesktopPlugin, camera_desktop_plugin, g_object_get_type())
|
||||||
|
|
||||||
|
// --- Method handlers ---
|
||||||
|
|
||||||
|
// Builds the Flutter response list from a vector of DeviceInfo.
|
||||||
|
static void respond_with_devices(FlMethodCall* method_call,
|
||||||
|
const std::vector<DeviceInfo>& devices) {
|
||||||
|
g_autoptr(FlValue) result = fl_value_new_list();
|
||||||
|
for (const auto& device : devices) {
|
||||||
|
g_autoptr(FlValue) entry = fl_value_new_map();
|
||||||
|
// Format: "Friendly Name (/dev/videoN)" or "Friendly Name (pw:42)"
|
||||||
|
std::string display_name =
|
||||||
|
device.name + " (" + device.device_path + ")";
|
||||||
|
fl_value_set_string_take(entry, "name",
|
||||||
|
fl_value_new_string(display_name.c_str()));
|
||||||
|
fl_value_set_string_take(entry, "lensDirection",
|
||||||
|
fl_value_new_int(device.lens_direction));
|
||||||
|
fl_value_set_string_take(entry, "sensorOrientation",
|
||||||
|
fl_value_new_int(device.sensor_orientation));
|
||||||
|
fl_value_append(result, entry);
|
||||||
|
}
|
||||||
|
fl_method_call_respond_success(method_call, result, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_available_cameras(CameraDesktopPlugin* self,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
if (self->data->use_pipewire && self->data->portal) {
|
||||||
|
// Async path: request portal permission, enumerate PipeWire nodes.
|
||||||
|
g_object_ref(method_call);
|
||||||
|
self->data->portal->EnumerateDevicesAsync(
|
||||||
|
[method_call](std::vector<DeviceInfo> devices) {
|
||||||
|
// If portal returned no devices, fall back to V4L2.
|
||||||
|
if (devices.empty()) {
|
||||||
|
g_info("[camera_desktop] PipeWire returned no cameras, falling back to V4L2");
|
||||||
|
devices = DeviceEnumerator::EnumerateDevices();
|
||||||
|
}
|
||||||
|
g_info("[camera_desktop] availableCameras returning %zu device(s)",
|
||||||
|
devices.size());
|
||||||
|
respond_with_devices(method_call, devices);
|
||||||
|
g_object_unref(method_call);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// V4L2 path (synchronous, for native Linux installs).
|
||||||
|
auto devices = DeviceEnumerator::EnumerateDevices();
|
||||||
|
g_info("[camera_desktop] availableCameras returning %zu device(s)",
|
||||||
|
devices.size());
|
||||||
|
respond_with_devices(method_call, devices);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_get_platform_capabilities(FlMethodCall* method_call) {
|
||||||
|
g_autoptr(FlValue) result = fl_value_new_map();
|
||||||
|
fl_value_set_string_take(result, "supportsMirrorControl",
|
||||||
|
fl_value_new_bool(true));
|
||||||
|
fl_value_set_string_take(result, "supportsVideoFpsControl",
|
||||||
|
fl_value_new_bool(true));
|
||||||
|
fl_value_set_string_take(result, "supportsVideoBitrateControl",
|
||||||
|
fl_value_new_bool(true));
|
||||||
|
fl_method_call_respond_success(method_call, result, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_create(CameraDesktopPlugin* self,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
FlValue* args = fl_method_call_get_args(method_call);
|
||||||
|
const char* camera_name =
|
||||||
|
fl_value_get_string(fl_value_lookup_string(args, "cameraName"));
|
||||||
|
int resolution_preset =
|
||||||
|
fl_value_get_int(fl_value_lookup_string(args, "resolutionPreset"));
|
||||||
|
FlValue* audio_val = fl_value_lookup_string(args, "enableAudio");
|
||||||
|
bool enable_audio = audio_val ? fl_value_get_bool(audio_val) : false;
|
||||||
|
|
||||||
|
int target_fps = 30;
|
||||||
|
FlValue* fps_val = fl_value_lookup_string(args, "fps");
|
||||||
|
if (fps_val && fl_value_get_type(fps_val) == FL_VALUE_TYPE_INT) {
|
||||||
|
target_fps = fl_value_get_int(fps_val);
|
||||||
|
} else if (fps_val && fl_value_get_type(fps_val) == FL_VALUE_TYPE_FLOAT) {
|
||||||
|
target_fps = static_cast<int>(fl_value_get_float(fps_val));
|
||||||
|
}
|
||||||
|
if (target_fps < 5) {
|
||||||
|
g_info("[camera_desktop] fps %d clamped to minimum 5", target_fps);
|
||||||
|
target_fps = 5;
|
||||||
|
}
|
||||||
|
if (target_fps > 60) {
|
||||||
|
g_info("[camera_desktop] fps %d clamped to maximum 60", target_fps);
|
||||||
|
target_fps = 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
int target_bitrate = 0;
|
||||||
|
FlValue* bitrate_val = fl_value_lookup_string(args, "videoBitrate");
|
||||||
|
if (bitrate_val && fl_value_get_type(bitrate_val) == FL_VALUE_TYPE_INT) {
|
||||||
|
target_bitrate = fl_value_get_int(bitrate_val);
|
||||||
|
} else if (bitrate_val &&
|
||||||
|
fl_value_get_type(bitrate_val) == FL_VALUE_TYPE_FLOAT) {
|
||||||
|
target_bitrate = static_cast<int>(fl_value_get_float(bitrate_val));
|
||||||
|
}
|
||||||
|
if (target_bitrate < 0) {
|
||||||
|
g_info("[camera_desktop] videoBitrate %d clamped to minimum 0",
|
||||||
|
target_bitrate);
|
||||||
|
target_bitrate = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int audio_bitrate = 0;
|
||||||
|
FlValue* audio_bitrate_val = fl_value_lookup_string(args, "audioBitrate");
|
||||||
|
if (audio_bitrate_val &&
|
||||||
|
fl_value_get_type(audio_bitrate_val) == FL_VALUE_TYPE_INT) {
|
||||||
|
audio_bitrate = fl_value_get_int(audio_bitrate_val);
|
||||||
|
} else if (audio_bitrate_val &&
|
||||||
|
fl_value_get_type(audio_bitrate_val) == FL_VALUE_TYPE_FLOAT) {
|
||||||
|
audio_bitrate = static_cast<int>(fl_value_get_float(audio_bitrate_val));
|
||||||
|
}
|
||||||
|
if (audio_bitrate < 0) {
|
||||||
|
g_info("[camera_desktop] audioBitrate %d clamped to minimum 0",
|
||||||
|
audio_bitrate);
|
||||||
|
audio_bitrate = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract device path from the camera name.
|
||||||
|
// Format: "Friendly Name (/dev/videoN)", extract the path in parentheses.
|
||||||
|
std::string name_str(camera_name);
|
||||||
|
std::string device_path;
|
||||||
|
size_t paren_start = name_str.rfind('(');
|
||||||
|
size_t paren_end = name_str.rfind(')');
|
||||||
|
if (paren_start != std::string::npos && paren_end != std::string::npos &&
|
||||||
|
paren_end > paren_start) {
|
||||||
|
device_path = name_str.substr(paren_start + 1, paren_end - paren_start - 1);
|
||||||
|
} else {
|
||||||
|
// Fallback: treat the whole name as a device path.
|
||||||
|
g_info("[camera_desktop] No parentheses found in camera name '%s',"
|
||||||
|
" using full name as device path", camera_name);
|
||||||
|
device_path = name_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect backend from device path prefix.
|
||||||
|
CameraBackend backend = CameraBackend::kV4L2;
|
||||||
|
if (device_path.size() > 3 && device_path.substr(0, 3) == "pw:") {
|
||||||
|
backend = CameraBackend::kPipeWire;
|
||||||
|
} else if (device_path.empty() || device_path.find("/dev/") != 0) {
|
||||||
|
g_autoptr(FlValue) details = fl_value_new_null();
|
||||||
|
fl_method_call_respond_error(method_call, "invalid_camera_name",
|
||||||
|
"Could not extract device path from camera name",
|
||||||
|
details, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enumerate resolutions based on backend.
|
||||||
|
std::vector<ResolutionInfo> resolutions;
|
||||||
|
if (backend == CameraBackend::kPipeWire) {
|
||||||
|
resolutions = PipeWirePortal::GetDefaultResolutions();
|
||||||
|
} else {
|
||||||
|
resolutions = DeviceEnumerator::EnumerateResolutions(device_path);
|
||||||
|
}
|
||||||
|
auto selected = DeviceEnumerator::SelectResolution(resolutions,
|
||||||
|
resolution_preset);
|
||||||
|
|
||||||
|
CameraConfig config;
|
||||||
|
config.device_path = device_path;
|
||||||
|
config.resolution_preset = resolution_preset;
|
||||||
|
config.enable_audio = enable_audio;
|
||||||
|
config.target_width = selected.width;
|
||||||
|
config.target_height = selected.height;
|
||||||
|
config.target_fps = target_fps > 0 ? target_fps : selected.max_fps;
|
||||||
|
config.target_bitrate = target_bitrate;
|
||||||
|
config.audio_bitrate = audio_bitrate;
|
||||||
|
config.backend = backend;
|
||||||
|
if (backend == CameraBackend::kPipeWire && self->data->portal) {
|
||||||
|
config.pw_fd = self->data->portal->pw_fd();
|
||||||
|
}
|
||||||
|
|
||||||
|
g_info("[camera_desktop] Creating camera: device=%s, backend=%s, %dx%d@%dfps",
|
||||||
|
config.device_path.c_str(),
|
||||||
|
config.backend == CameraBackend::kPipeWire ? "PipeWire" : "V4L2",
|
||||||
|
config.target_width, config.target_height, config.target_fps);
|
||||||
|
|
||||||
|
int camera_id = self->data->next_camera_id++;
|
||||||
|
auto camera = std::make_unique<Camera>(
|
||||||
|
camera_id, self->texture_registrar, self->channel, config);
|
||||||
|
|
||||||
|
int64_t texture_id = camera->RegisterTexture();
|
||||||
|
if (texture_id < 0) {
|
||||||
|
g_info("[camera_desktop] Camera %d: texture registration failed",
|
||||||
|
camera_id);
|
||||||
|
g_autoptr(FlValue) details = fl_value_new_null();
|
||||||
|
fl_method_call_respond_error(method_call, "texture_registration_failed",
|
||||||
|
"Failed to register Flutter texture",
|
||||||
|
details, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->data->cameras[camera_id] = std::move(camera);
|
||||||
|
|
||||||
|
g_autoptr(FlValue) result = fl_value_new_map();
|
||||||
|
fl_value_set_string_take(result, "cameraId",
|
||||||
|
fl_value_new_int(camera_id));
|
||||||
|
fl_value_set_string_take(result, "textureId",
|
||||||
|
fl_value_new_int(texture_id));
|
||||||
|
fl_method_call_respond_success(method_call, result, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Camera* find_camera(CameraDesktopPlugin* self,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
FlValue* args = fl_method_call_get_args(method_call);
|
||||||
|
int camera_id = fl_value_get_int(fl_value_lookup_string(args, "cameraId"));
|
||||||
|
auto it = self->data->cameras.find(camera_id);
|
||||||
|
if (it == self->data->cameras.end()) {
|
||||||
|
g_info("[camera_desktop] find_camera: camera_id %d not found", camera_id);
|
||||||
|
g_autoptr(FlValue) details = fl_value_new_null();
|
||||||
|
fl_method_call_respond_error(method_call, "camera_not_found",
|
||||||
|
"No camera found with the given ID",
|
||||||
|
details, nullptr);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return it->second.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_initialize(CameraDesktopPlugin* self,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
Camera* camera = find_camera(self, method_call);
|
||||||
|
if (!camera) return;
|
||||||
|
// Camera::Initialize responds asynchronously.
|
||||||
|
camera->Initialize(method_call);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_take_picture(CameraDesktopPlugin* self,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
Camera* camera = find_camera(self, method_call);
|
||||||
|
if (!camera) return;
|
||||||
|
camera->TakePicture(method_call);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_start_video_recording(CameraDesktopPlugin* self,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
Camera* camera = find_camera(self, method_call);
|
||||||
|
if (!camera) return;
|
||||||
|
camera->StartVideoRecording(method_call);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_stop_video_recording(CameraDesktopPlugin* self,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
Camera* camera = find_camera(self, method_call);
|
||||||
|
if (!camera) return;
|
||||||
|
camera->StopVideoRecording(method_call);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_start_image_stream(CameraDesktopPlugin* self,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
Camera* camera = find_camera(self, method_call);
|
||||||
|
if (!camera) return;
|
||||||
|
camera->StartImageStream();
|
||||||
|
const int64_t stream_handle = camera_desktop_ffi_register_stream_handle(camera);
|
||||||
|
FlValue* args = fl_method_call_get_args(method_call);
|
||||||
|
int camera_id = fl_value_get_int(fl_value_lookup_string(args, "cameraId"));
|
||||||
|
g_info("[camera_desktop] Camera %d: image stream started, streamHandle=%" G_GINT64_FORMAT,
|
||||||
|
camera_id, stream_handle);
|
||||||
|
g_autoptr(FlValue) result = fl_value_new_map();
|
||||||
|
fl_value_set_string_take(result, "streamHandle",
|
||||||
|
fl_value_new_int(stream_handle));
|
||||||
|
fl_method_call_respond_success(method_call, result, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_stop_image_stream(CameraDesktopPlugin* self,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
Camera* camera = find_camera(self, method_call);
|
||||||
|
if (!camera) return;
|
||||||
|
FlValue* args = fl_method_call_get_args(method_call);
|
||||||
|
int camera_id = fl_value_get_int(fl_value_lookup_string(args, "cameraId"));
|
||||||
|
FlValue* handle_val = fl_value_lookup_string(args, "streamHandle");
|
||||||
|
if (handle_val && fl_value_get_type(handle_val) == FL_VALUE_TYPE_INT) {
|
||||||
|
int64_t stream_handle = fl_value_get_int(handle_val);
|
||||||
|
g_info("[camera_desktop] Camera %d: stopping image stream,"
|
||||||
|
" streamHandle=%" G_GINT64_FORMAT, camera_id, stream_handle);
|
||||||
|
camera_desktop_ffi_release_stream_handle(stream_handle);
|
||||||
|
} else {
|
||||||
|
g_info("[camera_desktop] Camera %d: stopping image stream (no handle)",
|
||||||
|
camera_id);
|
||||||
|
}
|
||||||
|
camera->StopImageStream();
|
||||||
|
fl_method_call_respond_success(method_call, fl_value_new_null(), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_pause_preview(CameraDesktopPlugin* self,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
Camera* camera = find_camera(self, method_call);
|
||||||
|
if (!camera) return;
|
||||||
|
camera->PausePreview();
|
||||||
|
fl_method_call_respond_success(method_call, fl_value_new_null(), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_resume_preview(CameraDesktopPlugin* self,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
Camera* camera = find_camera(self, method_call);
|
||||||
|
if (!camera) return;
|
||||||
|
camera->ResumePreview();
|
||||||
|
fl_method_call_respond_success(method_call, fl_value_new_null(), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_set_mirror(CameraDesktopPlugin* self,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
Camera* camera = find_camera(self, method_call);
|
||||||
|
if (!camera) return;
|
||||||
|
|
||||||
|
FlValue* args = fl_method_call_get_args(method_call);
|
||||||
|
int camera_id = fl_value_get_int(fl_value_lookup_string(args, "cameraId"));
|
||||||
|
FlValue* mirrored_val = fl_value_lookup_string(args, "mirrored");
|
||||||
|
bool mirrored = mirrored_val ? fl_value_get_bool(mirrored_val) : true;
|
||||||
|
|
||||||
|
g_info("[camera_desktop] Camera %d: setMirror(%s)", camera_id,
|
||||||
|
mirrored ? "true" : "false");
|
||||||
|
camera->SetMirror(mirrored);
|
||||||
|
fl_method_call_respond_success(method_call, fl_value_new_null(), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_dispose(CameraDesktopPlugin* self,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
FlValue* args = fl_method_call_get_args(method_call);
|
||||||
|
int camera_id = fl_value_get_int(fl_value_lookup_string(args, "cameraId"));
|
||||||
|
|
||||||
|
g_info("[camera_desktop] handle_dispose: camera_id=%d", camera_id);
|
||||||
|
auto it = self->data->cameras.find(camera_id);
|
||||||
|
if (it != self->data->cameras.end()) {
|
||||||
|
camera_desktop_ffi_release_handles_for_camera(it->second.get());
|
||||||
|
it->second->Dispose();
|
||||||
|
self->data->cameras.erase(it);
|
||||||
|
}
|
||||||
|
fl_method_call_respond_success(method_call, fl_value_new_null(), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Plugin lifecycle ---
|
||||||
|
|
||||||
|
static void camera_desktop_plugin_handle_method_call(
|
||||||
|
CameraDesktopPlugin* self,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
const gchar* method = fl_method_call_get_name(method_call);
|
||||||
|
|
||||||
|
if (strcmp(method, "availableCameras") == 0) {
|
||||||
|
handle_available_cameras(self, method_call);
|
||||||
|
} else if (strcmp(method, "getPlatformCapabilities") == 0) {
|
||||||
|
handle_get_platform_capabilities(method_call);
|
||||||
|
} else if (strcmp(method, "create") == 0) {
|
||||||
|
handle_create(self, method_call);
|
||||||
|
} else if (strcmp(method, "initialize") == 0) {
|
||||||
|
handle_initialize(self, method_call);
|
||||||
|
} else if (strcmp(method, "takePicture") == 0) {
|
||||||
|
handle_take_picture(self, method_call);
|
||||||
|
} else if (strcmp(method, "startVideoRecording") == 0) {
|
||||||
|
handle_start_video_recording(self, method_call);
|
||||||
|
} else if (strcmp(method, "stopVideoRecording") == 0) {
|
||||||
|
handle_stop_video_recording(self, method_call);
|
||||||
|
} else if (strcmp(method, "startImageStream") == 0) {
|
||||||
|
handle_start_image_stream(self, method_call);
|
||||||
|
} else if (strcmp(method, "stopImageStream") == 0) {
|
||||||
|
handle_stop_image_stream(self, method_call);
|
||||||
|
} else if (strcmp(method, "pausePreview") == 0) {
|
||||||
|
handle_pause_preview(self, method_call);
|
||||||
|
} else if (strcmp(method, "resumePreview") == 0) {
|
||||||
|
handle_resume_preview(self, method_call);
|
||||||
|
} else if (strcmp(method, "setMirror") == 0) {
|
||||||
|
handle_set_mirror(self, method_call);
|
||||||
|
} else if (strcmp(method, "dispose") == 0) {
|
||||||
|
handle_dispose(self, method_call);
|
||||||
|
} else {
|
||||||
|
fl_method_call_respond_not_implemented(method_call, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void method_call_cb(FlMethodChannel* channel,
|
||||||
|
FlMethodCall* method_call,
|
||||||
|
gpointer user_data) {
|
||||||
|
CameraDesktopPlugin* plugin = CAMERA_DESKTOP_PLUGIN(user_data);
|
||||||
|
camera_desktop_plugin_handle_method_call(plugin, method_call);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void camera_desktop_plugin_dispose(GObject* object) {
|
||||||
|
CameraDesktopPlugin* self = CAMERA_DESKTOP_PLUGIN(object);
|
||||||
|
|
||||||
|
// Dispose all cameras.
|
||||||
|
if (self->data) {
|
||||||
|
g_info("[camera_desktop] Plugin teardown: disposing %zu camera(s)",
|
||||||
|
self->data->cameras.size());
|
||||||
|
for (auto& pair : self->data->cameras) {
|
||||||
|
camera_desktop_ffi_release_handles_for_camera(pair.second.get());
|
||||||
|
pair.second->Dispose();
|
||||||
|
}
|
||||||
|
delete self->data;
|
||||||
|
self->data = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_clear_object(&self->channel);
|
||||||
|
|
||||||
|
G_OBJECT_CLASS(camera_desktop_plugin_parent_class)->dispose(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void camera_desktop_plugin_class_init(CameraDesktopPluginClass* klass) {
|
||||||
|
G_OBJECT_CLASS(klass)->dispose = camera_desktop_plugin_dispose;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void camera_desktop_plugin_init(CameraDesktopPlugin* self) {
|
||||||
|
self->data = new PluginData();
|
||||||
|
self->data->use_pipewire = PipeWirePortal::ShouldUsePipeWire();
|
||||||
|
if (self->data->use_pipewire) {
|
||||||
|
g_info("[camera_desktop] Plugin init: PipeWire mode enabled");
|
||||||
|
self->data->portal = std::make_unique<PipeWirePortal>();
|
||||||
|
} else {
|
||||||
|
g_info("[camera_desktop] Plugin init: V4L2 mode (no PipeWire)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void camera_desktop_plugin_register_with_registrar(
|
||||||
|
FlPluginRegistrar* registrar) {
|
||||||
|
// Initialize GStreamer (safe to call multiple times).
|
||||||
|
gst_init(nullptr, nullptr);
|
||||||
|
g_info("[camera_desktop] GStreamer initialized (version %s)",
|
||||||
|
gst_version_string());
|
||||||
|
|
||||||
|
CameraDesktopPlugin* plugin = CAMERA_DESKTOP_PLUGIN(
|
||||||
|
g_object_new(camera_desktop_plugin_get_type(), nullptr));
|
||||||
|
|
||||||
|
plugin->texture_registrar =
|
||||||
|
fl_plugin_registrar_get_texture_registrar(registrar);
|
||||||
|
|
||||||
|
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
|
||||||
|
plugin->channel = fl_method_channel_new(
|
||||||
|
fl_plugin_registrar_get_messenger(registrar),
|
||||||
|
"plugins.flutter.io/camera_desktop", FL_METHOD_CODEC(codec));
|
||||||
|
fl_method_channel_set_method_call_handler(
|
||||||
|
plugin->channel, method_call_cb, g_object_ref(plugin), g_object_unref);
|
||||||
|
|
||||||
|
g_object_unref(plugin);
|
||||||
|
}
|
||||||
163
plugins/camera_desktop/linux/camera_texture.cc
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
#include "camera_texture.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
// Triple-buffer texture for safe GStreamer→Flutter frame delivery.
|
||||||
|
//
|
||||||
|
// - GStreamer streaming thread writes to buffers[write_idx].
|
||||||
|
// - After writing, it swaps write_idx ↔ ready_idx (under mutex).
|
||||||
|
// - Flutter render thread (copy_pixels) swaps ready_idx ↔ read_idx (under
|
||||||
|
// mutex) and returns buffers[read_idx]. This buffer is safe because neither
|
||||||
|
// the GStreamer thread nor the swap touches it until the next copy_pixels.
|
||||||
|
|
||||||
|
struct _CameraTexture {
|
||||||
|
FlPixelBufferTexture parent_instance;
|
||||||
|
|
||||||
|
uint8_t* buffers[3];
|
||||||
|
int write_idx;
|
||||||
|
int read_idx;
|
||||||
|
int ready_idx;
|
||||||
|
gboolean has_new_frame;
|
||||||
|
|
||||||
|
uint32_t width;
|
||||||
|
uint32_t height;
|
||||||
|
size_t buffer_size; // width * height * 4
|
||||||
|
|
||||||
|
GMutex mutex;
|
||||||
|
};
|
||||||
|
|
||||||
|
G_DEFINE_TYPE(CameraTexture, camera_texture,
|
||||||
|
fl_pixel_buffer_texture_get_type())
|
||||||
|
|
||||||
|
static gboolean camera_texture_copy_pixels_impl(
|
||||||
|
FlPixelBufferTexture* texture,
|
||||||
|
const uint8_t** out_buffer,
|
||||||
|
uint32_t* width,
|
||||||
|
uint32_t* height,
|
||||||
|
GError** error) {
|
||||||
|
CameraTexture* self = CAMERA_TEXTURE(texture);
|
||||||
|
|
||||||
|
g_mutex_lock(&self->mutex);
|
||||||
|
|
||||||
|
if (self->width == 0 || self->height == 0 ||
|
||||||
|
self->buffers[self->read_idx] == nullptr) {
|
||||||
|
g_mutex_unlock(&self->mutex);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap ready → read if a new frame is available.
|
||||||
|
if (self->has_new_frame) {
|
||||||
|
int tmp = self->read_idx;
|
||||||
|
self->read_idx = self->ready_idx;
|
||||||
|
self->ready_idx = tmp;
|
||||||
|
self->has_new_frame = FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out_buffer = self->buffers[self->read_idx];
|
||||||
|
*width = self->width;
|
||||||
|
*height = self->height;
|
||||||
|
|
||||||
|
g_mutex_unlock(&self->mutex);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void camera_texture_dispose(GObject* object) {
|
||||||
|
CameraTexture* self = CAMERA_TEXTURE(object);
|
||||||
|
|
||||||
|
g_mutex_lock(&self->mutex);
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
g_free(self->buffers[i]);
|
||||||
|
self->buffers[i] = nullptr;
|
||||||
|
}
|
||||||
|
self->width = 0;
|
||||||
|
self->height = 0;
|
||||||
|
self->buffer_size = 0;
|
||||||
|
g_mutex_unlock(&self->mutex);
|
||||||
|
|
||||||
|
g_mutex_clear(&self->mutex);
|
||||||
|
|
||||||
|
G_OBJECT_CLASS(camera_texture_parent_class)->dispose(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void camera_texture_class_init(CameraTextureClass* klass) {
|
||||||
|
G_OBJECT_CLASS(klass)->dispose = camera_texture_dispose;
|
||||||
|
FL_PIXEL_BUFFER_TEXTURE_CLASS(klass)->copy_pixels =
|
||||||
|
camera_texture_copy_pixels_impl;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void camera_texture_init(CameraTexture* self) {
|
||||||
|
g_mutex_init(&self->mutex);
|
||||||
|
self->write_idx = 0;
|
||||||
|
self->read_idx = 1;
|
||||||
|
self->ready_idx = 2;
|
||||||
|
self->has_new_frame = FALSE;
|
||||||
|
self->width = 0;
|
||||||
|
self->height = 0;
|
||||||
|
self->buffer_size = 0;
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
self->buffers[i] = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CameraTexture* camera_texture_new(void) {
|
||||||
|
return CAMERA_TEXTURE(g_object_new(CAMERA_TEXTURE_TYPE, nullptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
void camera_texture_update(CameraTexture* self,
|
||||||
|
const uint8_t* data,
|
||||||
|
uint32_t width,
|
||||||
|
uint32_t height) {
|
||||||
|
g_return_if_fail(CAMERA_IS_TEXTURE(self));
|
||||||
|
g_return_if_fail(data != nullptr);
|
||||||
|
|
||||||
|
size_t required = (size_t)width * height * 4;
|
||||||
|
|
||||||
|
// --- Phase 1: ensure buffers are allocated and capture write_idx. ---
|
||||||
|
// The lock is held briefly only to check/update dimensions and read
|
||||||
|
// write_idx. Reallocation (rare, only on resolution change) also happens
|
||||||
|
// here, under the lock, because copy_pixels_impl may be reading
|
||||||
|
// buffers[read_idx] concurrently and we must not free it mid-read.
|
||||||
|
g_mutex_lock(&self->mutex);
|
||||||
|
|
||||||
|
if (required != self->buffer_size) {
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
g_free(self->buffers[i]);
|
||||||
|
self->buffers[i] = (uint8_t*)g_malloc(required);
|
||||||
|
}
|
||||||
|
self->buffer_size = required;
|
||||||
|
self->width = width;
|
||||||
|
self->height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture the current write index while the lock is held. write_idx is
|
||||||
|
// exclusively owned by this thread (only this function ever modifies it),
|
||||||
|
// so it will not change between now and Phase 3.
|
||||||
|
int wi = self->write_idx;
|
||||||
|
|
||||||
|
g_mutex_unlock(&self->mutex);
|
||||||
|
|
||||||
|
// --- Phase 2: copy frame data into the write buffer (NO lock). ---
|
||||||
|
// C-6 FIX: The memcpy (up to 8 MB at 1080p) previously held the mutex for
|
||||||
|
// its full duration, which forced Flutter's render thread to stall every
|
||||||
|
// time it called copy_pixels_impl. Moving it outside the lock eliminates
|
||||||
|
// that contention. This is safe because:
|
||||||
|
// - write_idx is producer-exclusive (only this function touches it).
|
||||||
|
// - The consumer (copy_pixels_impl) only ever swaps ready_idx ↔ read_idx,
|
||||||
|
// never write_idx. So buffers[wi] is not touched by any other thread
|
||||||
|
// while we're here.
|
||||||
|
memcpy(self->buffers[wi], data, required);
|
||||||
|
|
||||||
|
// --- Phase 3: atomically swap write ↔ ready under the lock. ---
|
||||||
|
g_mutex_lock(&self->mutex);
|
||||||
|
|
||||||
|
int tmp = self->write_idx;
|
||||||
|
self->write_idx = self->ready_idx;
|
||||||
|
self->ready_idx = tmp;
|
||||||
|
self->has_new_frame = TRUE;
|
||||||
|
|
||||||
|
g_mutex_unlock(&self->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlTexture* camera_texture_as_fl_texture(CameraTexture* self) {
|
||||||
|
return FL_TEXTURE(self);
|
||||||
|
}
|
||||||
30
plugins/camera_desktop/linux/camera_texture.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#ifndef CAMERA_TEXTURE_H_
|
||||||
|
#define CAMERA_TEXTURE_H_
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
|
#define CAMERA_TEXTURE_TYPE (camera_texture_get_type())
|
||||||
|
G_DECLARE_FINAL_TYPE(CameraTexture, camera_texture, CAMERA, TEXTURE,
|
||||||
|
FlPixelBufferTexture)
|
||||||
|
|
||||||
|
// Creates a new CameraTexture instance.
|
||||||
|
CameraTexture* camera_texture_new(void);
|
||||||
|
|
||||||
|
// Updates the texture with new RGBA frame data.
|
||||||
|
// |data| must point to tightly-packed RGBA pixels (stride == width * 4).
|
||||||
|
// |width| and |height| are the frame dimensions.
|
||||||
|
// This is called from the GStreamer streaming thread; it writes to the
|
||||||
|
// internal write buffer and swaps it into the ready slot.
|
||||||
|
void camera_texture_update(CameraTexture* self,
|
||||||
|
const uint8_t* data,
|
||||||
|
uint32_t width,
|
||||||
|
uint32_t height);
|
||||||
|
|
||||||
|
// Returns the FlTexture base pointer (for registrar calls).
|
||||||
|
FlTexture* camera_texture_as_fl_texture(CameraTexture* self);
|
||||||
|
|
||||||
|
G_END_DECLS
|
||||||
|
|
||||||
|
#endif // CAMERA_TEXTURE_H_
|
||||||