Skip to content

Commit 7a487cf

Browse files
PipeWire camera portal support for Flatpak sandbox compatibility on Linux
1 parent fc3207d commit 7a487cf

12 files changed

Lines changed: 520 additions & 34 deletions

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 1.1.0
2+
3+
* Add PipeWire camera portal support for Flatpak sandbox compatibility on Linux
4+
* Automatically detect Flatpak environment and use `pipewiresrc` instead of `v4l2src`
5+
* Request camera access via `org.freedesktop.portal.Camera` D-Bus interface
6+
* Enumerate PipeWire camera nodes via `GstDeviceMonitor`
7+
* Fall back to V4L2 if portal is unavailable or user denies permission
8+
* No new build dependencies — uses GIO (D-Bus) and GStreamer APIs already linked
9+
110
## 1.0.8
211

312
* Fix macOS build failure on Xcode 26+ by removing unavailable `AVCaptureSessionInterruptionReasonKey` (iOS-only API)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Add `camera_desktop` alongside `camera` in your `pubspec.yaml`:
3030
```yaml
3131
dependencies:
3232
camera: ^0.11.0
33-
camera_desktop: ^0.0.8
33+
camera_desktop: ^1.1.0
3434
```
3535
3636
That's it. All three desktop platforms are covered, no additional packages needed.

example/pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ packages:
6363
path: ".."
6464
relative: true
6565
source: path
66-
version: "1.0.8"
66+
version: "1.1.0"
6767
camera_platform_interface:
6868
dependency: "direct main"
6969
description:

ios/camera_desktop.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'camera_desktop'
3-
s.version = '0.0.1'
3+
s.version = '1.1.0'
44
s.summary = 'Flutter camera plugin (iOS stub).'
55
s.description = <<-DESC
66
Flutter camera plugin for desktop platforms. iOS stub for platform declaration.

linux/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ list(APPEND PLUGIN_SOURCES
1313
"photo_handler.cc"
1414
"record_handler.cc"
1515
"image_stream_ffi.cc"
16+
"pipewire_portal.cc"
1617
)
1718

1819
add_library(${PLUGIN_NAME} SHARED

linux/camera.cc

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,40 @@ void Camera::Initialize(FlMethodCall* method_call) {
9292

9393
bool Camera::BuildPipeline(GError** error) {
9494
// Build pipeline with a tee to support branching for recording:
95-
// v4l2src ! videoconvert ! caps ! tee name=t
95+
// [source] ! videoconvert ! caps ! tee name=t
9696
// t. ! queue ! appsink (preview)
9797
// t. ! [recording branch, added later by RecordHandler]
98-
gchar* pipeline_str = g_strdup_printf(
99-
"v4l2src device=%s "
100-
"! videoconvert "
101-
"! videoflip name=flip method=horizontal-flip "
102-
"! video/x-raw,format=RGBA,width=%d,height=%d,framerate=%d/1 "
103-
"! tee name=t "
104-
"t. ! queue name=preview_queue ! "
105-
"appsink name=sink emit-signals=true max-buffers=2 drop=true "
106-
"sync=false",
107-
config_.device_path.c_str(), config_.target_width,
108-
config_.target_height, config_.target_fps);
98+
gchar* pipeline_str = nullptr;
99+
100+
if (config_.backend == CameraBackend::kPipeWire) {
101+
// PipeWire portal path: use pipewiresrc with the portal-provided fd.
102+
// Extract node id from "pw:<id>" device_path.
103+
std::string pw_node_id = config_.device_path.substr(3);
104+
pipeline_str = g_strdup_printf(
105+
"pipewiresrc fd=%d path=%s do-timestamp=true "
106+
"! videoconvert "
107+
"! videoflip name=flip method=horizontal-flip "
108+
"! video/x-raw,format=RGBA,width=%d,height=%d,framerate=%d/1 "
109+
"! tee name=t "
110+
"t. ! queue name=preview_queue ! "
111+
"appsink name=sink emit-signals=true max-buffers=2 drop=true "
112+
"sync=false",
113+
config_.pw_fd, pw_node_id.c_str(),
114+
config_.target_width, config_.target_height, config_.target_fps);
115+
} else {
116+
// V4L2 path (default for native Linux installs).
117+
pipeline_str = g_strdup_printf(
118+
"v4l2src device=%s "
119+
"! videoconvert "
120+
"! videoflip name=flip method=horizontal-flip "
121+
"! video/x-raw,format=RGBA,width=%d,height=%d,framerate=%d/1 "
122+
"! tee name=t "
123+
"t. ! queue name=preview_queue ! "
124+
"appsink name=sink emit-signals=true max-buffers=2 drop=true "
125+
"sync=false",
126+
config_.device_path.c_str(), config_.target_width,
127+
config_.target_height, config_.target_fps);
128+
}
109129

110130
pipeline_ = gst_parse_launch(pipeline_str, error);
111131
g_free(pipeline_str);

linux/camera.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
#include "device_enumerator.h"
1414
#include "record_handler.h"
1515

16+
enum class CameraBackend {
17+
kV4L2, // Traditional /dev/video* + v4l2src
18+
kPipeWire, // Portal-authorized pipewiresrc
19+
};
20+
1621
enum class CameraState {
1722
kCreated,
1823
kInitializing,
@@ -34,6 +39,8 @@ struct CameraConfig {
3439
int target_fps;
3540
int target_bitrate;
3641
int audio_bitrate = 0;
42+
CameraBackend backend = CameraBackend::kV4L2;
43+
int pw_fd = -1; // PipeWire remote fd (only used when backend == kPipeWire)
3744
};
3845

3946
class Camera {

linux/camera_desktop_plugin.cc

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#include "camera.h"
1313
#include "device_enumerator.h"
14+
#include "pipewire_portal.h"
1415

1516
int64_t camera_desktop_ffi_register_stream_handle(Camera* camera);
1617
void camera_desktop_ffi_release_stream_handle(int64_t stream_handle);
@@ -20,6 +21,8 @@ void camera_desktop_ffi_release_handles_for_camera(Camera* camera);
2021
struct PluginData {
2122
std::map<int, std::unique_ptr<Camera>> cameras;
2223
int next_camera_id = 1;
24+
std::unique_ptr<PipeWirePortal> portal;
25+
bool use_pipewire = false;
2326
};
2427

2528
#define CAMERA_DESKTOP_PLUGIN(obj) \
@@ -37,33 +40,48 @@ G_DEFINE_TYPE(CameraDesktopPlugin, camera_desktop_plugin, g_object_get_type())
3740

3841
// --- Method handlers ---
3942

40-
static void handle_available_cameras(CameraDesktopPlugin* self,
41-
FlMethodCall* method_call) {
42-
auto devices = DeviceEnumerator::EnumerateDevices();
43-
43+
// Builds the Flutter response list from a vector of DeviceInfo.
44+
static void respond_with_devices(FlMethodCall* method_call,
45+
const std::vector<DeviceInfo>& devices) {
4446
g_autoptr(FlValue) result = fl_value_new_list();
4547
for (const auto& device : devices) {
4648
g_autoptr(FlValue) entry = fl_value_new_map();
49+
// Format: "Friendly Name (/dev/videoN)" or "Friendly Name (pw:42)"
50+
std::string display_name =
51+
device.name + " (" + device.device_path + ")";
4752
fl_value_set_string_take(entry, "name",
48-
fl_value_new_string(device.name.c_str()));
53+
fl_value_new_string(display_name.c_str()));
4954
fl_value_set_string_take(entry, "lensDirection",
5055
fl_value_new_int(device.lens_direction));
5156
fl_value_set_string_take(entry, "sensorOrientation",
5257
fl_value_new_int(device.sensor_orientation));
53-
// Also pass the device path so Dart can pass it back in create().
54-
// The CameraDescription.name field will carry this.
55-
// Override name to include both friendly name and path for disambiguation.
56-
// Format: "Friendly Name (/dev/videoN)"
57-
std::string display_name =
58-
device.name + " (" + device.device_path + ")";
59-
fl_value_set_string(entry, "name",
60-
fl_value_new_string(display_name.c_str()));
6158
fl_value_append(result, entry);
6259
}
63-
6460
fl_method_call_respond_success(method_call, result, nullptr);
6561
}
6662

63+
static void handle_available_cameras(CameraDesktopPlugin* self,
64+
FlMethodCall* method_call) {
65+
if (self->data->use_pipewire && self->data->portal) {
66+
// Async path: request portal permission, enumerate PipeWire nodes.
67+
g_object_ref(method_call);
68+
self->data->portal->EnumerateDevicesAsync(
69+
[self, method_call](std::vector<DeviceInfo> devices) {
70+
// If portal returned no devices, fall back to V4L2.
71+
if (devices.empty()) {
72+
devices = DeviceEnumerator::EnumerateDevices();
73+
}
74+
respond_with_devices(method_call, devices);
75+
g_object_unref(method_call);
76+
});
77+
return;
78+
}
79+
80+
// V4L2 path (synchronous, for native Linux installs).
81+
auto devices = DeviceEnumerator::EnumerateDevices();
82+
respond_with_devices(method_call, devices);
83+
}
84+
6785
static void handle_get_platform_capabilities(FlMethodCall* method_call) {
6886
g_autoptr(FlValue) result = fl_value_new_map();
6987
fl_value_set_string_take(result, "supportsMirrorControl",
@@ -130,16 +148,25 @@ static void handle_create(CameraDesktopPlugin* self,
130148
device_path = name_str;
131149
}
132150

133-
if (device_path.empty() || device_path.find("/dev/") != 0) {
151+
// Detect backend from device path prefix.
152+
CameraBackend backend = CameraBackend::kV4L2;
153+
if (device_path.size() > 3 && device_path.substr(0, 3) == "pw:") {
154+
backend = CameraBackend::kPipeWire;
155+
} else if (device_path.empty() || device_path.find("/dev/") != 0) {
134156
g_autoptr(FlValue) details = fl_value_new_null();
135157
fl_method_call_respond_error(method_call, "invalid_camera_name",
136158
"Could not extract device path from camera name",
137159
details, nullptr);
138160
return;
139161
}
140162

141-
// Enumerate resolutions and select the best match for the preset.
142-
auto resolutions = DeviceEnumerator::EnumerateResolutions(device_path);
163+
// Enumerate resolutions based on backend.
164+
std::vector<ResolutionInfo> resolutions;
165+
if (backend == CameraBackend::kPipeWire) {
166+
resolutions = PipeWirePortal::GetDefaultResolutions();
167+
} else {
168+
resolutions = DeviceEnumerator::EnumerateResolutions(device_path);
169+
}
143170
auto selected = DeviceEnumerator::SelectResolution(resolutions,
144171
resolution_preset);
145172

@@ -152,6 +179,10 @@ static void handle_create(CameraDesktopPlugin* self,
152179
config.target_fps = target_fps > 0 ? target_fps : selected.max_fps;
153180
config.target_bitrate = target_bitrate;
154181
config.audio_bitrate = audio_bitrate;
182+
config.backend = backend;
183+
if (backend == CameraBackend::kPipeWire && self->data->portal) {
184+
config.pw_fd = self->data->portal->pw_fd();
185+
}
155186

156187
int camera_id = self->data->next_camera_id++;
157188
auto camera = std::make_unique<Camera>(
@@ -357,6 +388,10 @@ static void camera_desktop_plugin_class_init(CameraDesktopPluginClass* klass) {
357388

358389
static void camera_desktop_plugin_init(CameraDesktopPlugin* self) {
359390
self->data = new PluginData();
391+
self->data->use_pipewire = PipeWirePortal::ShouldUsePipeWire();
392+
if (self->data->use_pipewire) {
393+
self->data->portal = std::make_unique<PipeWirePortal>();
394+
}
360395
}
361396

362397
void camera_desktop_plugin_register_with_registrar(

0 commit comments

Comments
 (0)