add plugin camera desktop

This commit is contained in:
kyo
2026-06-22 09:39:31 +07:00
parent 92daeef0c5
commit 5407afbd6e
144 changed files with 15900 additions and 1 deletions

View 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
View 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/

View 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'

View 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

View 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.

View 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.

View 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.

View 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

View 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
}
}

View File

@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hugocornellier.camera_desktop">
</manifest>

View File

@@ -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)
}
}

View 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())

View 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.

View 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

View 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'

View 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.

View 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

View File

@@ -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)),
);
}

View File

@@ -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>>());
});
}

View 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),
),
);
}
}

View 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});
}

View 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),
),
],
),
),
),
);
},
),
),
);
}
}

View 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();
}
}

View 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),
);
},
),
],
),
);
}
}

View File

@@ -0,0 +1 @@
flutter/ephemeral

View 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()

View 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}
)

View File

@@ -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);
}

View File

@@ -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_

View File

@@ -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)

View 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}")

View 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);
}

View 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));
}

View 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_

View File

@@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -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"))
}

View 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

View 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

View File

@@ -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 */;
}

View File

@@ -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>

View File

@@ -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 = "&quot;$FLUTTER_ROOT&quot;/packages/flutter_tools/bin/macos_assemble.sh prepare&#10;">
<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>

View 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>

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -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>

View File

@@ -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.

View File

@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Debug.xcconfig"
#include "Warnings.xcconfig"

View File

@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Release.xcconfig"
#include "Warnings.xcconfig"

View File

@@ -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

View File

@@ -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>

View 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>

View File

@@ -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()
}
}

View File

@@ -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>

View File

@@ -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")
}
}

View 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"

View 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

View 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);
});
}

View 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/

View 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)

View 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}
)

View File

@@ -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"));
}

View File

@@ -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_

View File

@@ -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)

View 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)

View 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

View File

@@ -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);
}

View File

@@ -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_

View 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;
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -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>

View 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;
}

View 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_

View 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));
}
}

View 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_

View 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

View 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"),
]
)
]
)

View File

@@ -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)
}
}
}

View File

@@ -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>

View 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';

View 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;
}

View 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() {}
}

View 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) {}
}

View 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;
}
}

View 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()

View 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_);
}

View 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_

View 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);
}

View 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);
}

View 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_

Some files were not shown because too many files have changed in this diff Show More