feat(linux/xdgportal): implement event-driven capture (#4768)
This commit is contained in:
@@ -301,6 +301,8 @@ namespace egl {
|
||||
// PipeWire metadata
|
||||
std::optional<uint64_t> pts;
|
||||
std::optional<uint64_t> seq;
|
||||
std::optional<bool> pw_damage;
|
||||
std::optional<uint32_t> pw_flags;
|
||||
};
|
||||
|
||||
class sws_t {
|
||||
|
||||
@@ -723,6 +723,24 @@ namespace portal {
|
||||
|
||||
~pipewire_t() {
|
||||
cleanup_stream();
|
||||
|
||||
pw_thread_loop_lock(loop);
|
||||
|
||||
if (core) {
|
||||
pw_core_disconnect(core);
|
||||
core = nullptr;
|
||||
}
|
||||
if (context) {
|
||||
pw_context_destroy(context);
|
||||
context = nullptr;
|
||||
}
|
||||
|
||||
pw_thread_loop_unlock(loop);
|
||||
|
||||
pw_thread_loop_stop(loop);
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
pw_thread_loop_destroy(loop);
|
||||
}
|
||||
|
||||
@@ -734,6 +752,14 @@ namespace portal {
|
||||
return stream_data.frame_cv;
|
||||
}
|
||||
|
||||
bool is_frame_ready() const {
|
||||
return stream_data.frame_ready;
|
||||
}
|
||||
|
||||
void set_frame_ready(bool ready) {
|
||||
stream_data.frame_ready = ready;
|
||||
}
|
||||
|
||||
void init(int stream_fd, int stream_node, std::shared_ptr<shared_state_t> shared_state) {
|
||||
fd = stream_fd;
|
||||
node = stream_node;
|
||||
@@ -767,21 +793,8 @@ namespace portal {
|
||||
pw_stream_destroy(stream_data.stream);
|
||||
stream_data.stream = nullptr;
|
||||
}
|
||||
if (core) {
|
||||
pw_core_disconnect(core);
|
||||
core = nullptr;
|
||||
}
|
||||
if (context) {
|
||||
pw_context_destroy(context);
|
||||
context = nullptr;
|
||||
}
|
||||
|
||||
pw_thread_loop_unlock(loop);
|
||||
|
||||
pw_thread_loop_stop(loop);
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
session_cache_t::instance().invalidate();
|
||||
}
|
||||
@@ -843,14 +856,14 @@ namespace portal {
|
||||
}
|
||||
|
||||
// 2. Validate we have a buffer and a signal that it's "new"
|
||||
if (stream_data.current_buffer && stream_data.frame_ready) {
|
||||
if (stream_data.current_buffer) {
|
||||
struct spa_buffer *buf = stream_data.current_buffer->buffer;
|
||||
|
||||
if (buf->datas[0].chunk->size != 0) {
|
||||
const auto img_descriptor = static_cast<egl::img_descriptor_t *>(img);
|
||||
img_descriptor->frame_timestamp = std::chrono::steady_clock::now();
|
||||
|
||||
// Passthrough PipeWire metadata
|
||||
// PipeWire header metadata
|
||||
struct spa_meta_header *h = static_cast<struct spa_meta_header *>(
|
||||
spa_buffer_find_meta_data(buf, SPA_META_Header, sizeof(*h))
|
||||
);
|
||||
@@ -859,6 +872,23 @@ namespace portal {
|
||||
img_descriptor->pts = h->pts;
|
||||
}
|
||||
|
||||
// PipeWire flags
|
||||
if (buf->n_datas > 0) {
|
||||
img_descriptor->pw_flags = buf->datas[0].chunk->flags;
|
||||
}
|
||||
|
||||
// PipeWire damage metadata
|
||||
struct spa_meta_region *damage = (struct spa_meta_region *) spa_buffer_find_meta_data(
|
||||
stream_data.current_buffer->buffer,
|
||||
SPA_META_VideoDamage,
|
||||
sizeof(*damage)
|
||||
);
|
||||
if (damage) {
|
||||
img_descriptor->pw_damage = (damage->region.size.width > 0 && damage->region.size.height > 0);
|
||||
} else {
|
||||
img_descriptor->pw_damage = std::nullopt;
|
||||
}
|
||||
|
||||
if (buf->datas[0].type == SPA_DATA_DmaBuf) {
|
||||
img_descriptor->sd.width = stream_data.format.info.raw.size.width;
|
||||
img_descriptor->sd.height = stream_data.format.info.raw.size.height;
|
||||
@@ -874,10 +904,6 @@ namespace portal {
|
||||
// Point the encoder to the front buffer
|
||||
img->data = stream_data.front_buffer->data();
|
||||
img->row_pitch = stream_data.local_stride;
|
||||
|
||||
// Reset flags
|
||||
stream_data.frame_ready = false;
|
||||
stream_data.current_buffer = nullptr;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1088,7 +1114,7 @@ namespace portal {
|
||||
|
||||
// Ack the buffer type and metadata
|
||||
std::array<uint8_t, SPA_POD_BUFFER_SIZE> buffer;
|
||||
std::array<const struct spa_pod *, 2> params;
|
||||
std::array<const struct spa_pod *, 3> params;
|
||||
int n_params = 0;
|
||||
struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
|
||||
auto buffer_param = static_cast<const struct spa_pod *>(spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_dataType, SPA_POD_Int(buffer_types)));
|
||||
@@ -1097,6 +1123,10 @@ namespace portal {
|
||||
auto meta_param = static_cast<const struct spa_pod *>(spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))));
|
||||
params[n_params] = meta_param;
|
||||
n_params++;
|
||||
int videoDamageRegionCount = 16;
|
||||
auto damage_param = static_cast<const struct spa_pod *>(spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoDamage), SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int(sizeof(struct spa_meta_region) * videoDamageRegionCount, sizeof(struct spa_meta_region) * 1, sizeof(struct spa_meta_region) * videoDamageRegionCount)));
|
||||
params[n_params] = damage_param;
|
||||
n_params++;
|
||||
|
||||
pw_stream_update_params(d->stream, params.data(), n_params);
|
||||
}
|
||||
@@ -1192,48 +1222,33 @@ namespace portal {
|
||||
|
||||
platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool show_cursor) {
|
||||
// FIXME: show_cursor is ignored
|
||||
auto start_time = std::chrono::steady_clock::now();
|
||||
auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||
int retries = 0;
|
||||
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
if (!wait_for_frame(deadline)) {
|
||||
return stream_stopped.load() ? platf::capture_e::interrupted : platf::capture_e::timeout;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (!pull_free_image_cb(img_out)) {
|
||||
return platf::capture_e::interrupted;
|
||||
}
|
||||
|
||||
const auto img_egl = static_cast<egl::img_descriptor_t *>(img_out.get());
|
||||
auto *img_egl = static_cast<egl::img_descriptor_t *>(img_out.get());
|
||||
img_egl->reset();
|
||||
pipewire.fill_img(img_egl);
|
||||
|
||||
// Check if we got valid data (either DMA-BUF fd or memory pointer)
|
||||
bool is_valid_data = (img_egl->sd.fds[0] >= 0 || img_egl->data != nullptr);
|
||||
|
||||
// Duplicate detection: PipeWire seq increments on each new frame,
|
||||
// pts advances with each buffer update. Both must advance to accept frame.
|
||||
bool is_duplicate = (img_egl->seq.has_value() && img_egl->pts.has_value() && last_pts.has_value() && last_seq.has_value() && img_egl->pts.value() == last_pts.value() && img_egl->seq.value() == last_seq.value());
|
||||
|
||||
if (is_valid_data && !is_duplicate) {
|
||||
// Frame found; check deadline
|
||||
auto end_time = std::chrono::steady_clock::now();
|
||||
if (end_time - start_time > timeout) {
|
||||
return platf::capture_e::timeout;
|
||||
}
|
||||
|
||||
if (img_egl->seq.has_value() && img_egl->pts.has_value()) {
|
||||
last_seq = img_egl->seq.value();
|
||||
last_pts = img_egl->pts.value();
|
||||
}
|
||||
img_egl->sequence = ++sequence;
|
||||
// Check if we got valid data (either DMA-BUF fd or memory pointer), then filter duplicates
|
||||
if ((img_egl->sd.fds[0] >= 0 || img_egl->data != nullptr) && !is_buffer_redundant(img_egl)) {
|
||||
// Update frame metadata
|
||||
update_metadata(img_egl, retries);
|
||||
return platf::capture_e::ok;
|
||||
}
|
||||
|
||||
// No valid frame yet, or it was a duplicate
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (now - start_time >= timeout) {
|
||||
return platf::capture_e::timeout;
|
||||
}
|
||||
|
||||
std::unique_lock lock(pipewire.frame_mutex());
|
||||
pipewire.frame_cv().wait_until(lock, start_time + timeout);
|
||||
retries++;
|
||||
}
|
||||
return platf::capture_e::timeout;
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::img_t> alloc_img() override {
|
||||
@@ -1274,6 +1289,8 @@ namespace portal {
|
||||
stream_stopped.store(false);
|
||||
previous_height.store(0);
|
||||
previous_width.store(0);
|
||||
// Delay interrupt signal to give Portal time to detect change
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
|
||||
return platf::capture_e::interrupted;
|
||||
} else {
|
||||
BOOST_LOG(warning) << "PipeWire stream disconnected. Forcing session reset."sv;
|
||||
@@ -1349,6 +1366,44 @@ namespace portal {
|
||||
}
|
||||
|
||||
private:
|
||||
bool is_buffer_redundant(const egl::img_descriptor_t *img) {
|
||||
// Check for corrupted frame
|
||||
if (img->pw_flags.has_value() && (img->pw_flags.value() & SPA_CHUNK_FLAG_CORRUPTED)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If PTS is identical, only drop if damage metadata confirms no change
|
||||
if (img->pts.has_value() && last_pts.has_value() && img->pts.value() == last_pts.value()) {
|
||||
return img->pw_damage.has_value() && !img->pw_damage.value();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void update_metadata(egl::img_descriptor_t *img, int retries) {
|
||||
last_seq = img->seq;
|
||||
last_pts = img->pts;
|
||||
img->sequence = ++sequence;
|
||||
|
||||
if (retries > 0) {
|
||||
BOOST_LOG(debug) << "Processed frame after " << retries << " redundant events."sv;
|
||||
}
|
||||
}
|
||||
|
||||
bool wait_for_frame(std::chrono::steady_clock::time_point deadline) {
|
||||
std::unique_lock<std::mutex> lock(pipewire.frame_mutex());
|
||||
|
||||
bool success = pipewire.frame_cv().wait_until(lock, deadline, [&] {
|
||||
return pipewire.is_frame_ready() || stream_stopped.load();
|
||||
});
|
||||
|
||||
if (success && !stream_stopped.load()) {
|
||||
pipewire.set_frame_ready(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static uint32_t lookup_pw_format(uint64_t fourcc) {
|
||||
for (const auto &fmt : format_map) {
|
||||
if (fmt.fourcc == 0) {
|
||||
|
||||
Reference in New Issue
Block a user