feat(linux/xdgportal): implement event-driven capture (#4768)
This commit is contained in:
@@ -301,6 +301,8 @@ namespace egl {
|
|||||||
// PipeWire metadata
|
// PipeWire metadata
|
||||||
std::optional<uint64_t> pts;
|
std::optional<uint64_t> pts;
|
||||||
std::optional<uint64_t> seq;
|
std::optional<uint64_t> seq;
|
||||||
|
std::optional<bool> pw_damage;
|
||||||
|
std::optional<uint32_t> pw_flags;
|
||||||
};
|
};
|
||||||
|
|
||||||
class sws_t {
|
class sws_t {
|
||||||
|
|||||||
@@ -723,6 +723,24 @@ namespace portal {
|
|||||||
|
|
||||||
~pipewire_t() {
|
~pipewire_t() {
|
||||||
cleanup_stream();
|
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);
|
pw_thread_loop_destroy(loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -734,6 +752,14 @@ namespace portal {
|
|||||||
return stream_data.frame_cv;
|
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) {
|
void init(int stream_fd, int stream_node, std::shared_ptr<shared_state_t> shared_state) {
|
||||||
fd = stream_fd;
|
fd = stream_fd;
|
||||||
node = stream_node;
|
node = stream_node;
|
||||||
@@ -767,21 +793,8 @@ namespace portal {
|
|||||||
pw_stream_destroy(stream_data.stream);
|
pw_stream_destroy(stream_data.stream);
|
||||||
stream_data.stream = nullptr;
|
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_unlock(loop);
|
||||||
|
|
||||||
pw_thread_loop_stop(loop);
|
|
||||||
if (fd >= 0) {
|
|
||||||
close(fd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
session_cache_t::instance().invalidate();
|
session_cache_t::instance().invalidate();
|
||||||
}
|
}
|
||||||
@@ -843,14 +856,14 @@ namespace portal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Validate we have a buffer and a signal that it's "new"
|
// 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;
|
struct spa_buffer *buf = stream_data.current_buffer->buffer;
|
||||||
|
|
||||||
if (buf->datas[0].chunk->size != 0) {
|
if (buf->datas[0].chunk->size != 0) {
|
||||||
const auto img_descriptor = static_cast<egl::img_descriptor_t *>(img);
|
const auto img_descriptor = static_cast<egl::img_descriptor_t *>(img);
|
||||||
img_descriptor->frame_timestamp = std::chrono::steady_clock::now();
|
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 *>(
|
struct spa_meta_header *h = static_cast<struct spa_meta_header *>(
|
||||||
spa_buffer_find_meta_data(buf, SPA_META_Header, sizeof(*h))
|
spa_buffer_find_meta_data(buf, SPA_META_Header, sizeof(*h))
|
||||||
);
|
);
|
||||||
@@ -859,6 +872,23 @@ namespace portal {
|
|||||||
img_descriptor->pts = h->pts;
|
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) {
|
if (buf->datas[0].type == SPA_DATA_DmaBuf) {
|
||||||
img_descriptor->sd.width = stream_data.format.info.raw.size.width;
|
img_descriptor->sd.width = stream_data.format.info.raw.size.width;
|
||||||
img_descriptor->sd.height = stream_data.format.info.raw.size.height;
|
img_descriptor->sd.height = stream_data.format.info.raw.size.height;
|
||||||
@@ -874,10 +904,6 @@ namespace portal {
|
|||||||
// Point the encoder to the front buffer
|
// Point the encoder to the front buffer
|
||||||
img->data = stream_data.front_buffer->data();
|
img->data = stream_data.front_buffer->data();
|
||||||
img->row_pitch = stream_data.local_stride;
|
img->row_pitch = stream_data.local_stride;
|
||||||
|
|
||||||
// Reset flags
|
|
||||||
stream_data.frame_ready = false;
|
|
||||||
stream_data.current_buffer = nullptr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1088,7 +1114,7 @@ namespace portal {
|
|||||||
|
|
||||||
// Ack the buffer type and metadata
|
// Ack the buffer type and metadata
|
||||||
std::array<uint8_t, SPA_POD_BUFFER_SIZE> buffer;
|
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;
|
int n_params = 0;
|
||||||
struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
|
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)));
|
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))));
|
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;
|
params[n_params] = meta_param;
|
||||||
n_params++;
|
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);
|
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) {
|
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
|
// 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)) {
|
if (!pull_free_image_cb(img_out)) {
|
||||||
return platf::capture_e::interrupted;
|
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();
|
img_egl->reset();
|
||||||
pipewire.fill_img(img_egl);
|
pipewire.fill_img(img_egl);
|
||||||
|
|
||||||
// Check if we got valid data (either DMA-BUF fd or memory pointer)
|
// Check if we got valid data (either DMA-BUF fd or memory pointer), then filter duplicates
|
||||||
bool is_valid_data = (img_egl->sd.fds[0] >= 0 || img_egl->data != nullptr);
|
if ((img_egl->sd.fds[0] >= 0 || img_egl->data != nullptr) && !is_buffer_redundant(img_egl)) {
|
||||||
|
// Update frame metadata
|
||||||
// Duplicate detection: PipeWire seq increments on each new frame,
|
update_metadata(img_egl, retries);
|
||||||
// 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;
|
|
||||||
return platf::capture_e::ok;
|
return platf::capture_e::ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No valid frame yet, or it was a duplicate
|
// No valid frame yet, or it was a duplicate
|
||||||
auto now = std::chrono::steady_clock::now();
|
retries++;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
return platf::capture_e::timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<platf::img_t> alloc_img() override {
|
std::shared_ptr<platf::img_t> alloc_img() override {
|
||||||
@@ -1274,6 +1289,8 @@ namespace portal {
|
|||||||
stream_stopped.store(false);
|
stream_stopped.store(false);
|
||||||
previous_height.store(0);
|
previous_height.store(0);
|
||||||
previous_width.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;
|
return platf::capture_e::interrupted;
|
||||||
} else {
|
} else {
|
||||||
BOOST_LOG(warning) << "PipeWire stream disconnected. Forcing session reset."sv;
|
BOOST_LOG(warning) << "PipeWire stream disconnected. Forcing session reset."sv;
|
||||||
@@ -1349,6 +1366,44 @@ namespace portal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
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) {
|
static uint32_t lookup_pw_format(uint64_t fourcc) {
|
||||||
for (const auto &fmt : format_map) {
|
for (const auto &fmt : format_map) {
|
||||||
if (fmt.fourcc == 0) {
|
if (fmt.fourcc == 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user